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
@@ -11,13 +11,14 @@ interface Props {
|
||||
dailyTarget: number;
|
||||
onAdd: (date: string) => void;
|
||||
onEdit: (entry: TimeEntry) => void;
|
||||
onCopy: (entry: TimeEntry) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wochenansicht wie ein moderner Stundenzettel: sieben Spalten, jede Buchung
|
||||
* als Karte mit Kundenfarbe. Auf dem Handy ein Tagesstreifen mit Liste.
|
||||
*/
|
||||
export const WeekView: React.FC<Props> = ({ from, entries, readOnly, dailyTarget, onAdd, onEdit }) => {
|
||||
export const WeekView: React.FC<Props> = ({ from, entries, readOnly, dailyTarget, onAdd, onEdit, onCopy }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const today = todayIso();
|
||||
const days = Array.from({ length: 7 }, (_, i) => addDays(from, i));
|
||||
@@ -58,7 +59,7 @@ export const WeekView: React.FC<Props> = ({ from, entries, readOnly, dailyTarget
|
||||
</div>
|
||||
<div key={selected} className="space-y-2">
|
||||
{list.map((entry, i) => (
|
||||
<EntryCard key={entry.id} entry={entry} index={i} onClick={() => onEdit(entry)} readOnly={readOnly} large />
|
||||
<EntryCard key={entry.id} entry={entry} index={i} onClick={() => onEdit(entry)} onCopy={() => onCopy(entry)} readOnly={readOnly} large />
|
||||
))}
|
||||
{list.length === 0 && (
|
||||
<button
|
||||
@@ -102,7 +103,7 @@ export const WeekView: React.FC<Props> = ({ from, entries, readOnly, dailyTarget
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2 p-2">
|
||||
{list.map((entry, j) => (
|
||||
<EntryCard key={entry.id} entry={entry} index={j} onClick={() => onEdit(entry)} readOnly={readOnly} />
|
||||
<EntryCard key={entry.id} entry={entry} index={j} onClick={() => onEdit(entry)} onCopy={() => onCopy(entry)} readOnly={readOnly} />
|
||||
))}
|
||||
{!readOnly && (
|
||||
<button
|
||||
@@ -131,32 +132,52 @@ const DayMeter: React.FC<{ minutes: number; target: number }> = ({ minutes, targ
|
||||
);
|
||||
};
|
||||
|
||||
export const EntryCard: React.FC<{ entry: TimeEntry; index: number; onClick: () => void; readOnly: boolean; large?: boolean }> = ({
|
||||
export const EntryCard: React.FC<{ entry: TimeEntry; index: number; onClick: () => void; onCopy?: () => void; readOnly: boolean; large?: boolean }> = ({
|
||||
entry,
|
||||
index,
|
||||
onClick,
|
||||
onCopy,
|
||||
readOnly,
|
||||
large,
|
||||
}) => {
|
||||
const color = colorFor(entry.customer_id, entry.customer_color);
|
||||
const canCopy = !readOnly && !!onCopy;
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={readOnly && !large}
|
||||
className={`animate-rise relative w-full overflow-hidden rounded-xl border border-ink-200 bg-surface text-left transition ${
|
||||
readOnly ? 'cursor-default' : 'hover:-translate-y-0.5 hover:border-brand-300 hover:shadow-lift'
|
||||
} ${large ? 'p-4 pl-5' : 'p-2.5 pl-3.5'}`}
|
||||
style={{ '--i': index } as React.CSSProperties}
|
||||
title={`${entry.project_display}${entry.task_name ? ` · ${entry.task_name}` : ''}${entry.note ? `\n${entry.note}` : ''}`}
|
||||
>
|
||||
<span aria-hidden className="absolute inset-y-0 left-0 w-1.5" style={{ background: color }} />
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className={`min-w-0 font-extrabold text-ink-900 ${large ? 'text-sm' : 'line-clamp-2 text-xs leading-snug'}`}>{entry.project_display}</p>
|
||||
<span className={`tabular shrink-0 font-extrabold text-ink-800 ${large ? 'text-lg' : 'text-xs'}`}>{formatDuration(entry.minutes)}</span>
|
||||
</div>
|
||||
{entry.task_name && <p className={`truncate font-semibold text-ink-500 ${large ? 'text-xs' : 'text-[11px]'}`}>{entry.task_name}</p>}
|
||||
{entry.note && <p className={`mt-0.5 text-ink-400 ${large ? 'text-xs' : 'line-clamp-2 text-[11px] leading-snug'}`}>{entry.note}</p>}
|
||||
{!entry.billable && <span className="mt-1 inline-block rounded-full bg-ink-100 px-1.5 text-[9px] font-bold uppercase tracking-wide text-ink-500">non-billable</span>}
|
||||
</button>
|
||||
// Kopier-Knopf als Geschwister der Karte - ein Button im Button ist kein gueltiges HTML.
|
||||
<div className="group/card relative">
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={readOnly && !large}
|
||||
className={`animate-rise relative w-full overflow-hidden rounded-xl border border-ink-200 bg-surface text-left transition ${
|
||||
readOnly ? 'cursor-default' : 'hover:-translate-y-0.5 hover:border-brand-300 hover:shadow-lift'
|
||||
} ${large ? 'p-4 pl-5' : 'p-2.5 pl-3.5'}`}
|
||||
style={{ '--i': index } as React.CSSProperties}
|
||||
title={`${entry.project_display}${entry.task_name ? ` · ${entry.task_name}` : ''}${entry.note ? `
|
||||
${entry.note}` : ''}`}
|
||||
>
|
||||
<span aria-hidden className="absolute inset-y-0 left-0 w-1.5" style={{ background: color }} />
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className={`min-w-0 font-extrabold text-ink-900 ${large ? 'text-sm' : 'line-clamp-2 text-xs leading-snug'}`}>{entry.project_display}</p>
|
||||
<span className={`tabular shrink-0 font-extrabold text-ink-800 ${large ? 'text-lg' : 'text-xs'}`}>{formatDuration(entry.minutes)}</span>
|
||||
</div>
|
||||
<div className={canCopy && large ? 'pr-10' : ''}>
|
||||
{entry.task_name && <p className={`truncate font-semibold text-ink-500 ${large ? 'text-xs' : 'text-[11px]'}`}>{entry.task_name}</p>}
|
||||
{entry.note && <p className={`mt-0.5 text-ink-400 ${large ? 'text-xs' : 'line-clamp-2 text-[11px] leading-snug'}`}>{entry.note}</p>}
|
||||
{!entry.billable && <span className="mt-1 inline-block rounded-full bg-ink-100 px-1.5 text-[9px] font-bold uppercase tracking-wide text-ink-500">non-billable</span>}
|
||||
</div>
|
||||
</button>
|
||||
{canCopy && (
|
||||
<button
|
||||
onClick={onCopy}
|
||||
title="Als neue Buchung kopieren"
|
||||
aria-label="Buchung kopieren"
|
||||
className={`absolute flex items-center justify-center rounded-lg border border-ink-200 bg-surface text-ink-500 shadow-card transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 pointer-fine:opacity-0 pointer-fine:group-hover/card:opacity-100 pointer-fine:focus-visible:opacity-100 ${
|
||||
large ? 'bottom-3 right-3 h-9 w-9' : 'bottom-1.5 right-1.5 h-6 w-6'
|
||||
}`}
|
||||
>
|
||||
<Icon name="copy" className={large ? 'h-4 w-4' : 'h-3.5 w-3.5'} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user