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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
interface TabBarProps<T extends string> {
|
||||
tabs: { id: T; label: string }[];
|
||||
active: T;
|
||||
onChange: (id: T) => void;
|
||||
}
|
||||
|
||||
/** Reiterleiste der Finanzbuchhaltungs-Module — Muster Zertifikat-Manager. */
|
||||
export function TabBar<T extends string>({ tabs, active, onChange }: TabBarProps<T>) {
|
||||
return (
|
||||
<nav className="flex gap-6 overflow-x-auto border-b border-border">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => onChange(tab.id)}
|
||||
aria-current={active === tab.id ? 'page' : undefined}
|
||||
className={`pb-2 text-sm font-medium transition-colors ${
|
||||
active === tab.id
|
||||
? 'border-b-2 border-primary-strong font-semibold text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,13 @@ const GLYPHS: Record<ModuleIconId, ReactNode> = {
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</>
|
||||
),
|
||||
utensils: (
|
||||
<>
|
||||
<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2" />
|
||||
<path d="M7 2v20" />
|
||||
<path d="M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7" />
|
||||
</>
|
||||
),
|
||||
tile: (
|
||||
<>
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
||||
|
||||
Reference in New Issue
Block a user