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:
schakal00783
2026-09-18 00:13:35 +02:00
co-authored by Claude Opus 5
parent 2af516ec20
commit 8c93df11d9
12 changed files with 182 additions and 35 deletions
+6 -2
View File
@@ -32,7 +32,8 @@ function inListOrder(projects: Project[]) {
*/
function defaultProject(projects: Project[], date: string, settings: { hours_per_day: number; budget_warn_percent: number; budget_warn_days: number }): number | '' {
const ordered = inListOrder(projects);
const billable = ordered.filter((p) => p.type === 'billable');
// Billable und Abruf werden beide nach Aufwand abgerechnet.
const billable = ordered.filter((p) => p.type === 'billable' || p.type === 'abruf');
const bookableNow = billable.find(
(p) => checkPot(p, date, 0, settings.hours_per_day, settings.budget_warn_percent, settings.budget_warn_days).ok
);
@@ -288,7 +289,10 @@ export const EntryModal: React.FC<Props> = ({ projects, date: initialDate, entry
{PROJECT_TYPE_LABELS[project.type]} · {project.code}
</p>
<p className={`mt-1 text-sm font-semibold ${check?.level === 'error' ? 'text-red-700' : check?.level === 'warn' ? 'text-amber-700' : 'text-ink-700'}`}>
{check?.message ?? 'Freies Projekt ohne Budgettopf.'}
{check?.message ??
(project.type === 'abruf'
? 'Abruf: Abrechnung nach Aufwand, kein Budgetlimit.'
: 'Freies Projekt ohne Budgettopf.')}
</p>
{pot && pot.budget_hours != null && (
<div className="mt-3">