feat(buchung): Buchungen kopieren
Duplizieren-Button im Bearbeiten-Dialog und Kopier-Icon je Buchung in Liste und Wochenansicht. Die Kopie oeffnet den Dialog als neue Buchung fuer heute mit Projekt, Task, Notiz und Zeit der Vorlage. Inaktive Projekte oder Tasks fallen auf die Standardauswahl zurueck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b6ba7860bf
commit
2b9b7a6ae3
@@ -13,8 +13,11 @@ 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;
|
||||
}
|
||||
|
||||
/** Reihenfolge wie in der Auswahlliste: nach Kunde gruppiert, innerhalb in API-Reihenfolge. */
|
||||
@@ -51,19 +54,26 @@ 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, onClose, onSaved }) => {
|
||||
export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry, template, 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 | ''>(() => entry?.project_id ?? defaultProject(bookable, initialDate, settings));
|
||||
const [projectId, setProjectId] = useState<number | ''>(() => source?.project_id ?? defaultProject(bookable, initialDate, settings));
|
||||
// Neue Buchung: erster aktiver Task des vorausgewaehlten Projekts.
|
||||
const [taskId, setTaskId] = useState<number | ''>(() =>
|
||||
entry ? entry.task_id ?? '' : firstTask(bookable.find((p) => p.id === projectId))
|
||||
source ? source.task_id ?? '' : firstTask(bookable.find((p) => p.id === projectId))
|
||||
);
|
||||
const [note, setNote] = useState(entry?.note ?? '');
|
||||
const [note, setNote] = useState(source?.note ?? '');
|
||||
const [date, setDate] = useState(entry?.entry_date ?? initialDate);
|
||||
const [timeText, setTimeText] = useState(entry ? formatDuration(entry.minutes) : '0:00');
|
||||
const [timeText, setTimeText] = useState(source ? formatDuration(source.minutes) : '0:00');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const timeRef = useRef<HTMLInputElement>(null);
|
||||
@@ -95,7 +105,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 && entry && entry.project_id === projectId) return; // bewusst ohne Task gebucht
|
||||
if (!taskId && source && source.project_id === projectId) return; // bewusst ohne Task gebucht
|
||||
setTaskId(firstTask(project ?? undefined));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
@@ -172,16 +182,23 @@ export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={entry ? 'Buchung bearbeiten' : 'Zeit buchen'}
|
||||
title={entry ? 'Buchung bearbeiten' : copyFrom ? 'Buchung kopieren' : 'Zeit buchen'}
|
||||
subtitle={formatDate(date, 'long')}
|
||||
onClose={onClose}
|
||||
size="md"
|
||||
footer={
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{entry ? (
|
||||
<Button variant="ghost" icon="trash" className="text-red-600 hover:bg-red-50" onClick={() => setConfirmDelete(true)}>
|
||||
Löschen
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="ghost" icon="trash" className="text-red-600 hover:bg-red-50" onClick={() => setConfirmDelete(true)} aria-label="Löschen">
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="hidden text-[11px] text-ink-400 sm:block">Enter im Zeitfeld bucht · sonst Strg + Enter</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user