Files
tessera-ctl/apps/web/src/components/accounting/file-drop-area.tsx
T
schalli 1f85277a3b feat(kantine-datev): Kantinenabrechnung als Modul in neuer Gruppe Finanzbuchhaltung
- neue Seitenleisten-Kategorie accounting (Finanzbuchhaltung / Financial accounting)
- CSV (UTF-8, UTF-8 mit BOM, Windows-1252) pruefen, Vorschau mit Zeilenfehlern, DATEV-Lohn-ASCII-Export
- Beraternummer, Mandantennummer, Lohnart je Mandant als Admin-Einstellung (KantineDatevConfig mit Zeilenschutz), anfangs leer
- hochgeladene Daten werden nicht gespeichert; Download per Blob (auch im Desktop-Client)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 11:32:55 +02:00

98 lines
2.9 KiB
TypeScript

'use client';
import { useRef, useState } from 'react';
interface FileDropAreaProps {
onFile: (file: File) => void;
onClear: () => void;
accept: string;
file: File | null;
/** Text in der leeren Flaeche. */
placeholder: string;
/** Zweite Zeile (erlaubte Formate). */
hint?: string;
/** Beschriftung des Entfernen-Knopfs (fuer Screenreader). */
clearLabel: string;
disabled?: boolean;
}
/**
* Ablageflaeche fuer eine einzelne Datei (Finanzbuchhaltungs-Module,
* quick-261002-fm5). Eigenstaendig, ohne Texte aus einem anderen Modul —
* Bauform wie die Ablage des Zertifikat-Managers: echte Schaltflaeche fuer
* Klick UND Ziehen, Entfernen als Geschwister daneben.
*/
export function FileDropArea({
onFile,
onClear,
accept,
file,
placeholder,
hint,
clearLabel,
disabled = false,
}: FileDropAreaProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [isDragOver, setIsDragOver] = useState(false);
return (
<div>
<input
ref={inputRef}
type="file"
accept={accept}
className="hidden"
data-testid="file-drop-input"
onChange={(e) => {
const picked = e.target.files?.[0];
if (picked) onFile(picked);
e.target.value = '';
}}
/>
<div className="relative">
<button
type="button"
disabled={disabled}
onClick={() => inputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={(e) => {
e.preventDefault();
setIsDragOver(false);
const dropped = e.dataTransfer.files?.[0];
if (dropped && !disabled) onFile(dropped);
}}
className={`block w-full cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${
isDragOver ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'
}`}
>
{file ? (
<span className="text-sm text-foreground">
{file.name}{' '}
<span className="text-muted-foreground">({(file.size / 1024).toFixed(1)} KB)</span>
</span>
) : (
<span className="block space-y-1">
<span className="block text-sm text-muted-foreground">{placeholder}</span>
{hint && <span className="block text-xs text-muted-foreground">{hint}</span>}
</span>
)}
</button>
{file && (
<button
type="button"
aria-label={clearLabel}
onClick={onClear}
className="absolute right-2 top-2 rounded border border-border bg-card px-2 py-0.5 text-xs text-muted-foreground hover:text-foreground"
>
&#x2715;
</button>
)}
</div>
</div>
);
}