1f85277a3b
- 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>
98 lines
2.9 KiB
TypeScript
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"
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|