feat: time tracking + billing — hourly rates, time entries, invoices (P1)

Database: time_entries, billing_rates, invoices tables with RLS.
Backend: CRUD services+handlers for time entries, billing rates, invoices.
  - Time entries: list/create/update/delete, summary by case/user/month
  - Billing rates: upsert with auto-close previous, current rate lookup
  - Invoices: create with auto-number (RE-YYYY-NNN), status transitions
    (draft->sent->paid, cancellation), link time entries on invoice create
API: 11 new endpoints under /api/time-entries, /api/billing-rates, /api/invoices
Frontend: Zeiterfassung tab on case detail, /abrechnung overview with filters,
  /abrechnung/rechnungen list+detail with status actions, billing rates settings
Also: resolved merge conflicts between audit-trail and role-based branches,
  added missing types (Notification, AuditLogResponse, NotificationPreferences)
This commit is contained in:
m
2026-03-30 11:24:36 +02:00
parent 8e65463130
commit 238811727d
23 changed files with 2346 additions and 129 deletions

View File

@@ -29,14 +29,11 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
deadlineRuleSvc := services.NewDeadlineRuleService(db)
calculator := services.NewDeadlineCalculator(holidaySvc)
storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey)
<<<<<<< HEAD
documentSvc := services.NewDocumentService(db, storageCli, auditSvc)
||||||| 82878df
documentSvc := services.NewDocumentService(db, storageCli)
=======
documentSvc := services.NewDocumentService(db, storageCli)
assignmentSvc := services.NewCaseAssignmentService(db)
>>>>>>> mai/pike/p0-role-based
timeEntrySvc := services.NewTimeEntryService(db, auditSvc)
billingRateSvc := services.NewBillingRateService(db, auditSvc)
invoiceSvc := services.NewInvoiceService(db, auditSvc)
// AI service (optional — only if API key is configured)
var aiH *handlers.AIHandler
@@ -71,6 +68,9 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
eventH := handlers.NewCaseEventHandler(db)
docH := handlers.NewDocumentHandler(documentSvc)
assignmentH := handlers.NewCaseAssignmentHandler(assignmentSvc)
timeEntryH := handlers.NewTimeEntryHandler(timeEntrySvc)
billingRateH := handlers.NewBillingRateHandler(billingRateSvc)
invoiceH := handlers.NewInvoiceHandler(invoiceSvc)
// Public routes
mux.HandleFunc("GET /health", handleHealth(db))
@@ -112,7 +112,7 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
scoped.HandleFunc("GET /api/cases", caseH.List)
scoped.HandleFunc("POST /api/cases", perm(auth.PermCreateCase, caseH.Create))
scoped.HandleFunc("GET /api/cases/{id}", caseH.Get)
scoped.HandleFunc("PUT /api/cases/{id}", caseH.Update) // case-level access checked in handler
scoped.HandleFunc("PUT /api/cases/{id}", caseH.Update)
scoped.HandleFunc("DELETE /api/cases/{id}", perm(auth.PermCreateCase, caseH.Delete))
// Parties — same access as case editing
@@ -162,21 +162,15 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
// Dashboard — all can view
scoped.HandleFunc("GET /api/dashboard", dashboardH.Get)
<<<<<<< HEAD
// Audit log
scoped.HandleFunc("GET /api/audit-log", auditH.List)
// Audit log — view requires PermViewAuditLog
scoped.HandleFunc("GET /api/audit-log", perm(auth.PermViewAuditLog, auditH.List))
// Documents
||||||| 82878df
// Documents
=======
// Documents — all can upload, delete checked in handler (own vs all)
>>>>>>> mai/pike/p0-role-based
scoped.HandleFunc("GET /api/cases/{id}/documents", docH.ListByCase)
scoped.HandleFunc("POST /api/cases/{id}/documents", perm(auth.PermUploadDocuments, docH.Upload))
scoped.HandleFunc("GET /api/documents/{docId}", docH.Download)
scoped.HandleFunc("GET /api/documents/{docId}/meta", docH.GetMeta)
scoped.HandleFunc("DELETE /api/documents/{docId}", docH.Delete) // permission check inside handler
scoped.HandleFunc("DELETE /api/documents/{docId}", docH.Delete)
// AI endpoints (rate limited: 5 req/min burst 10 per IP)
if aiH != nil {
@@ -185,7 +179,6 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
scoped.HandleFunc("POST /api/ai/summarize-case", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.SummarizeCase)))
}
<<<<<<< HEAD
// Notifications
if notifH != nil {
scoped.HandleFunc("GET /api/notifications", notifH.List)
@@ -196,18 +189,32 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
scoped.HandleFunc("PUT /api/notification-preferences", notifH.UpdatePreferences)
}
// CalDAV sync endpoints
||||||| 82878df
// CalDAV sync endpoints
=======
// CalDAV sync endpoints — settings permission required
>>>>>>> mai/pike/p0-role-based
if calDAVSvc != nil {
calDAVH := handlers.NewCalDAVHandler(calDAVSvc)
scoped.HandleFunc("POST /api/caldav/sync", perm(auth.PermManageSettings, calDAVH.TriggerSync))
scoped.HandleFunc("GET /api/caldav/status", calDAVH.GetStatus)
}
// Time entries — billing permission for create/update/delete
scoped.HandleFunc("GET /api/cases/{id}/time-entries", timeEntryH.ListForCase)
scoped.HandleFunc("POST /api/cases/{id}/time-entries", perm(auth.PermManageBilling, timeEntryH.Create))
scoped.HandleFunc("GET /api/time-entries", timeEntryH.List)
scoped.HandleFunc("GET /api/time-entries/summary", perm(auth.PermManageBilling, timeEntryH.Summary))
scoped.HandleFunc("PUT /api/time-entries/{id}", perm(auth.PermManageBilling, timeEntryH.Update))
scoped.HandleFunc("DELETE /api/time-entries/{id}", perm(auth.PermManageBilling, timeEntryH.Delete))
// Billing rates — billing permission required
scoped.HandleFunc("GET /api/billing-rates", perm(auth.PermManageBilling, billingRateH.List))
scoped.HandleFunc("PUT /api/billing-rates", perm(auth.PermManageBilling, billingRateH.Upsert))
// Invoices — billing permission required
scoped.HandleFunc("GET /api/invoices", perm(auth.PermManageBilling, invoiceH.List))
scoped.HandleFunc("POST /api/invoices", perm(auth.PermManageBilling, invoiceH.Create))
scoped.HandleFunc("GET /api/invoices/{id}", perm(auth.PermManageBilling, invoiceH.Get))
scoped.HandleFunc("PUT /api/invoices/{id}", perm(auth.PermManageBilling, invoiceH.Update))
scoped.HandleFunc("PATCH /api/invoices/{id}/status", perm(auth.PermManageBilling, invoiceH.UpdateStatus))
// Wire: auth -> tenant routes go directly, scoped routes get tenant resolver
api.Handle("/api/", tenantResolver.Resolve(scoped))