diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java index 2fef46d3..3c1411b1 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java @@ -28,6 +28,9 @@ public interface PersonRepository extends JpaRepository { // Lookup by full alias string, used during ODS mass import Optional findByAliasIgnoreCase(String alias); + // Exact first+last name match, used for filename-based sender lookup + Optional findByFirstNameIgnoreCaseAndLastNameIgnoreCase(String firstName, String lastName); + // --- Correspondent queries --- @Query(value = """ diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index 95cd0921..bb379194 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -63,9 +63,15 @@ public class DocumentService { document = existingDoc.get(); } else { // New uploads from the drop zone always start as incomplete + ParsedFilename parsed = parseFilenameData(originalFilename); + Person sender = (parsed != null) + ? personService.findByName(parsed.firstName(), parsed.lastName()).orElse(null) + : null; document = Document.builder() .originalFilename(originalFilename) - .title(stripExtension(originalFilename)) + .title(parsed != null ? parsed.title() : stripExtension(originalFilename)) + .documentDate(parsed != null ? parsed.date() : null) + .sender(sender) .status(DocumentStatus.UPLOADED) .metadataComplete(false) .build(); @@ -356,6 +362,81 @@ public class DocumentService { return dot > 0 ? filename.substring(0, dot) : filename; } + private record ParsedFilename(LocalDate date, String firstName, String lastName) { + String title() { + String dateDisplay = String.format("%02d.%02d.%d", + date.getDayOfMonth(), date.getMonthValue(), date.getYear()); + return firstName + " " + lastName + " (" + dateDisplay + ")"; + } + } + + /** + * Parses a structured filename into its date and name components. + * + * Algorithm: split stem on "_", identify the date token (first or last segment), + * treat the outermost remaining segment as firstName, rest as lastName parts. + * Compound last names (e.g. "de_Gruyter") are supported naturally. + * Returns null for unrecognised filenames. + * + * Examples: + * 18881025_de_Gruyter_Walter.pdf → date=1888-10-25, firstName=Walter, lastName=de Gruyter + * 1965-03-12_Mueller_Hans.pdf → date=1965-03-12, firstName=Hans, lastName=Mueller + * Mueller_Hans_19650312.pdf → date=1965-03-12, firstName=Hans, lastName=Mueller + */ + private static ParsedFilename parseFilenameData(String filename) { + if (filename == null) return null; + int dot = filename.lastIndexOf('.'); + if (dot < 0) return null; + String stem = filename.substring(0, dot); + + String[] parts = stem.split("_", -1); + if (parts.length < 3) return null; + + String dateIso; + String[] nameParts; + + String dateFromFirst = tryParseDate(parts[0]); + if (dateFromFirst != null) { + dateIso = dateFromFirst; + nameParts = Arrays.copyOfRange(parts, 1, parts.length); + } else { + String dateFromLast = tryParseDate(parts[parts.length - 1]); + if (dateFromLast == null) return null; + dateIso = dateFromLast; + nameParts = Arrays.copyOfRange(parts, 0, parts.length - 1); + } + + if (nameParts.length < 2) return null; + for (String p : nameParts) { + if (!p.matches("\\p{L}+")) return null; + } + + String firstName = nameParts[nameParts.length - 1]; + String lastName = String.join(" ", Arrays.copyOfRange(nameParts, 0, nameParts.length - 1)); + return new ParsedFilename(LocalDate.parse(dateIso), firstName, lastName); + } + + // Used by tests and as a public utility; delegates to parseFilenameData. + static String titleFromFilename(String filename) { + if (filename == null) return null; + ParsedFilename parsed = parseFilenameData(filename); + return parsed != null ? parsed.title() : stripExtension(filename); + } + + private static String tryParseDate(String s) { + if (s.matches("\\d{4}-\\d{2}-\\d{2}")) { + int m = Integer.parseInt(s.substring(5, 7)); + int d = Integer.parseInt(s.substring(8, 10)); + if (m >= 1 && m <= 12 && d >= 1 && d <= 31) return s; + } else if (s.matches("\\d{8}")) { + int m = Integer.parseInt(s.substring(4, 6)); + int d = Integer.parseInt(s.substring(6, 8)); + if (m >= 1 && m <= 12 && d >= 1 && d <= 31) + return s.substring(0, 4) + "-" + s.substring(4, 6) + "-" + s.substring(6, 8); + } + return null; + } + private static String sha256Hex(byte[] bytes) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java index a3b56da1..8ac9a3bd 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java @@ -1,6 +1,7 @@ package org.raddatz.familienarchiv.service; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.raddatz.familienarchiv.dto.PersonUpdateDTO; @@ -42,6 +43,10 @@ public class PersonService { return personRepository.findAllById(ids); } + public Optional findByName(String firstName, String lastName) { + return personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase(firstName, lastName); + } + @Transactional public Person findOrCreateByAlias(String rawName) { String alias = rawName.trim(); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java index 195810ab..c4a1c6d2 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentStatus; +import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.repository.DocumentRepository; import org.springframework.data.domain.Sort; @@ -419,6 +420,53 @@ class DocumentServiceTest { assertThat(existing.isMetadataComplete()).isTrue(); } + @Test + void storeDocument_parsesDateFromFilename_forNewDocument() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "19650312_Mueller_Hans.pdf", "application/pdf", new byte[]{1}); + when(documentRepository.findFirstByOriginalFilename(any())).thenReturn(Optional.empty()); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("path", "hash")); + when(personService.findByName(any(), any())).thenReturn(Optional.empty()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Document.class); + documentService.storeDocument(file); + + verify(documentRepository).save(captor.capture()); + assertThat(captor.getValue().getDocumentDate()).isEqualTo(java.time.LocalDate.of(1965, 3, 12)); + assertThat(captor.getValue().getTitle()).isEqualTo("Hans Mueller (12.03.1965)"); + } + + @Test + void storeDocument_setsSender_whenPersonExistsForParsedName() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "18881025_de_Gruyter_Walter.pdf", "application/pdf", new byte[]{1}); + Person walter = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("de Gruyter").build(); + when(documentRepository.findFirstByOriginalFilename(any())).thenReturn(Optional.empty()); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("path", "hash")); + when(personService.findByName("Walter", "de Gruyter")).thenReturn(Optional.of(walter)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Document.class); + documentService.storeDocument(file); + + verify(documentRepository).save(captor.capture()); + assertThat(captor.getValue().getSender()).isEqualTo(walter); + } + + @Test + void storeDocument_leavesSenderNull_whenPersonNotFound() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "19650312_Mueller_Hans.pdf", "application/pdf", new byte[]{1}); + when(documentRepository.findFirstByOriginalFilename(any())).thenReturn(Optional.empty()); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("path", "hash")); + when(personService.findByName(any(), any())).thenReturn(Optional.empty()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Document.class); + documentService.storeDocument(file); + + verify(documentRepository).save(captor.capture()); + assertThat(captor.getValue().getSender()).isNull(); + } + // ─── createDocument metadataComplete ───────────────────────────────────── @Test @@ -518,4 +566,52 @@ class DocumentServiceTest { assertThat(count).isEqualTo(2); } + + // ─── titleFromFilename ──────────────────────────────────────────────────── + + @Test + void titleFromFilename_dateIso_name() { + assertThat(DocumentService.titleFromFilename("1965-03-12_Mueller_Hans.pdf")) + .isEqualTo("Hans Mueller (12.03.1965)"); + } + + @Test + void titleFromFilename_dateCompact_name() { + assertThat(DocumentService.titleFromFilename("19650312_Mueller_Hans.pdf")) + .isEqualTo("Hans Mueller (12.03.1965)"); + } + + @Test + void titleFromFilename_name_dateIso() { + assertThat(DocumentService.titleFromFilename("Mueller_Hans_1965-03-12.pdf")) + .isEqualTo("Hans Mueller (12.03.1965)"); + } + + @Test + void titleFromFilename_name_dateCompact() { + assertThat(DocumentService.titleFromFilename("Mueller_Hans_19650312.pdf")) + .isEqualTo("Hans Mueller (12.03.1965)"); + } + + @Test + void titleFromFilename_compound_lastName_dateFirst() { + assertThat(DocumentService.titleFromFilename("18881025_de_Gruyter_Walter.pdf")) + .isEqualTo("Walter de Gruyter (25.10.1888)"); + } + + @Test + void titleFromFilename_compound_lastName_dateLast() { + assertThat(DocumentService.titleFromFilename("de_Gruyter_Walter_18881025.pdf")) + .isEqualTo("Walter de Gruyter (25.10.1888)"); + } + + @Test + void titleFromFilename_fallsBackToStripExtension() { + assertThat(DocumentService.titleFromFilename("scan_001.pdf")).isEqualTo("scan_001"); + } + + @Test + void titleFromFilename_null_returnsNull() { + assertThat(DocumentService.titleFromFilename(null)).isNull(); + } } diff --git a/frontend/messages/de.json b/frontend/messages/de.json index c441cbf8..6fc63606 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -270,6 +270,7 @@ "pdf_annotations_hide": "Annotierungen verbergen", "upload_drop_hint": "Dateien ablegen oder auswählen", "upload_accepted_types": "PDF, JPEG, PNG, TIFF", + "upload_filename_hint": "Tipp: 2024-03-15_Mueller_Hans.pdf → Datum und Absender werden vorausgefüllt", "upload_success": "{count} Dokument(e) erstellt", "upload_duplicate": "{filename} existiert bereits —", "upload_duplicate_link": "Zum Dokument", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index abd1c189..44be16ab 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -270,6 +270,7 @@ "pdf_annotations_hide": "Hide annotations", "upload_drop_hint": "Drop files or click to select", "upload_accepted_types": "PDF, JPEG, PNG, TIFF", + "upload_filename_hint": "Tip: 2024-03-15_Mueller_Hans.pdf → date and sender pre-filled", "upload_success": "{count} document(s) created", "upload_duplicate": "{filename} already exists —", "upload_duplicate_link": "View document", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 0839bf4e..54438a09 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -270,6 +270,7 @@ "pdf_annotations_hide": "Ocultar anotaciones", "upload_drop_hint": "Soltar archivos o hacer clic para seleccionar", "upload_accepted_types": "PDF, JPEG, PNG, TIFF", + "upload_filename_hint": "Consejo: 2024-03-15_Mueller_Hans.pdf → fecha y remitente prellenados", "upload_success": "{count} documento(s) creado(s)", "upload_duplicate": "{filename} ya existe —", "upload_duplicate_link": "Ver documento", diff --git a/frontend/src/lib/components/PersonTypeahead.svelte b/frontend/src/lib/components/PersonTypeahead.svelte index 91df7d02..5196108a 100644 --- a/frontend/src/lib/components/PersonTypeahead.svelte +++ b/frontend/src/lib/components/PersonTypeahead.svelte @@ -9,6 +9,7 @@ interface Props { label: string; value?: string; initialName?: string; + suggestedName?: string; restrictToCorrespondentsOf?: string; onchange?: (value: string) => void; } @@ -18,12 +19,20 @@ let { label, value = $bindable(''), initialName = '', + suggestedName = '', restrictToCorrespondentsOf, onchange }: Props = $props(); let searchTerm = $state(initialName); +$effect(() => { + const suggested = suggestedName; + if (suggested && !untrack(() => value)) { + searchTerm = suggested; + } +}); + let results: Person[] = $state([]); let showDropdown = $state(false); let loading = $state(false); diff --git a/frontend/src/lib/components/document/DescriptionSection.svelte b/frontend/src/lib/components/document/DescriptionSection.svelte index 10cda974..6d2c13c4 100644 --- a/frontend/src/lib/components/document/DescriptionSection.svelte +++ b/frontend/src/lib/components/document/DescriptionSection.svelte @@ -1,4 +1,5 @@
@@ -33,7 +46,11 @@ let { id="title" type="text" name="title" - value={initialTitle} + value={titleValue} + oninput={(e) => { + titleValue = (e.target as HTMLInputElement).value; + titleDirty = true; + }} required={titleRequired} class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink" /> diff --git a/frontend/src/lib/components/document/WhoWhenSection.svelte b/frontend/src/lib/components/document/WhoWhenSection.svelte index 71b2feaf..ff90efdf 100644 --- a/frontend/src/lib/components/document/WhoWhenSection.svelte +++ b/frontend/src/lib/components/document/WhoWhenSection.svelte @@ -16,13 +16,17 @@ let { selectedReceivers = $bindable([]), initialDateIso = '', initialLocation = '', - initialSenderName = '' + initialSenderName = '', + suggestedDateIso = '', + suggestedSenderName = '' }: { senderId?: string; selectedReceivers?: Person[]; initialDateIso?: string; initialLocation?: string; initialSenderName?: string; + suggestedDateIso?: string; + suggestedSenderName?: string; } = $props(); let dateDisplay = $state(untrack(() => isoToGerman(initialDateIso))); @@ -37,6 +41,14 @@ function handleDateInput(e: Event) { dateIso = result.iso; dateDirty = true; } + +$effect(() => { + const suggested = suggestedDateIso; + if (suggested && !untrack(() => dateDirty)) { + dateDisplay = isoToGerman(suggested); + dateIso = suggested; + } +});
@@ -90,6 +102,7 @@ function handleDateInput(e: Event) { label={m.form_label_sender()} bind:value={senderId} initialName={initialSenderName} + suggestedName={suggestedSenderName} />
diff --git a/frontend/src/lib/utils/filename.spec.ts b/frontend/src/lib/utils/filename.spec.ts new file mode 100644 index 00000000..297c8a1e --- /dev/null +++ b/frontend/src/lib/utils/filename.spec.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { parseFilename, stripExtension } from './filename'; + +describe('parseFilename', () => { + describe('date-first patterns', () => { + it('YYYY-MM-DD_Lastname_Firstname', () => { + expect(parseFilename('1965-03-12_Mueller_Hans.pdf')).toEqual({ + dateIso: '1965-03-12', + personName: 'Hans Mueller', + suggestedTitle: 'Hans Mueller (12.03.1965)' + }); + }); + + it('YYYYMMDD_Lastname_Firstname', () => { + expect(parseFilename('19650312_Mueller_Hans.pdf')).toEqual({ + dateIso: '1965-03-12', + personName: 'Hans Mueller', + suggestedTitle: 'Hans Mueller (12.03.1965)' + }); + }); + + it('YYYYMMDD_compound_lastname_Firstname', () => { + expect(parseFilename('18881025_de_Gruyter_Walter.pdf')).toEqual({ + dateIso: '1888-10-25', + personName: 'Walter de Gruyter', + suggestedTitle: 'Walter de Gruyter (25.10.1888)' + }); + }); + + it('handles umlauts in names', () => { + const result = parseFilename('2024-01-15_Müller_Jürgen.pdf'); + expect(result.personName).toBe('Jürgen Müller'); + }); + }); + + describe('date-last patterns', () => { + it('Lastname_Firstname_YYYY-MM-DD', () => { + expect(parseFilename('Mueller_Hans_1965-03-12.pdf')).toEqual({ + dateIso: '1965-03-12', + personName: 'Hans Mueller', + suggestedTitle: 'Hans Mueller (12.03.1965)' + }); + }); + + it('Lastname_Firstname_YYYYMMDD', () => { + expect(parseFilename('Mueller_Hans_19650312.pdf')).toEqual({ + dateIso: '1965-03-12', + personName: 'Hans Mueller', + suggestedTitle: 'Hans Mueller (12.03.1965)' + }); + }); + + it('compound_lastname_Firstname_YYYYMMDD', () => { + expect(parseFilename('de_Gruyter_Walter_18881025.pdf')).toEqual({ + dateIso: '1888-10-25', + personName: 'Walter de Gruyter', + suggestedTitle: 'Walter de Gruyter (25.10.1888)' + }); + }); + }); + + describe('non-matching filenames', () => { + it('returns empty for date-only filename', () => { + expect(parseFilename('1965-03-12.pdf')).toEqual({}); + }); + + it('returns empty for two segments with no date', () => { + expect(parseFilename('Mueller_Hans.pdf')).toEqual({}); + }); + + it('returns empty for unstructured filename', () => { + expect(parseFilename('scan_001.pdf')).toEqual({}); + }); + + it('returns empty for three name segments with no date', () => { + expect(parseFilename('Mueller_Hans_Juergen.pdf')).toEqual({}); + }); + + it('returns empty for filename without extension', () => { + expect(parseFilename('1965-03-12_Mueller_Hans')).toEqual({}); + }); + + it('rejects implausible date (month 13)', () => { + expect(parseFilename('19651345_Mueller_Hans.pdf')).toEqual({}); + }); + }); +}); + +describe('stripExtension', () => { + it('removes the extension', () => { + expect(stripExtension('document.pdf')).toBe('document'); + }); + + it('removes only the last extension', () => { + expect(stripExtension('archive.tar.gz')).toBe('archive.tar'); + }); + + it('leaves names without extension unchanged', () => { + expect(stripExtension('nodotfile')).toBe('nodotfile'); + }); +}); diff --git a/frontend/src/lib/utils/filename.ts b/frontend/src/lib/utils/filename.ts new file mode 100644 index 00000000..101fdd1c --- /dev/null +++ b/frontend/src/lib/utils/filename.ts @@ -0,0 +1,83 @@ +import { isoToGerman } from './date'; + +export interface FilenameParseResult { + /** ISO format: YYYY-MM-DD */ + dateIso?: string; + /** "Firstname Lastname" — order reversed from filename convention */ + personName?: string; + /** Ready-to-use title, e.g. "Hans Mueller (12.03.1965)" */ + suggestedTitle?: string; +} + +// A date token is either YYYY-MM-DD or YYYYMMDD with a plausible month/day range. +function tryParseDate(s: string): string | undefined { + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { + const m = parseInt(s.slice(5, 7)); + const d = parseInt(s.slice(8, 10)); + if (m >= 1 && m <= 12 && d >= 1 && d <= 31) return s; + } else if (/^\d{8}$/.test(s)) { + const m = parseInt(s.slice(4, 6)); + const d = parseInt(s.slice(6, 8)); + if (m >= 1 && m <= 12 && d >= 1 && d <= 31) + return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`; + } + return undefined; +} + +const NAME_PART = /^\p{L}+$/u; + +/** + * Parses a structured filename and extracts a date and person name. + * + * Supported conventions (date-first or date-last, compound last names supported): + * YYYY-MM-DD_Lastname_Firstname.ext + * YYYYMMDD_Lastname_Firstname.ext + * YYYYMMDD_de_Gruyter_Walter.ext ← compound last name: lastName="de Gruyter" + * Lastname_Firstname_YYYY-MM-DD.ext + * Lastname_Firstname_YYYYMMDD.ext + * de_Gruyter_Walter_YYYYMMDD.ext ← compound last name: lastName="de Gruyter" + * + * Algorithm: split on "_", identify the date token (first or last segment), + * treat the outermost remaining segment as firstName, rest as lastName parts. + * Returns {} for anything that doesn't match cleanly. + */ +export function parseFilename(filename: string): FilenameParseResult { + const dot = filename.lastIndexOf('.'); + if (dot < 0) return {}; // no extension — not a real file + const stem = filename.slice(0, dot); + const parts = stem.split('_'); + + // Minimum: date + at least one lastName segment + firstName = 3 parts + if (parts.length < 3) return {}; + + let dateIso: string; + let nameParts: string[]; + + const dateFromFirst = tryParseDate(parts[0]); + if (dateFromFirst) { + dateIso = dateFromFirst; + nameParts = parts.slice(1); + } else { + const dateFromLast = tryParseDate(parts[parts.length - 1]); + if (!dateFromLast) return {}; + dateIso = dateFromLast; + nameParts = parts.slice(0, -1); + } + + // Need at least lastName + firstName after removing the date + if (nameParts.length < 2) return {}; + + // All name segments must be pure letters (covers umlauts via \p{L}) + if (!nameParts.every((p) => NAME_PART.test(p))) return {}; + + const firstName = nameParts[nameParts.length - 1]; + const lastName = nameParts.slice(0, -1).join(' '); + const personName = `${firstName} ${lastName}`; + const suggestedTitle = `${personName} (${isoToGerman(dateIso)})`; + + return { dateIso, personName, suggestedTitle }; +} + +export function stripExtension(filename: string): string { + return filename.replace(/\.[^/.]+$/, ''); +} diff --git a/frontend/src/routes/DropZone.svelte b/frontend/src/routes/DropZone.svelte index d845d027..9806d231 100644 --- a/frontend/src/routes/DropZone.svelte +++ b/frontend/src/routes/DropZone.svelte @@ -179,8 +179,11 @@ $effect(() => { {uploadProgress}%
{:else} - {m.upload_drop_hint()} - {m.upload_accepted_types()} +
+ {m.upload_drop_hint()} + {m.upload_accepted_types()} + {m.upload_filename_hint()} +
{/if} diff --git a/frontend/src/routes/documents/new/+page.svelte b/frontend/src/routes/documents/new/+page.svelte index 6eb3b0c8..ac2d8a6f 100644 --- a/frontend/src/routes/documents/new/+page.svelte +++ b/frontend/src/routes/documents/new/+page.svelte @@ -6,6 +6,7 @@ import WhoWhenSection from '$lib/components/document/WhoWhenSection.svelte'; import DescriptionSection from '$lib/components/document/DescriptionSection.svelte'; import TranscriptionSection from '$lib/components/document/TranscriptionSection.svelte'; import FileSectionNew from './FileSectionNew.svelte'; +import { type FilenameParseResult } from '$lib/utils/filename'; let { data, form } = $props(); @@ -14,6 +15,8 @@ let senderId = $state(untrack(() => data.initialSenderId)); let selectedReceivers: { id: string; firstName: string; lastName: string }[] = $state( untrack(() => data.initialReceivers) ); + +let parsedSuggestion = $state({});
@@ -50,10 +53,16 @@ let selectedReceivers: { id: string; firstName: string; lastName: string }[] = $ bind:senderId={senderId} bind:selectedReceivers={selectedReceivers} initialSenderName={data.initialSenderName} + suggestedDateIso={parsedSuggestion.dateIso ?? ''} + suggestedSenderName={parsedSuggestion.personName ?? ''} + /> + - - + (parsedSuggestion = r)} />
import { m } from '$lib/paraglide/messages.js'; +import { parseFilename, type FilenameParseResult } from '$lib/utils/filename'; + +let { + onfileParsed +}: { + onfileParsed?: (result: FilenameParseResult) => void; +} = $props(); + +function handleFileChange(e: Event) { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) onfileParsed?.(parseFilename(file.name)); +}
@@ -15,6 +27,7 @@ import { m } from '$lib/paraglide/messages.js'; id="file-upload" type="file" name="file" + onchange={handleFileChange} class="block w-full cursor-pointer text-sm text-ink-2 file:mr-4 file:rounded file:border-0 file:bg-muted