diff --git a/components/import/UnderlagImportWizard.tsx b/components/import/UnderlagImportWizard.tsx index 47c691b7..4a6e061d 100644 --- a/components/import/UnderlagImportWizard.tsx +++ b/components/import/UnderlagImportWizard.tsx @@ -19,7 +19,7 @@ import { FyPicker } from '@/components/common/FyPicker' import { mapWithConcurrency } from '@/lib/concurrency' import { getErrorMessage } from '@/lib/errors/get-error-message' import { cn, formatDate } from '@/lib/utils' -import { FileUp, Loader2 } from 'lucide-react' +import { FileUp, FolderUp, Loader2 } from 'lucide-react' import type { FiscalPeriod } from '@/types' import type { UnderlagPlan, @@ -49,6 +49,7 @@ import type { type Step = 'select' | 'review' | 'result' const ACCEPTED_TYPES = 'application/pdf,image/jpeg,image/png,image/webp' +const ACCEPTED_MIME = new Set(ACCEPTED_TYPES.split(',')) /** * Parallel attach requests during the final step. Same size as the @@ -105,6 +106,7 @@ export default function UnderlagImportWizard() { const { toast } = useToast() const { dialogProps, confirm } = useDestructiveConfirm() const fileInputRef = useRef(null) + const folderInputRef = useRef(null) const [step, setStep] = useState('select') const [isLoading, setIsLoading] = useState(false) @@ -162,7 +164,7 @@ export default function UnderlagImportWizard() { ) const handleFilesSelected = useCallback( - async (fileList: FileList | null) => { + async (fileList: FileList | File[] | null) => { if (!fileList || fileList.length === 0) return if (!fiscalPeriodId) return const files = Array.from(fileList) @@ -207,6 +209,27 @@ export default function UnderlagImportWizard() { [fetchPlan, fiscalPeriod, fiscalPeriodId], ) + // Folder pick (#2189): a migration's underlag arrives as one folder, and + // Ctrl+A in the file picker is not obvious to everyone. A directory pick + // ignores `accept` and includes every file in the tree (Thumbs.db, + // .DS_Store, the export tool's own CSV), so keep only the document types + // the attach route takes before planning; the plan keys on file.name, so + // subfolders flatten harmlessly. + const handleFolderSelected = useCallback( + (fileList: FileList | null) => { + if (!fileList || fileList.length === 0) return + const files = Array.from(fileList).filter( + (f) => ACCEPTED_MIME.has(f.type) && !f.name.startsWith('.'), + ) + if (files.length === 0) { + setError(t('underlag_folder_no_documents')) + return + } + void handleFilesSelected(files) + }, + [handleFilesSelected, t], + ) + const updateRow = useCallback((id: string, patch: Partial) => { setRows((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row))) }, []) @@ -433,18 +456,39 @@ export default function UnderlagImportWizard() { className="hidden" onChange={(e) => handleFilesSelected(e.target.files)} /> + {/* webkitdirectory is not in React's input typings but is what + every current browser honours for a folder pick; spread so + the attribute reaches the DOM without a type escape hatch. */} + handleFolderSelected(e.target.files)} + {...({ webkitdirectory: '' } as Record)} + /> - +
+ + +
{/* Both the picker and the button are disabled until a year is chosen, and if the company has no fiscal years at all the diff --git a/messages/en.json b/messages/en.json index b9b65539..48eda726 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7451,6 +7451,8 @@ "underlag_intro": "A SIE file carries the bookkeeping but not the receipts. If your old system named each receipt after its verifikat, they can be attached automatically, with no AI reading required.", "underlag_intro_formats": "Filenames that work: A31_abc123.pdf, A31.pdf, A-31 receipt.pdf, 2024-A-31.pdf. Matching uses the voucher number from your old system, not the number here.", "underlag_pick_files": "Pick files", + "underlag_pick_folder": "Pick a folder", + "underlag_folder_no_documents": "The folder holds no PDF or image files (PDF, JPEG, PNG, WebP).", "underlag_year_help": "The old system restarts voucher numbering every year, so A31 only identifies a verifikat within one year. Pick the year the export came from. Files are never attached to any other year.", "underlag_year_required": "Pick a fiscal year above to continue. If the year you want is missing, its bookkeeping was not imported via SIE.", "underlag_year_label": "Which fiscal year are these receipts from?", diff --git a/messages/sv.json b/messages/sv.json index 797e903d..ff980177 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -7451,6 +7451,8 @@ "underlag_intro": "En SIE-fil innehåller bokföringen men inte underlagen. Om ditt gamla system namngav varje underlag efter sitt verifikat kan de kopplas automatiskt, utan AI-tolkning.", "underlag_intro_formats": "Filnamn som fungerar: A31_abc123.pdf, A31.pdf, A-31 kvitto.pdf, 2024-A-31.pdf. Matchningen sker mot verifikatnumret i ditt gamla system, inte mot numret här.", "underlag_pick_files": "Välj filer", + "underlag_pick_folder": "Välj mapp", + "underlag_folder_no_documents": "Mappen innehåller inga PDF- eller bildfiler (PDF, JPEG, PNG, WebP).", "underlag_year_help": "Verifikatnumret i filnamnet räknas om varje år i det gamla systemet, så A31 pekar bara ut ett verifikat inom ett år. Välj det år exporten kommer från. Filerna kopplas aldrig till något annat år.", "underlag_year_required": "Välj ett räkenskapsår ovan för att fortsätta. Saknas det år du söker har bokföringen för det året inte importerats via SIE.", "underlag_year_label": "Vilket räkenskapsår gäller underlagen?",