feat(projekte): Projekttyp "Abruf" fuer Abrechnung nach Aufwand
Abruf braucht keinen Budgettopf: es gibt kein zugesagtes Volumen und kein Ablaufdatum, gebucht wird, was anfaellt. Der Satz kann deshalb nicht am Topf haengen und steht am Projekt (pulse_projects.rate, Stunde oder Tag). Der Umsatz sind die abrechenbaren Stunden mal diesem Satz; offen ist nichts. - Migration ergaenzt den ENUM-Wert und die Spalte rate - Anlegen verlangt einen Satz und nimmt keine Toepfe an; ein Topf nachtraeglich anzulegen wird abgewiesen - Wechsel auf Abruf nur, solange auf den Toepfen nichts gebucht ist - sonst wuerden Buchungen ihr Budget verlieren; leere Toepfe fallen weg - Buchungsdialog, Projektkarte, Projektdetail und Projektreport zeigen Satz und Umsatz statt Budgetbalken Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2af516ec20
commit
8c93df11d9
@@ -55,6 +55,7 @@ export const ProjectDetail: React.FC<Props> = ({ project: p, canEdit, users, onC
|
||||
setNewTask('');
|
||||
};
|
||||
|
||||
const isAbruf = p.type === 'abruf';
|
||||
const maxTask = Math.max(1, ...p.tasks.map((t) => t.used_minutes));
|
||||
const totalPotHours = p.pots.reduce((s, pot) => s + (pot.budget_hours ?? 0), 0);
|
||||
|
||||
@@ -65,7 +66,7 @@ export const ProjectDetail: React.FC<Props> = ({ project: p, canEdit, users, onC
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge tone={TYPE_TONE[p.type]}>
|
||||
{PROJECT_TYPE_LABELS[p.type]}
|
||||
{p.type === 'billable' && ` · pro ${p.billing_unit === 'day' ? 'Tag' : 'Stunde'}`}
|
||||
{(p.type === 'billable' || p.type === 'abruf') && ` · pro ${p.billing_unit === 'day' ? 'Tag' : 'Stunde'}`}
|
||||
</Badge>
|
||||
<span className="tabular text-xs font-bold text-ink-400">{p.code}</span>
|
||||
{p.tags.map((t) => (
|
||||
@@ -127,15 +128,33 @@ export const ProjectDetail: React.FC<Props> = ({ project: p, canEdit, users, onC
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-sm text-ink-900">
|
||||
<Icon name="pot" className="h-4 w-4 text-brand-600" />
|
||||
Budgettöpfe
|
||||
{isAbruf ? 'Abrechnung nach Aufwand' : 'Budgettöpfe'}
|
||||
</h3>
|
||||
{canEdit && (
|
||||
{canEdit && !isAbruf && (
|
||||
<Button size="sm" variant="secondary" icon="plus" onClick={() => setPotDialog({ pot: null })}>
|
||||
Topf
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAbruf && (
|
||||
/* Abruf hat kein Volumen und kein Ablaufdatum - nur einen Satz. */
|
||||
<div className="rounded-xl border border-ink-200 p-3">
|
||||
<div className="tabular grid grid-cols-2 gap-2 text-[11px] sm:grid-cols-4">
|
||||
<Kv label="Gebucht" value={`${formatHours(p.used_minutes, 1)} h`} />
|
||||
<Kv label="davon abrechenbar" value={`${formatHours(p.billable_minutes, 1)} h`} />
|
||||
<Kv
|
||||
label={p.billing_unit === 'day' ? 'Tagessatz' : 'Stundensatz'}
|
||||
value={canEdit ? formatEuro(p.rate, 2) : '–'}
|
||||
/>
|
||||
<Kv label="Umsatz" value={canEdit ? formatEuro(p.earned, 2) : '–'} />
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-ink-400">
|
||||
Kein Budgettopf: gebucht wird innerhalb der Projektlaufzeit ohne Limit, der Umsatz ergibt sich aus den abrechenbaren Stunden.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{p.pots.length > 1 && (
|
||||
<div className="mb-3 rounded-xl bg-ink-50 p-3">
|
||||
<p className="text-[11px] font-extrabold uppercase tracking-wider text-ink-400">Gesamt über alle Töpfe</p>
|
||||
|
||||
Reference in New Issue
Block a user