diff --git a/backend/internal/application/services/document_service.go b/backend/internal/application/services/document_service.go index 0f7940b..3f8ee19 100644 --- a/backend/internal/application/services/document_service.go +++ b/backend/internal/application/services/document_service.go @@ -409,8 +409,8 @@ func (s *DocumentService) FindByReference(ctx context.Context, ref string, refTy } // FindOrCreateDocument performs smart lookup by URL/path/reference or creates new document if not found -func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error) { - logger.Logger.Info("Find or create document", "reference", ref) +func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) { + logger.Logger.Info("Find or create document", "reference", ref, "created_by", createdBy) refType := detectReferenceType(ref) logger.Logger.Debug("Reference type detected", "type", refType, "reference", ref) @@ -426,7 +426,7 @@ func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string) return doc, false, nil } - logger.Logger.Info("Document not found, creating new one", "reference", ref) + logger.Logger.Info("Document not found, creating new one", "reference", ref, "created_by", createdBy) var title string switch refType { @@ -441,6 +441,7 @@ func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string) createReq := CreateDocumentRequest{ Reference: ref, Title: title, + CreatedBy: createdBy, } if refType == ReferenceTypeReference { @@ -449,7 +450,7 @@ func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string) URL: "", } - doc, err := s.repo.Create(ctx, ref, input, "") + doc, err := s.repo.Create(ctx, ref, input, createdBy) if err != nil { logger.Logger.Error("Failed to create document with custom doc_id", "doc_id", ref, @@ -459,7 +460,8 @@ func (s *DocumentService) FindOrCreateDocument(ctx context.Context, ref string) logger.Logger.Info("Document created with custom doc_id", "doc_id", ref, - "title", title) + "title", title, + "created_by", createdBy) return doc, true, nil } diff --git a/backend/internal/application/services/document_service_duplicate_test.go b/backend/internal/application/services/document_service_duplicate_test.go index 602071a..e810f69 100644 --- a/backend/internal/application/services/document_service_duplicate_test.go +++ b/backend/internal/application/services/document_service_duplicate_test.go @@ -105,7 +105,7 @@ func TestFindOrCreateDocument_SameReferenceTwice(t *testing.T) { reference := "doc-123" // First call - should create document - doc1, isNew1, err := service.FindOrCreateDocument(ctx, reference) + doc1, isNew1, err := service.FindOrCreateDocument(ctx, reference, "") if err != nil { t.Fatalf("First FindOrCreateDocument failed: %v", err) } @@ -119,7 +119,7 @@ func TestFindOrCreateDocument_SameReferenceTwice(t *testing.T) { } // Second call with SAME reference - should find existing document - doc2, isNew2, err := service.FindOrCreateDocument(ctx, reference) + doc2, isNew2, err := service.FindOrCreateDocument(ctx, reference, "") if err != nil { t.Fatalf("Second FindOrCreateDocument failed: %v", err) } @@ -152,7 +152,7 @@ func TestFindOrCreateDocument_URLReference(t *testing.T) { urlRef := "https://example.com/policy.pdf" // First call - should create document - doc1, isNew1, err := service.FindOrCreateDocument(ctx, urlRef) + doc1, isNew1, err := service.FindOrCreateDocument(ctx, urlRef, "") if err != nil { t.Fatalf("First FindOrCreateDocument failed: %v", err) } @@ -164,7 +164,7 @@ func TestFindOrCreateDocument_URLReference(t *testing.T) { firstDocID := doc1.DocID // Second call with SAME URL - should find existing document - doc2, isNew2, err := service.FindOrCreateDocument(ctx, urlRef) + doc2, isNew2, err := service.FindOrCreateDocument(ctx, urlRef, "") if err != nil { t.Fatalf("Second FindOrCreateDocument failed: %v", err) } diff --git a/backend/internal/application/services/document_service_test.go b/backend/internal/application/services/document_service_test.go index aba0fb9..a755a0c 100644 --- a/backend/internal/application/services/document_service_test.go +++ b/backend/internal/application/services/document_service_test.go @@ -691,7 +691,7 @@ func TestDocumentService_FindOrCreateDocument_Found(t *testing.T) { service := NewDocumentService(mockRepo, &mockDocExpectedSignerRepoTest{}, nil) ctx := context.Background() - doc, created, err := service.FindOrCreateDocument(ctx, "https://example.com/existing.pdf") + doc, created, err := service.FindOrCreateDocument(ctx, "https://example.com/existing.pdf", "") if err != nil { t.Fatalf("FindOrCreateDocument failed: %v", err) @@ -723,7 +723,7 @@ func TestDocumentService_FindOrCreateDocument_CreateWithURL(t *testing.T) { service := NewDocumentService(mockRepo, &mockDocExpectedSignerRepoTest{}, nil) ctx := context.Background() - doc, created, err := service.FindOrCreateDocument(ctx, "https://example.com/new-doc.pdf") + doc, created, err := service.FindOrCreateDocument(ctx, "https://example.com/new-doc.pdf", "") if err != nil { t.Fatalf("FindOrCreateDocument failed: %v", err) @@ -759,7 +759,7 @@ func TestDocumentService_FindOrCreateDocument_CreateWithPath(t *testing.T) { service := NewDocumentService(mockRepo, &mockDocExpectedSignerRepoTest{}, nil) ctx := context.Background() - doc, created, err := service.FindOrCreateDocument(ctx, "/home/user/important-file.pdf") + doc, created, err := service.FindOrCreateDocument(ctx, "/home/user/important-file.pdf", "") if err != nil { t.Fatalf("FindOrCreateDocument failed: %v", err) @@ -805,7 +805,7 @@ func TestDocumentService_FindOrCreateDocument_CreateWithReference(t *testing.T) service := NewDocumentService(mockRepo, &mockDocExpectedSignerRepoTest{}, nil) ctx := context.Background() - doc, created, err := service.FindOrCreateDocument(ctx, "company-policy-2024") + doc, created, err := service.FindOrCreateDocument(ctx, "company-policy-2024", "") if err != nil { t.Fatalf("FindOrCreateDocument failed: %v", err) diff --git a/backend/internal/presentation/api/documents/handler.go b/backend/internal/presentation/api/documents/handler.go index b6ea9d2..3fabcd2 100644 --- a/backend/internal/presentation/api/documents/handler.go +++ b/backend/internal/presentation/api/documents/handler.go @@ -20,7 +20,7 @@ import ( // documentService defines the interface for document operations type documentService interface { CreateDocument(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error) - FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error) + FindOrCreateDocument(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) FindByReference(ctx context.Context, ref string, refType string) (*models.Document, error) List(ctx context.Context, limit, offset int) ([]*models.Document, error) Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error) @@ -483,7 +483,7 @@ func (h *Handler) HandleFindOrCreateDocument(w http.ResponseWriter, r *http.Requ } // User is authenticated, create the document - doc, isNew, err := h.documentService.FindOrCreateDocument(ctx, ref) + doc, isNew, err := h.documentService.FindOrCreateDocument(ctx, ref, user.Email) if err != nil { logger.Logger.Error("Failed to create document", "reference", ref, @@ -605,11 +605,9 @@ func (h *Handler) HandleListMyDocuments(w http.ResponseWriter, r *http.Request) UpdatedAt: doc.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } - if sigs, err := h.signatureService.GetDocumentSignatures(ctx, doc.DocID); err == nil { - dto.SignatureCount = len(sigs) - } - + // Get stats which correctly calculates SignedCount as expected signers who signed if stats, err := h.documentService.GetExpectedSignerStats(ctx, doc.DocID); err == nil { + dto.SignatureCount = stats.SignedCount dto.ExpectedSignerCount = stats.ExpectedCount } diff --git a/backend/internal/presentation/api/documents/handler_test.go b/backend/internal/presentation/api/documents/handler_test.go index bf93456..e98ffd3 100644 --- a/backend/internal/presentation/api/documents/handler_test.go +++ b/backend/internal/presentation/api/documents/handler_test.go @@ -99,7 +99,7 @@ func (m *mockAuthorizer) CanCreateDocument(_ context.Context, userEmail string) // Mock document service type mockDocumentService struct { createDocFunc func(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error) - findOrCreateDocFunc func(ctx context.Context, ref string) (*models.Document, bool, error) + findOrCreateDocFunc func(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) findByReferenceFunc func(ctx context.Context, ref string, refType string) (*models.Document, error) } @@ -110,9 +110,9 @@ func (m *mockDocumentService) CreateDocument(ctx context.Context, req services.C return testDoc, nil } -func (m *mockDocumentService) FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error) { +func (m *mockDocumentService) FindOrCreateDocument(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) { if m.findOrCreateDocFunc != nil { - return m.findOrCreateDocFunc(ctx, ref) + return m.findOrCreateDocFunc(ctx, ref, createdBy) } return testDoc, true, nil } @@ -524,7 +524,7 @@ func TestHandler_HandleFindOrCreateDocument_CreateNew(t *testing.T) { // Document not found - return nil, nil (not an error) return nil, nil }, - findOrCreateDocFunc: func(ctx context.Context, ref string) (*models.Document, bool, error) { + findOrCreateDocFunc: func(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) { assert.Equal(t, "https://example.com/new-doc.pdf", ref) return testDoc, true, nil }, diff --git a/backend/internal/presentation/api/router.go b/backend/internal/presentation/api/router.go index ea42fb3..c709f98 100644 --- a/backend/internal/presentation/api/router.go +++ b/backend/internal/presentation/api/router.go @@ -49,7 +49,7 @@ type signatureService interface { // documentService defines document operations type documentService interface { CreateDocument(ctx context.Context, req services.CreateDocumentRequest) (*models.Document, error) - FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error) + FindOrCreateDocument(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) FindByReference(ctx context.Context, ref string, refType string) (*models.Document, error) List(ctx context.Context, limit, offset int) ([]*models.Document, error) Search(ctx context.Context, query string, limit, offset int) ([]*models.Document, error) diff --git a/backend/pkg/web/middleware_embed.go b/backend/pkg/web/middleware_embed.go index fbbd58f..7916608 100644 --- a/backend/pkg/web/middleware_embed.go +++ b/backend/pkg/web/middleware_embed.go @@ -13,7 +13,7 @@ import ( ) type docService interface { - FindOrCreateDocument(ctx context.Context, ref string) (*models.Document, bool, error) + FindOrCreateDocument(ctx context.Context, ref string, createdBy string) (*models.Document, bool, error) } // webhookPublisher defines minimal publish capability @@ -60,7 +60,7 @@ func EmbedDocumentMiddleware( // Try to create document if it doesn't exist ctx := r.Context() - doc, isNew, err := docService.FindOrCreateDocument(ctx, docID) + doc, isNew, err := docService.FindOrCreateDocument(ctx, docID, "") if err != nil { logger.Logger.Error("Failed to find/create document for embed", "doc_id", docID, diff --git a/webapp/cypress.config.ts b/webapp/cypress.config.ts index 6ce5b12..05551c3 100644 --- a/webapp/cypress.config.ts +++ b/webapp/cypress.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ e2e: { baseUrl: 'http://localhost:8080', specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + excludeSpecPattern: ['cypress/e2e/demo.cy.ts'], supportFile: 'cypress/support/e2e.ts', fixturesFolder: 'cypress/fixtures', video: false, diff --git a/webapp/cypress/e2e/03-admin-signers-management.cy.ts b/webapp/cypress/e2e/03-admin-signers-management.cy.ts index d24d239..183393e 100644 --- a/webapp/cypress/e2e/03-admin-signers-management.cy.ts +++ b/webapp/cypress/e2e/03-admin-signers-management.cy.ts @@ -2,7 +2,6 @@ /// describe('Test 3: Admin - Expected Signers Management', () => { - const adminEmail = 'admin@test.com' const docId = 'test-admin-doc-' + Date.now() beforeEach(() => { @@ -61,9 +60,9 @@ describe('Test 3: Admin - Expected Signers Management', () => { cy.contains('Pending').should('be.visible') // Step 8: Verify stats - cy.contains('Expected').should('be.visible') + cy.contains('Expected readers').should('be.visible') cy.contains('3').should('be.visible') // 3 expected signers - cy.contains('Confirmed').parent().should('contain', '0') // 0 confirmed + cy.contains('Confirmed').parent().parent().should('contain', '0') // 0 confirmed }) it('should allow admin to remove expected signer', () => { diff --git a/webapp/cypress/e2e/04-admin-email-reminders.cy.ts b/webapp/cypress/e2e/04-admin-email-reminders.cy.ts index 4f99717..e1c3c56 100644 --- a/webapp/cypress/e2e/04-admin-email-reminders.cy.ts +++ b/webapp/cypress/e2e/04-admin-email-reminders.cy.ts @@ -2,7 +2,6 @@ /// describe('Test 4: Admin - Email Reminders', () => { - const adminEmail = 'admin@test.com' const docId = 'test-reminders-' + Date.now() const alice = 'alice@test.com' const bob = 'bob@test.com' @@ -57,8 +56,8 @@ describe('Test 4: Admin - Email Reminders', () => { cy.url({ timeout: 10000 }).should('include', `/admin/docs/${docId}`) // Step 7: Verify stats (1 signed, 1 pending) - cy.contains('Confirmed').parent().should('contain', '1') - cy.contains('Pending').parent().should('contain', '1') + cy.contains('Confirmed').parent().parent().should('contain', '1') + cy.contains('Pending').parent().parent().should('contain', '1') // Step 8: Send reminders to all pending cy.clearMailbox() // Clear previous emails diff --git a/webapp/cypress/e2e/07-admin-document-deletion.cy.ts b/webapp/cypress/e2e/07-admin-document-deletion.cy.ts index c055f5c..f395bce 100644 --- a/webapp/cypress/e2e/07-admin-document-deletion.cy.ts +++ b/webapp/cypress/e2e/07-admin-document-deletion.cy.ts @@ -2,7 +2,6 @@ /// describe('Test 7: Admin - Document Deletion', () => { - const adminEmail = 'admin@test.com' const testUser = 'deletetest@example.com' const docId = 'doc-to-delete-' + Date.now() diff --git a/webapp/cypress/e2e/09-complete-workflow.cy.ts b/webapp/cypress/e2e/09-complete-workflow.cy.ts index a8ddef3..d713656 100644 --- a/webapp/cypress/e2e/09-complete-workflow.cy.ts +++ b/webapp/cypress/e2e/09-complete-workflow.cy.ts @@ -2,7 +2,6 @@ /// describe('Test 9: Complete End-to-End Workflow', () => { - const adminEmail = 'admin@test.com' const alice = 'alice@test.com' const bob = 'bob@test.com' const charlie = 'charlie@test.com' @@ -36,8 +35,8 @@ describe('Test 9: Complete End-to-End Workflow', () => { cy.contains(charlie).should('be.visible') // Verify stats: 0/3 signed (0%) - cy.contains('Confirmed').parent().should('contain', '0') - cy.contains('Expected').parent().should('contain', '3') + cy.contains('Confirmed').parent().parent().should('contain', '0') + cy.contains('Expected readers').parent().parent().should('contain', '3') // ===== STEP 3: Admin sends reminders → 3 emails sent ===== cy.log('STEP 3: Admin sends reminders to all signers') @@ -69,8 +68,8 @@ describe('Test 9: Complete End-to-End Workflow', () => { cy.loginAsAdmin() cy.visit(`/admin/docs/${docId}`) - cy.contains('Confirmed', { timeout: 10000 }).parent().should('contain', '1') - cy.contains('Pending').parent().should('contain', '2') + cy.contains('Confirmed', { timeout: 10000 }).parent().parent().should('contain', '1') + cy.contains('Pending').parent().parent().should('contain', '2') // ===== STEP 6: Bob logs in and signs ===== cy.log('STEP 6: Bob signs the document') @@ -87,8 +86,8 @@ describe('Test 9: Complete End-to-End Workflow', () => { cy.loginAsAdmin() cy.visit(`/admin/docs/${docId}`) - cy.contains('Confirmed', { timeout: 10000 }).parent().should('contain', '2') - cy.contains('Pending').parent().should('contain', '1') + cy.contains('Confirmed', { timeout: 10000 }).parent().parent().should('contain', '2') + cy.contains('Pending').parent().parent().should('contain', '1') // ===== STEP 8: Admin sends new reminder → 1 email (charlie only) ===== cy.log('STEP 8: Admin sends reminder to remaining signer') @@ -137,8 +136,8 @@ describe('Test 9: Complete End-to-End Workflow', () => { cy.loginAsAdmin() cy.visit(`/admin/docs/${docId}`) - cy.contains('Confirmed', { timeout: 10000 }).parent().should('contain', '3') - cy.contains('Expected').parent().should('contain', '3') + cy.contains('Confirmed', { timeout: 10000 }).parent().parent().should('contain', '3') + cy.contains('Expected readers').parent().parent().should('contain', '3') // All signers should show "Confirmed" status cy.contains('tr', alice).should('contain', 'Confirmed') @@ -146,6 +145,6 @@ describe('Test 9: Complete End-to-End Workflow', () => { cy.contains('tr', charlie).should('contain', 'Confirmed') // No pending signers - cy.contains('Pending').parent().should('contain', '0') + cy.contains('Pending').parent().parent().should('contain', '0') }) }) diff --git a/webapp/cypress/e2e/10-unexpected-signatures.cy.ts b/webapp/cypress/e2e/10-unexpected-signatures.cy.ts index 813bd08..2392845 100644 --- a/webapp/cypress/e2e/10-unexpected-signatures.cy.ts +++ b/webapp/cypress/e2e/10-unexpected-signatures.cy.ts @@ -2,7 +2,6 @@ /// describe('Test 10: Unexpected Signatures Tracking', () => { - const adminEmail = 'admin@test.com' const alice = 'alice@test.com' const bob = 'bob@test.com' const charlie = 'charlie-unexpected@test.com' // Not in expected list @@ -34,8 +33,8 @@ describe('Test 10: Unexpected Signatures Tracking', () => { cy.contains(bob).should('be.visible') // Verify initial stats - cy.contains('Expected').parent().should('contain', '2') - cy.contains('Confirmed').parent().should('contain', '0') + cy.contains('Expected readers').parent().parent().should('contain', '2') + cy.contains('Confirmed').parent().parent().should('contain', '0') // ===== STEP 2: Charlie (not expected) accesses and signs the document ===== cy.log('STEP 2: Unexpected user (Charlie) signs the document') @@ -60,8 +59,8 @@ describe('Test 10: Unexpected Signatures Tracking', () => { cy.contains(bob).should('be.visible') // Stats should show: 0/2 expected signed, but there's an unexpected signature - cy.contains('Expected').parent().should('contain', '2') - cy.contains('Confirmed').parent().should('contain', '0') // 0 expected signed + cy.contains('Expected readers').parent().parent().should('contain', '2') + cy.contains('Confirmed').parent().parent().should('contain', '0') // 0 expected signed // Unexpected signatures section should exist cy.contains(/Unexpected|Additional.*confirmations/, { timeout: 10000 }).should('be.visible') @@ -90,8 +89,8 @@ describe('Test 10: Unexpected Signatures Tracking', () => { cy.visit(`/admin/docs/${docId}`) // Expected stats: 1/2 signed - cy.contains('Confirmed', { timeout: 10000 }).parent().should('contain', '1') - cy.contains('Expected').parent().should('contain', '2') + cy.contains('Confirmed', { timeout: 10000 }).parent().parent().should('contain', '1') + cy.contains('Expected readers').parent().parent().should('contain', '2') // Alice should show "Confirmed" in expected section cy.contains('tr', alice).should('contain', 'Confirmed') diff --git a/webapp/cypress/e2e/demo.cy.ts b/webapp/cypress/e2e/demo.cy.ts index 6c71c81..2ea2929 100644 --- a/webapp/cypress/e2e/demo.cy.ts +++ b/webapp/cypress/e2e/demo.cy.ts @@ -21,7 +21,6 @@ const PAUSE_LONG = 2500 // Pause for important moments const PAUSE_XLONG = 3500 // Extra long for key features describe('Ackify Demo Video', () => { - const adminEmail = 'admin@test.com' const alice = 'alice@demo.com' const bob = 'bob@demo.com' let docId: string diff --git a/webapp/cypress/fixtures/pdf-exemple.pdf b/webapp/cypress/fixtures/pdf-exemple.pdf new file mode 100644 index 0000000..60f685c Binary files /dev/null and b/webapp/cypress/fixtures/pdf-exemple.pdf differ diff --git a/webapp/cypress/tsconfig.json b/webapp/cypress/tsconfig.json new file mode 100644 index 0000000..cb42300 --- /dev/null +++ b/webapp/cypress/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "types": ["cypress", "node"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "extends": "../tsconfig.json" +} diff --git a/webapp/src/components/RemindersSection.vue b/webapp/src/components/RemindersSection.vue new file mode 100644 index 0000000..ce94036 --- /dev/null +++ b/webapp/src/components/RemindersSection.vue @@ -0,0 +1,136 @@ + + + + diff --git a/webapp/src/components/SignersSection.vue b/webapp/src/components/SignersSection.vue new file mode 100644 index 0000000..ac5dcc9 --- /dev/null +++ b/webapp/src/components/SignersSection.vue @@ -0,0 +1,296 @@ + + + + diff --git a/webapp/src/components/viewer/DocumentViewer.vue b/webapp/src/components/viewer/DocumentViewer.vue index fd48428..821ca7d 100644 --- a/webapp/src/components/viewer/DocumentViewer.vue +++ b/webapp/src/components/viewer/DocumentViewer.vue @@ -44,6 +44,7 @@ const emit = defineEmits<{ readComplete: [] checksumMismatch: [expected: string, actual: string] checksumVerified: [] + loadError: [error: string] }>() const { t } = useI18n() @@ -305,6 +306,14 @@ watch(proxyUrl, (newUrl) => { } }, { immediate: true }) +// Watch for errors and emit loadError event +watch([error, contentError], ([newError, newContentError]) => { + const errorMsg = newError || newContentError + if (errorMsg) { + emit('loadError', errorMsg) + } +}) + onMounted(() => { setTimeout(checkContentFits, 500) }) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 0540d99..0d7e24c 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -125,10 +125,12 @@ "description": "Veuillez patienter pendant que nous préparons le document pour la signature." }, "external": { - "title": "Document externe", - "description": "Veuillez lire le document ci-dessous avant de confirmer votre lecture.", - "openDocument": "Ouvrir le document", - "noUrl": "Aucune URL disponible pour ce document" + "title": "Externes Dokument", + "description": "Bitte lesen Sie das Dokument, bevor Sie Ihre Lesung bestätigen.", + "descriptionWithUrl": "Dieses Dokument ist nicht direkt zugänglich. Bitte öffnen Sie es über den unten stehenden Link, lesen Sie es und kehren Sie dann zurück, um Ihre Lesung zu bestätigen.", + "documentUrl": "Dokument-URL", + "openDocument": "Dokument öffnen", + "noUrl": "Keine URL für dieses Dokument verfügbar" }, "alreadySigned": { "title": "Lecture déjà confirmée", @@ -580,15 +582,15 @@ "nameLabel": "Name", "namePlaceholder": "Vollständiger Name", "reader": "Leser", - "readers": "✓ Lecteurs attendus", + "readers": "Erwartete Leser", "user": "Benutzer", "status": "Status", - "statusConfirmed": "✓ Confirmé", - "statusPending": "⏳ En attente", + "statusConfirmed": "Bestätigt", + "statusPending": "Ausstehend", "confirmedOn": "Bestätigt am", "noExpectedSigners": "Keine erwarteten Leser", "noSignatures": "Keine Bestätigungen", - "reminders": "📧 Relances par email", + "reminders": "E-Mail Erinnerungen", "remindersDescription": "Envoyer des rappels aux lecteurs en attente de confirmation", "remindersSent": "Gesendete Erinnerungen", "toRemind": "Zu erinnern", @@ -598,9 +600,9 @@ "sendReminders": "Envoyer les relances", "sendToAll": "Envoyer à tous les lecteurs en attente ({count})", "sendToSelected": "Envoyer uniquement aux sélectionnés ({count})", - "allContacted": "✓ Tous les lecteurs attendus ont été contactés ou ont confirmé", - "emailServiceDisabled": "⚠️ Le service d'envoi d'emails est actuellement désactivé. L'historique des rappels reste visible, mais l'envoi de nouveaux rappels n'est pas disponible.", - "unexpectedSignatures": "⚠ Confirmations de lecture complémentaires", + "allContacted": "Alle erwarteten Leser wurden kontaktiert oder haben bestätigt", + "emailServiceDisabled": "Der E-Mail-Dienst ist derzeit deaktiviert. Der Erinnerungsverlauf bleibt sichtbar, aber das Senden neuer Erinnerungen ist nicht verfügbar.", + "unexpectedSignatures": "Zusätzliche Lesebestätigungen", "unexpectedDescription": "Utilisateurs ayant confirmé mais non présents dans la liste des lecteurs attendus", "createdBy": "Créé par {by} le {date}", "saving": "Enregistrement...", @@ -614,18 +616,18 @@ "remindersSentGeneric": "Relances envoyées avec succès", "confirmSendReminders": "Envoyer des relances à {count} lecteur(s) en attente de confirmation ?", "confirmSendRemindersSelected": "Envoyer des relances à {count} lecteur(s) sélectionné(s) ?", - "confirmSendRemindersTitle": "📧 Envoyer des relances", - "removeSignerTitle": "⚠️ Retirer le lecteur attendu", + "confirmSendRemindersTitle": "Erinnerungen senden", + "removeSignerTitle": "Erwarteten Leser entfernen", "removeSignerMessage": "Retirer {email} de la liste des lecteurs attendus ?", "metadataWarning": { - "title": "⚠️ Attention : Invalidation des signatures", + "title": "Achtung: Signaturungültigmachung", "description": "Vous êtes sur le point de modifier des informations critiques du document (URL, checksum, algorithme ou description).", "warning": "Cette modification entraînera l'invalidation de toutes les signatures existantes, car elles sont liées cryptographiquement au contenu actuel du document.", "currentSignatures": "Signatures actuelles qui seront invalidées :", "confirm": "Je comprends, continuer", "cancel": "Annuler" }, - "dangerZone": "⚠️ Zone de danger", + "dangerZone": "Gefahrenzone", "dangerZoneDescription": "Irreversible Aktionen für dieses Dokument", "deleteDocument": "Dieses Dokument löschen", "deleteDocumentDescription": "Cette action supprimera définitivement le document, ses métadonnées, les lecteurs attendus et toutes les confirmations associées.\nCette action est irréversible.", @@ -635,7 +637,7 @@ "deleteItem2": "Die Liste der erwarteten Leser", "deleteItem3": "Alle kryptografischen Bestätigungen", "deleteItem4": "Der Erinnerungsverlauf", - "deleteConfirmTitle": "⚠️ Confirmer la suppression", + "deleteConfirmTitle": "Löschen bestätigen", "deleteConfirmButton": "Supprimer définitivement", "documentId": "Document ID:", "importCSV": "Import CSV", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 5244a33..ced562b 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -126,7 +126,9 @@ }, "external": { "title": "External document", - "description": "Please read the document below before confirming your reading.", + "description": "Please read the document before confirming your reading.", + "descriptionWithUrl": "This document is not directly accessible. Please open it via the link below, read it, then return to confirm your reading.", + "documentUrl": "Document URL", "openDocument": "Open document", "noUrl": "No URL available for this document" }, @@ -582,15 +584,15 @@ "nameLabel": "Name", "namePlaceholder": "Full name", "reader": "Reader", - "readers": "✓ Expected readers", + "readers": "Expected readers", "user": "User", "status": "Status", - "statusConfirmed": "✓ Confirmed", - "statusPending": "⏳ Pending", + "statusConfirmed": "Confirmed", + "statusPending": "Pending", "confirmedOn": "Confirmed on", "noExpectedSigners": "No expected readers", "noSignatures": "No confirmations", - "reminders": "📧 Email reminders", + "reminders": "Email reminders", "remindersDescription": "Send reminders to readers awaiting confirmation", "remindersSent": "Reminders sent", "toRemind": "To remind", @@ -600,9 +602,9 @@ "sendReminders": "Send reminders", "sendToAll": "Send to all pending readers ({count})", "sendToSelected": "Send to selected only ({count})", - "allContacted": "✓ All expected readers have been contacted or confirmed", - "emailServiceDisabled": "⚠️ The email service is currently disabled. Reminder history remains visible, but sending new reminders is not available.", - "unexpectedSignatures": "⚠ Additional reading confirmations", + "allContacted": "All expected readers have been contacted or confirmed", + "emailServiceDisabled": "The email service is currently disabled. Reminder history remains visible, but sending new reminders is not available.", + "unexpectedSignatures": "Additional reading confirmations", "unexpectedDescription": "Users who confirmed but are not on the expected readers list", "createdBy": "Created by {by} on {date}", "saving": "Saving...", @@ -616,18 +618,18 @@ "remindersSentGeneric": "Reminders sent successfully", "confirmSendReminders": "Send reminders to {count} reader(s) awaiting confirmation?", "confirmSendRemindersSelected": "Send reminders to {count} selected reader(s)?", - "confirmSendRemindersTitle": "📧 Send reminders", - "removeSignerTitle": "⚠️ Remove expected reader", + "confirmSendRemindersTitle": "Send reminders", + "removeSignerTitle": "Remove expected reader", "removeSignerMessage": "Remove {email} from the expected readers list?", "metadataWarning": { - "title": "⚠️ Warning: Signature invalidation", + "title": "Warning: Signature invalidation", "description": "You are about to modify critical document information (URL, checksum, algorithm or description).", "warning": "This modification will invalidate all existing signatures, as they are cryptographically linked to the current document content.", "currentSignatures": "Current signatures that will be invalidated:", "confirm": "I understand, continue", "cancel": "Cancel" }, - "dangerZone": "⚠️ Danger zone", + "dangerZone": "Danger zone", "dangerZoneDescription": "Irreversible actions on this document", "deleteDocument": "Delete this document", "deleteDocumentDescription": "This action will permanently delete the document, its metadata, expected readers and all associated confirmations.\nThis action is irreversible.", @@ -637,7 +639,7 @@ "deleteItem2": "The list of expected readers", "deleteItem3": "All cryptographic confirmations", "deleteItem4": "The reminder history", - "deleteConfirmTitle": "⚠️ Confirm deletion", + "deleteConfirmTitle": "Confirm deletion", "deleteConfirmButton": "Delete permanently", "documentId": "Document ID:", "importCSV": "Import CSV", diff --git a/webapp/src/locales/es.json b/webapp/src/locales/es.json index 73929bb..bda7a1b 100644 --- a/webapp/src/locales/es.json +++ b/webapp/src/locales/es.json @@ -125,10 +125,12 @@ "description": "Veuillez patienter pendant que nous préparons le document pour la signature." }, "external": { - "title": "Document externe", - "description": "Veuillez lire le document ci-dessous avant de confirmer votre lecture.", - "openDocument": "Ouvrir le document", - "noUrl": "Aucune URL disponible pour ce document" + "title": "Documento externo", + "description": "Por favor, lea el documento antes de confirmar su lectura.", + "descriptionWithUrl": "Este documento no es accesible directamente. Ábralo a través del enlace de abajo, léalo y luego regrese para confirmar su lectura.", + "documentUrl": "URL del documento", + "openDocument": "Abrir documento", + "noUrl": "No hay URL disponible para este documento" }, "alreadySigned": { "title": "Lecture déjà confirmée", @@ -580,15 +582,15 @@ "nameLabel": "Nombre", "namePlaceholder": "Nombre completo", "reader": "Lector", - "readers": "✓ Lecteurs attendus", + "readers": "Lectores esperados", "user": "Usuario", "status": "Estado", - "statusConfirmed": "✓ Confirmé", - "statusPending": "⏳ En attente", + "statusConfirmed": "Confirmado", + "statusPending": "Pendiente", "confirmedOn": "Confirmado el", "noExpectedSigners": "Ningún lector esperado", "noSignatures": "Ninguna confirmación", - "reminders": "📧 Relances par email", + "reminders": "Recordatorios por email", "remindersDescription": "Envoyer des rappels aux lecteurs en attente de confirmation", "remindersSent": "Recordatorios enviados", "toRemind": "Para recordar", @@ -598,9 +600,9 @@ "sendReminders": "Envoyer les relances", "sendToAll": "Envoyer à tous les lecteurs en attente ({count})", "sendToSelected": "Envoyer uniquement aux sélectionnés ({count})", - "allContacted": "✓ Tous les lecteurs attendus ont été contactés ou ont confirmé", - "emailServiceDisabled": "⚠️ Le service d'envoi d'emails est actuellement désactivé. L'historique des rappels reste visible, mais l'envoi de nouveaux rappels n'est pas disponible.", - "unexpectedSignatures": "⚠ Confirmations de lecture complémentaires", + "allContacted": "Todos los lectores esperados han sido contactados o confirmados", + "emailServiceDisabled": "El servicio de email está desactivado. El historial de recordatorios permanece visible, pero el envío de nuevos recordatorios no está disponible.", + "unexpectedSignatures": "Confirmaciones de lectura adicionales", "unexpectedDescription": "Utilisateurs ayant confirmé mais non présents dans la liste des lecteurs attendus", "createdBy": "Créé par {by} le {date}", "saving": "Enregistrement...", @@ -614,18 +616,18 @@ "remindersSentGeneric": "Relances envoyées avec succès", "confirmSendReminders": "Envoyer des relances à {count} lecteur(s) en attente de confirmation ?", "confirmSendRemindersSelected": "Envoyer des relances à {count} lecteur(s) sélectionné(s) ?", - "confirmSendRemindersTitle": "📧 Envoyer des relances", - "removeSignerTitle": "⚠️ Retirer le lecteur attendu", + "confirmSendRemindersTitle": "Enviar recordatorios", + "removeSignerTitle": "Eliminar lector esperado", "removeSignerMessage": "Retirer {email} de la liste des lecteurs attendus ?", "metadataWarning": { - "title": "⚠️ Attention : Invalidation des signatures", + "title": "Atención: Invalidación de firmas", "description": "Vous êtes sur le point de modifier des informations critiques du document (URL, checksum, algorithme ou description).", "warning": "Cette modification entraînera l'invalidation de toutes les signatures existantes, car elles sont liées cryptographiquement au contenu actuel du document.", "currentSignatures": "Signatures actuelles qui seront invalidées :", "confirm": "Je comprends, continuer", "cancel": "Annuler" }, - "dangerZone": "⚠️ Zone de danger", + "dangerZone": "Zona de peligro", "dangerZoneDescription": "Acciones irreversibles sobre este documento", "deleteDocument": "Eliminar este documento", "deleteDocumentDescription": "Cette action supprimera définitivement le document, ses métadonnées, les lecteurs attendus et toutes les confirmations associées.\nCette action est irréversible.", @@ -635,7 +637,7 @@ "deleteItem2": "La lista de lectores esperados", "deleteItem3": "Todas las confirmaciones criptográficas", "deleteItem4": "El historial de recordatorios", - "deleteConfirmTitle": "⚠️ Confirmer la suppression", + "deleteConfirmTitle": "Confirmar eliminación", "deleteConfirmButton": "Supprimer définitivement", "documentId": "Document ID:", "importCSV": "Import CSV", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index b94b330..ef00606 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -126,7 +126,9 @@ }, "external": { "title": "Document externe", - "description": "Veuillez lire le document ci-dessous avant de confirmer votre lecture.", + "description": "Veuillez lire le document avant de confirmer votre lecture.", + "descriptionWithUrl": "Ce document n'est pas accessible directement. Veuillez l'ouvrir via le lien ci-dessous, le lire, puis revenir confirmer votre lecture.", + "documentUrl": "Adresse du document", "openDocument": "Ouvrir le document", "noUrl": "Aucune URL disponible pour ce document" }, @@ -579,15 +581,15 @@ "nameLabel": "Nom", "namePlaceholder": "Nom complet", "reader": "Lecteur", - "readers": "✓ Lecteurs attendus", + "readers": "Lecteurs attendus", "user": "Utilisateur", "status": "Statut", - "statusConfirmed": "✓ Confirmé", - "statusPending": "⏳ En attente", + "statusConfirmed": "Confirmé", + "statusPending": "En attente", "confirmedOn": "Confirmé le", "noExpectedSigners": "Aucun lecteur attendu", "noSignatures": "Aucune confirmation", - "reminders": "📧 Relances par email", + "reminders": "Relances par email", "remindersDescription": "Envoyer des rappels aux lecteurs en attente de confirmation", "remindersSent": "Relances envoyées", "toRemind": "À relancer", @@ -597,9 +599,9 @@ "sendReminders": "Envoyer les relances", "sendToAll": "Envoyer à tous les lecteurs en attente ({count})", "sendToSelected": "Envoyer uniquement aux sélectionnés ({count})", - "allContacted": "✓ Tous les lecteurs attendus ont été contactés ou ont confirmé", - "emailServiceDisabled": "⚠️ Le service d'envoi d'emails est actuellement désactivé. L'historique des rappels reste visible, mais l'envoi de nouveaux rappels n'est pas disponible.", - "unexpectedSignatures": "⚠ Confirmations de lecture complémentaires", + "allContacted": "Tous les lecteurs attendus ont été contactés ou ont confirmé", + "emailServiceDisabled": "Le service d'envoi d'emails est actuellement désactivé. L'historique des rappels reste visible, mais l'envoi de nouveaux rappels n'est pas disponible.", + "unexpectedSignatures": "Confirmations de lecture complémentaires", "unexpectedDescription": "Utilisateurs ayant confirmé mais non présents dans la liste des lecteurs attendus", "createdBy": "Créé par {by} le {date}", "saving": "Enregistrement...", @@ -613,18 +615,18 @@ "remindersSentGeneric": "Relances envoyées avec succès", "confirmSendReminders": "Envoyer des relances à {count} lecteur(s) en attente de confirmation ?", "confirmSendRemindersSelected": "Envoyer des relances à {count} lecteur(s) sélectionné(s) ?", - "confirmSendRemindersTitle": "📧 Envoyer des relances", - "removeSignerTitle": "⚠️ Retirer le lecteur attendu", + "confirmSendRemindersTitle": "Envoyer des relances", + "removeSignerTitle": "Retirer le lecteur attendu", "removeSignerMessage": "Retirer {email} de la liste des lecteurs attendus ?", "metadataWarning": { - "title": "⚠️ Attention : Invalidation des signatures", + "title": "Attention : Invalidation des signatures", "description": "Vous êtes sur le point de modifier des informations critiques du document (URL, checksum, algorithme ou description).", "warning": "Cette modification entraînera l'invalidation de toutes les signatures existantes, car elles sont liées cryptographiquement au contenu actuel du document.", "currentSignatures": "Signatures actuelles qui seront invalidées :", "confirm": "Je comprends, continuer", "cancel": "Annuler" }, - "dangerZone": "⚠️ Zone de danger", + "dangerZone": "Zone de danger", "dangerZoneDescription": "Actions irréversibles sur ce document", "deleteDocument": "Supprimer ce document", "deleteDocumentDescription": "Cette action supprimera définitivement le document, ses métadonnées, les lecteurs attendus et toutes les confirmations associées.\nCette action est irréversible.", @@ -634,7 +636,7 @@ "deleteItem2": "La liste des lecteurs attendus", "deleteItem3": "Toutes les confirmations cryptographiques", "deleteItem4": "L'historique des relances", - "deleteConfirmTitle": "⚠️ Confirmer la suppression", + "deleteConfirmTitle": "Confirmer la suppression", "deleteConfirmButton": "Supprimer définitivement", "documentId": "Document ID:", "importCSV": "Import CSV", diff --git a/webapp/src/locales/it.json b/webapp/src/locales/it.json index ea4dcca..e808437 100644 --- a/webapp/src/locales/it.json +++ b/webapp/src/locales/it.json @@ -125,10 +125,12 @@ "description": "Veuillez patienter pendant que nous préparons le document pour la signature." }, "external": { - "title": "Document externe", - "description": "Veuillez lire le document ci-dessous avant de confirmer votre lecture.", - "openDocument": "Ouvrir le document", - "noUrl": "Aucune URL disponible pour ce document" + "title": "Documento esterno", + "description": "Per favore, leggi il documento prima di confermare la tua lettura.", + "descriptionWithUrl": "Questo documento non è accessibile direttamente. Aprilo tramite il link qui sotto, leggilo, poi torna per confermare la tua lettura.", + "documentUrl": "URL del documento", + "openDocument": "Apri documento", + "noUrl": "Nessun URL disponibile per questo documento" }, "alreadySigned": { "title": "Lecture déjà confirmée", @@ -580,15 +582,15 @@ "nameLabel": "Nome", "namePlaceholder": "Nome completo", "reader": "Lettore", - "readers": "✓ Lecteurs attendus", + "readers": "Lettori previsti", "user": "Utente", "status": "Stato", - "statusConfirmed": "✓ Confirmé", - "statusPending": "⏳ En attente", + "statusConfirmed": "Confermato", + "statusPending": "In attesa", "confirmedOn": "Confermato il", "noExpectedSigners": "Nessun lettore previsto", "noSignatures": "Nessuna conferma", - "reminders": "📧 Relances par email", + "reminders": "Promemoria via email", "remindersDescription": "Envoyer des rappels aux lecteurs en attente de confirmation", "remindersSent": "Promemoria inviati", "toRemind": "Da ricordare", @@ -598,9 +600,9 @@ "sendReminders": "Envoyer les relances", "sendToAll": "Envoyer à tous les lecteurs en attente ({count})", "sendToSelected": "Envoyer uniquement aux sélectionnés ({count})", - "allContacted": "✓ Tous les lecteurs attendus ont été contactés ou ont confirmé", - "emailServiceDisabled": "⚠️ Le service d'envoi d'emails est actuellement désactivé. L'historique des rappels reste visible, mais l'envoi de nouveaux rappels n'est pas disponible.", - "unexpectedSignatures": "⚠ Confirmations de lecture complémentaires", + "allContacted": "Tutti i lettori previsti sono stati contattati o hanno confermato", + "emailServiceDisabled": "Il servizio email è disattivato. La cronologia dei promemoria rimane visibile, ma l'invio di nuovi promemoria non è disponibile.", + "unexpectedSignatures": "Conferme di lettura aggiuntive", "unexpectedDescription": "Utilisateurs ayant confirmé mais non présents dans la liste des lecteurs attendus", "createdBy": "Créé par {by} le {date}", "saving": "Enregistrement...", @@ -614,18 +616,18 @@ "remindersSentGeneric": "Relances envoyées avec succès", "confirmSendReminders": "Envoyer des relances à {count} lecteur(s) en attente de confirmation ?", "confirmSendRemindersSelected": "Envoyer des relances à {count} lecteur(s) sélectionné(s) ?", - "confirmSendRemindersTitle": "📧 Envoyer des relances", - "removeSignerTitle": "⚠️ Retirer le lecteur attendu", + "confirmSendRemindersTitle": "Invia promemoria", + "removeSignerTitle": "Rimuovi lettore previsto", "removeSignerMessage": "Retirer {email} de la liste des lecteurs attendus ?", "metadataWarning": { - "title": "⚠️ Attention : Invalidation des signatures", + "title": "Attenzione: Invalidazione delle firme", "description": "Vous êtes sur le point de modifier des informations critiques du document (URL, checksum, algorithme ou description).", "warning": "Cette modification entraînera l'invalidation de toutes les signatures existantes, car elles sont liées cryptographiquement au contenu actuel du document.", "currentSignatures": "Signatures actuelles qui seront invalidées :", "confirm": "Je comprends, continuer", "cancel": "Annuler" }, - "dangerZone": "⚠️ Zone de danger", + "dangerZone": "Zona pericolosa", "dangerZoneDescription": "Azioni irreversibili su questo documento", "deleteDocument": "Elimina questo documento", "deleteDocumentDescription": "Cette action supprimera définitivement le document, ses métadonnées, les lecteurs attendus et toutes les confirmations associées.\nCette action est irréversible.", @@ -635,7 +637,7 @@ "deleteItem2": "L'elenco dei lettori previsti", "deleteItem3": "Tutte le conferme crittografiche", "deleteItem4": "La cronologia dei promemoria", - "deleteConfirmTitle": "⚠️ Confirmer la suppression", + "deleteConfirmTitle": "Conferma eliminazione", "deleteConfirmButton": "Supprimer définitivement", "documentId": "Document ID:", "importCSV": "Import CSV", diff --git a/webapp/src/pages/DocumentEditPage.vue b/webapp/src/pages/DocumentEditPage.vue index 542c2e8..28bf1b3 100644 --- a/webapp/src/pages/DocumentEditPage.vue +++ b/webapp/src/pages/DocumentEditPage.vue @@ -12,31 +12,37 @@ import { removeExpectedSigner, sendReminders, deleteDocument, + previewCSVSigners, + importSigners, type DocumentStatus, + type CSVPreviewResult, + type CSVSignerEntry, } from '@/services/admin' import { extractError } from '@/services/http' import { useConfigStore } from '@/stores/config' +import SignersSection from '@/components/SignersSection.vue' +import RemindersSection from '@/components/RemindersSection.vue' import { ArrowLeft, - Users, CheckCircle, - Mail, - Plus, Loader2, Copy, - Clock, X, Trash2, - Search, AlertCircle, + AlertTriangle, ChevronRight, ExternalLink, Check, FileText, + FileCheck, + FileX, Eye, Download, ScrollText, ShieldCheck, + Users, + Clock, } from 'lucide-vue-next' const route = useRoute() @@ -59,9 +65,17 @@ const showAddSignersModal = ref(false) const showDeleteConfirmModal = ref(false) const showRemoveSignerModal = ref(false) const showSendRemindersModal = ref(false) +const showImportCSVModal = ref(false) const signerToRemove = ref('') const remindersMessage = ref('') +// CSV Import +const csvFile = ref(null) +const csvPreview = ref(null) +const analyzingCSV = ref(false) +const importingCSV = ref(false) +const csvError = ref('') + // Metadata form const metadataForm = ref('all') @@ -108,22 +121,15 @@ const shareLink = computed(() => { return documentStatus.value.shareLink }) -const stats = computed(() => documentStatus.value?.stats) +const stats = computed(() => documentStatus.value?.stats ?? null) const reminderStats = computed(() => documentStatus.value?.reminderStats) const smtpEnabled = computed(() => configStore.smtpEnabled) const expectedSigners = computed(() => documentStatus.value?.expectedSigners || []) -const filteredSigners = computed(() => { - const filter = signerFilter.value.toLowerCase().trim() - if (!filter) return expectedSigners.value - return expectedSigners.value.filter(signer => - signer.email.toLowerCase().includes(filter) || - (signer.name && signer.name.toLowerCase().includes(filter)) || - (signer.userName && signer.userName.toLowerCase().includes(filter)) - ) -}) const documentMetadata = computed(() => documentStatus.value?.document) const documentTitle = computed(() => documentMetadata.value?.title || docId.value) const isStoredDocument = computed(() => !!documentMetadata.value?.storageKey) +const unexpectedSignatures = computed(() => documentStatus.value?.unexpectedSignatures || []) +const isAdmin = computed(() => authStore.isAdmin) // Methods async function loadDocumentStatus() { @@ -246,9 +252,10 @@ async function removeSigner() { } } -function confirmSendReminders() { +function handleReminderSend(mode: 'all' | 'selected') { + sendMode.value = mode remindersMessage.value = - sendMode.value === 'all' + mode === 'all' ? t('documentEdit.confirmSendReminders', { count: reminderStats.value?.pendingCount || 0 }) : t('documentEdit.confirmSendRemindersSelected', { count: selectedEmails.value.length }) showSendRemindersModal.value = true @@ -315,15 +322,6 @@ function formatDate(dateString: string | undefined): string { }) } -function toggleEmailSelection(email: string) { - const index = selectedEmails.value.indexOf(email) - if (index > -1) { - selectedEmails.value.splice(index, 1) - } else { - selectedEmails.value.push(email) - } -} - async function handleDeleteDocument() { try { deletingDocument.value = true @@ -340,6 +338,90 @@ async function handleDeleteDocument() { } } +// CSV Import functions (admin only) +function openImportCSVModal() { + csvFile.value = null + csvPreview.value = null + csvError.value = '' + showImportCSVModal.value = true +} + +function handleCSVFileChange(event: Event) { + const target = event.target as HTMLInputElement + if (target.files && target.files[0]) { + csvFile.value = target.files[0] + csvPreview.value = null + csvError.value = '' + } +} + +async function analyzeCSV() { + if (!csvFile.value) return + + try { + analyzingCSV.value = true + csvError.value = '' + const response = await previewCSVSigners(docId.value, csvFile.value) + csvPreview.value = response.data + } catch (err) { + csvError.value = extractError(err) + console.error('Failed to analyze CSV:', err) + } finally { + analyzingCSV.value = false + } +} + +function getSignerStatus(signer: CSVSignerEntry): 'valid' | 'exists' { + if (!csvPreview.value) return 'valid' + return csvPreview.value.existingEmails.includes(signer.email) ? 'exists' : 'valid' +} + +const signersToImport = computed(() => { + if (!csvPreview.value) return [] + return csvPreview.value.signers.filter( + s => !csvPreview.value!.existingEmails.includes(s.email) + ) +}) + +async function confirmImportCSV() { + if (!csvPreview.value || signersToImport.value.length === 0) return + + try { + importingCSV.value = true + csvError.value = '' + + const signersData = signersToImport.value.map(s => ({ + email: s.email, + name: s.name + })) + + const response = await importSigners(docId.value, signersData) + + showImportCSVModal.value = false + csvFile.value = null + csvPreview.value = null + + success.value = t('admin.documentDetail.csvImportSuccess', { + imported: response.data.imported, + skipped: response.data.skipped + }) + await loadDocumentStatus() + setTimeout(() => (success.value = ''), 3000) + } catch (err) { + csvError.value = extractError(err) + console.error('Failed to import signers:', err) + } finally { + importingCSV.value = false + } +} + +function closeImportCSVModal() { + showImportCSVModal.value = false + csvFile.value = null + csvPreview.value = null + csvError.value = '' +} + onMounted(async () => { if (!authStore.initialized) { await authStore.checkAuth() @@ -582,146 +664,27 @@ onMounted(async () => { -
-
-
-
-

