feat(buchung): Buchung per Monatskalender auf mehrere Tage kopieren

Das Kopier-Icon (Liste, Wochenansicht) und "Kopieren" im Bearbeiten-Dialog
oeffnen jetzt einen kleinen Monatskalender. Dort werden beliebige Tage
an- und abgewaehlt, auch ueber Monate hinweg, und in einem Schritt gebucht.

- Tage ausserhalb der Projektlaufzeit bzw. ohne Budgettopf sind gesperrt.
- Urlaub/Feiertage sind markiert, bereits gebuchte Stunden je Tag sichtbar.
- "Werktage" waehlt Mo-Fr des Monats ohne Urlaub und Feiertag.
- Buchungen laufen nacheinander; fehlgeschlagene Tage bleiben ausgewaehlt.
- Der Vorlage-Modus im Buchungsdialog entfaellt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-25 13:51:18 +02:00
co-authored by Claude Opus 5.5
parent 2b9b7a6ae3
commit ef709d423c
5 changed files with 304 additions and 25 deletions
+9 -18
View File
@@ -13,8 +13,6 @@ interface Props {
projects: Project[];
date: string;
entry?: TimeEntry | null;
/** Vorlage fuer eine neue Buchung (Kopie): Projekt, Task, Notiz und Zeit werden uebernommen. */
template?: TimeEntry | null;
onClose: () => void;
onSaved: () => void;
onDuplicate?: (entry: TimeEntry) => void;
@@ -54,26 +52,19 @@ function firstTask(project: Project | undefined): number | '' {
* Buchungsdialog: oben Projekt, darunter Task, darunter Freitext mit der Zeit
* rechts daneben. Die Zeit versteht "1:23" und "1,5".
*/
export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry, template, onClose, onSaved, onDuplicate }) => {
export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry, onClose, onSaved, onDuplicate }) => {
const { settings } = useAuth();
const { addToast } = useToast();
const bookable = useMemo(() => projects.filter((p) => p.is_active || p.id === entry?.project_id), [projects, entry]);
/*
* Kopie: nur uebernehmen, solange das Projekt noch buchbar ist. Ein
* inaktiver Task faellt ueber den Projekt-Effekt unten auf den ersten
* aktiven Task zurueck.
*/
const copyFrom = !entry && template && bookable.some((p) => p.id === template.project_id) ? template : null;
const source = entry ?? copyFrom;
const [projectId, setProjectId] = useState<number | ''>(() => source?.project_id ?? defaultProject(bookable, initialDate, settings));
const [projectId, setProjectId] = useState<number | ''>(() => entry?.project_id ?? defaultProject(bookable, initialDate, settings));
// Neue Buchung: erster aktiver Task des vorausgewaehlten Projekts.
const [taskId, setTaskId] = useState<number | ''>(() =>
source ? source.task_id ?? '' : firstTask(bookable.find((p) => p.id === projectId))
entry ? entry.task_id ?? '' : firstTask(bookable.find((p) => p.id === projectId))
);
const [note, setNote] = useState(source?.note ?? '');
const [note, setNote] = useState(entry?.note ?? '');
const [date, setDate] = useState(entry?.entry_date ?? initialDate);
const [timeText, setTimeText] = useState(source ? formatDuration(source.minutes) : '0:00');
const [timeText, setTimeText] = useState(entry ? formatDuration(entry.minutes) : '0:00');
const [busy, setBusy] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const timeRef = useRef<HTMLInputElement>(null);
@@ -105,7 +96,7 @@ export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry
// Projekt gewechselt: passt der Task nicht mehr, den ersten Task des neuen Projekts nehmen.
useEffect(() => {
if (taskId && tasks.some((t) => t.id === taskId)) return;
if (!taskId && source && source.project_id === projectId) return; // bewusst ohne Task gebucht
if (!taskId && entry && entry.project_id === projectId) return; // bewusst ohne Task gebucht
setTaskId(firstTask(project ?? undefined));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
@@ -182,7 +173,7 @@ export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry
return (
<>
<Modal
title={entry ? 'Buchung bearbeiten' : copyFrom ? 'Buchung kopieren' : 'Zeit buchen'}
title={entry ? 'Buchung bearbeiten' : 'Zeit buchen'}
subtitle={formatDate(date, 'long')}
onClose={onClose}
size="md"
@@ -194,8 +185,8 @@ export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry
<span className="hidden sm:inline">Löschen</span>
</Button>
{onDuplicate && (
<Button variant="ghost" icon="copy" onClick={() => onDuplicate(entry)} title="Als neue Buchung kopieren" aria-label="Duplizieren">
<span className="hidden sm:inline">Duplizieren</span>
<Button variant="ghost" icon="copy" onClick={() => onDuplicate(entry)} title="Auf andere Tage kopieren" aria-label="Kopieren">
<span className="hidden sm:inline">Kopieren</span>
</Button>
)}
</div>