{{ t('documentEdit.readers.title') }}

-

{{ stats.signedCount }} / {{ stats.expectedCount }} {{ t('documentEdit.readers.confirmed') }}

-
- -
-
-
-
-
- - -
- - - - - -
-
-
-
- -
-

{{ signer.userName || signer.name || signer.email }}

-

{{ signer.email }}

-
-
- - {{ signer.hasSigned ? t('documentEdit.readers.statusConfirmed') : t('documentEdit.readers.statusPending') }} - -
-
- {{ signer.signedAt ? formatDate(signer.signedAt) : '-' }} - -
-
-
-
- -
- -

{{ t('documentEdit.readers.noReaders') }}

-
-
-
+ -
-
-

{{ t('documentEdit.reminders.title') }}

-

{{ t('documentEdit.reminders.description') }}

-
-
-
-
-

{{ t('documentEdit.reminders.sent') }}

-

{{ reminderStats.totalSent }}

-
-
-

{{ t('documentEdit.reminders.toRemind') }}

-

{{ reminderStats.pendingCount }}

-
-
-

{{ t('documentEdit.reminders.lastSent') }}

-

{{ formatDate(reminderStats.lastSentAt) }}

-
-
- -
-

{{ t('documentEdit.reminders.emailDisabled') }}

-
- -
-
- - -
- -
-
-
+
@@ -840,5 +803,103 @@ onMounted(async () => {
+ + +
+
+
+

{{ t('admin.documentDetail.importCSVTitle') }}

+ +
+
+
+

{{ csvError }}

+
+ +
+
+ + +

{{ t('admin.documentDetail.csvFormatHelp') }}

+
+
+ + +
+
+ +
+
+
+ +
+

{{ t('admin.documentDetail.validEntries') }}

+

{{ signersToImport.length }}

+
+
+
+ +
+

{{ t('admin.documentDetail.existingEntries') }}

+

{{ csvPreview.existingEmails.length }}

+
+
+
+ +
+

{{ t('admin.documentDetail.invalidEntries') }}

+

{{ csvPreview.invalidCount }}

+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + +
{{ t('admin.documentDetail.lineNumber') }}{{ t('admin.documentDetail.email') }}{{ t('admin.documentDetail.name') }}{{ t('admin.documentDetail.status') }}
{{ signer.lineNumber }}{{ signer.email }}{{ signer.name || '-' }} + + {{ getSignerStatus(signer) === 'exists' ? t('admin.documentDetail.statusExists') : t('admin.documentDetail.statusValid') }} + +
+
+
+ +
+ +
+ + +
+
+
+
+
+
diff --git a/webapp/src/pages/HomePage.vue b/webapp/src/pages/HomePage.vue index b0b1ae4..a85d92a 100644 --- a/webapp/src/pages/HomePage.vue +++ b/webapp/src/pages/HomePage.vue @@ -58,6 +58,7 @@ const calculatingChecksum = ref(false) // New state for integrated viewer const readComplete = ref(false) const certifyChecked = ref(false) +const documentLoadFailed = ref(false) // Check if current user has signed this document const userHasSigned = computed(() => { @@ -112,6 +113,7 @@ async function handleDocumentReference(ref: string) { needsAuth.value = false readComplete.value = false certifyChecked.value = false + documentLoadFailed.value = false console.log('Loading document for reference:', ref) @@ -195,6 +197,11 @@ function handleReadComplete() { readComplete.value = true } +function handleDocumentLoadError(error: string) { + console.log('Document load failed:', error) + documentLoadFailed.value = true +} + async function handleSigned() { showSuccessMessage.value = true errorMessage.value = null @@ -541,7 +548,7 @@ onMounted(async () => {
-
+
{ :stored-checksum="currentDocument.checksum" :checksum-algorithm="currentDocument.checksumAlgorithm" @read-complete="handleReadComplete" + @load-error="handleDocumentLoadError" />
- +
@@ -566,18 +574,30 @@ onMounted(async () => { {{ t('sign.external.title') }}

- {{ t('sign.external.description') }} + {{ currentDocument.url ? t('sign.external.descriptionWithUrl') : t('sign.external.description') }}

- - - {{ t('sign.external.openDocument') }} - +

{{ t('sign.external.noUrl') }}

diff --git a/webapp/src/pages/admin/AdminDocumentDetail.vue b/webapp/src/pages/admin/AdminDocumentDetail.vue index cae88f4..1cf057a 100644 --- a/webapp/src/pages/admin/AdminDocumentDetail.vue +++ b/webapp/src/pages/admin/AdminDocumentDetail.vue @@ -19,22 +19,18 @@ import { } from '@/services/admin' import { extractError } from '@/services/http' import { useConfigStore } from '@/stores/config' +import SignersSection from '@/components/SignersSection.vue' +import RemindersSection from '@/components/RemindersSection.vue' import { ArrowLeft, - Users, CheckCircle, - Mail, - Plus, Loader2, Copy, - Clock, X, Trash2, - Upload, AlertTriangle, FileCheck, FileX, - Search, AlertCircle, ChevronRight, ExternalLink, @@ -44,6 +40,8 @@ import { Download, ScrollText, ShieldCheck, + Users, + Clock, } from 'lucide-vue-next' const route = useRoute() @@ -114,7 +112,6 @@ const savingMetadata = ref(false) // Expected signers form const signersEmails = ref('') const addingSigners = ref(false) -const signerFilter = ref('') // Reminders const sendMode = ref<'all' | 'selected'>('all') @@ -130,19 +127,10 @@ const shareLink = computed(() => { return documentStatus.value.shareLink }) -const stats = computed(() => documentStatus.value?.stats) +const stats = computed(() => documentStatus.value?.stats ?? null) const reminderStats = computed(() => documentStatus.value?.reminderStats) const smtpEnabled = computed(() => configStore.smtpEnabled) const expectedSigners = computed(() => documentStatus.value?.expectedSigners || []) -const filteredSigners = computed(() => { - const filter = signerFilter.value.toLowerCase().trim() - if (!filter) return expectedSigners.value - return expectedSigners.value.filter(signer => - signer.email.toLowerCase().includes(filter) || - (signer.name && signer.name.toLowerCase().includes(filter)) || - (signer.userName && signer.userName.toLowerCase().includes(filter)) - ) -}) const unexpectedSignatures = computed(() => documentStatus.value?.unexpectedSignatures || []) const documentMetadata = computed(() => documentStatus.value?.document) const documentTitle = computed(() => documentMetadata.value?.title || docId.value) @@ -294,9 +282,10 @@ function cancelRemoveSigner() { signerToRemove.value = '' } -function confirmSendReminders() { +function handleReminderSend(mode: 'all' | 'selected') { + sendMode.value = mode remindersMessage.value = - sendMode.value === 'all' + mode === 'all' ? t('admin.documentDetail.confirmSendReminders', { count: reminderStats.value?.pendingCount || 0 }) : t('admin.documentDetail.confirmSendRemindersSelected', { count: selectedEmails.value.length }) showSendRemindersModal.value = true @@ -369,15 +358,6 @@ function formatDate(dateString: string | undefined): string { }) } -function toggleEmailSelection(email: string) { - const index = selectedEmails.value.indexOf(email) - if (index > -1) { - selectedEmails.value.splice(index, 1) - } else { - selectedEmails.value.push(email) - } -} - async function handleDeleteDocument() { try { deletingDocument.value = true @@ -699,171 +679,27 @@ onMounted(() => {
-
-
-
-
-

{{ t('admin.documentDetail.readers') }}

-

{{ stats.signedCount }} / {{ stats.expectedCount }} {{ t('admin.dashboard.stats.signed').toLowerCase() }}

-
-
- - -
-
-
-
-
-
- - -
- - - - - -
-
-
-
- -
-

{{ signer.userName || signer.name || signer.email }}

-

{{ signer.email }}

-
-
- - {{ signer.hasSigned ? t('admin.documentDetail.statusConfirmed') : t('admin.documentDetail.statusPending') }} - -
-
- {{ signer.signedAt ? formatDate(signer.signedAt) : '-' }} - -
-
-
-
- -
- -

{{ t('admin.documentDetail.noExpectedSigners') }}

-
- - -
-

- - {{ t('admin.documentDetail.unexpectedSignatures') }} - {{ unexpectedSignatures.length }} -

-

{{ t('admin.documentDetail.unexpectedDescription') }}

-
-
-
-

{{ sig.userName || sig.userEmail }}

-

{{ sig.userEmail }}

-
- {{ formatDate(sig.signedAtUTC) }} -
-
-
-
-
+ -
-
-

{{ t('admin.documentDetail.reminders') }}

-

{{ t('admin.documentDetail.remindersDescription') }}

-
-
-
-
-

{{ t('admin.documentDetail.remindersSent') }}

-

{{ reminderStats.totalSent }}

-
-
-

{{ t('admin.documentDetail.toRemind') }}

-

{{ reminderStats.pendingCount }}

-
-
-

{{ t('admin.documentDetail.lastReminder') }}

-

{{ formatDate(reminderStats.lastSentAt) }}

-
-
- -
-

{{ t('admin.documentDetail.emailServiceDisabled') }}

-
- -
-
- - -
- -
-
-
+