Compare commits

..

4 Commits

Author SHA1 Message Date
m
d76ffec758 fix: wire all missing routes in router.go
Register routes for reports, time entries, invoices, billing rates,
and document templates. All handlers and services already existed but
were not connected in the router.

Permission mapping:
- Reports, invoices, billing rates: PermManageBilling (partners+owners)
- Templates create/update/delete: PermCreateCase
- Time entries, template read/render: all authenticated users
2026-03-30 13:11:17 +02:00
m
4b0ccac384 fix: auto-strip /api/ prefix in api client + document convention
The api client now calls normalizePath() to strip accidental /api/
prefixes. This prevents the recurring /api/api/ double-prefix bug.
Added convention note to .claude/CLAUDE.md so future workers know.
2026-03-30 13:05:02 +02:00
m
3030ef1e8b fix: add all missing type exports (TimeEntry, Invoice, reports, notifications, audit) 2026-03-30 11:52:10 +02:00
m
2578060638 fix: add missing TEMPLATE_CATEGORY_LABELS export to types.ts 2026-03-30 11:43:36 +02:00
4 changed files with 304 additions and 27 deletions

View File

@@ -18,6 +18,7 @@
- ESLint must pass before committing
- Import aliases: `@/` maps to `src/`
- Bun as package manager (not npm/yarn/pnpm)
- **API paths: NEVER include `/api/` prefix.** The `api` client in `lib/api.ts` already has `baseUrl="/api"`. Write `api.get("/cases")` NOT `api.get("/api/cases")`. The client auto-strips accidental `/api/` prefixes but don't rely on it.
## General

View File

@@ -32,6 +32,11 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey)
documentSvc := services.NewDocumentService(db, storageCli, auditSvc)
assignmentSvc := services.NewCaseAssignmentService(db)
reportSvc := services.NewReportingService(db)
timeEntrySvc := services.NewTimeEntryService(db, auditSvc)
invoiceSvc := services.NewInvoiceService(db, auditSvc)
billingRateSvc := services.NewBillingRateService(db, auditSvc)
templateSvc := services.NewTemplateService(db, auditSvc)
// AI service (optional — only if API key is configured)
var aiH *handlers.AIHandler
@@ -71,6 +76,11 @@ 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)
reportH := handlers.NewReportHandler(reportSvc)
timeH := handlers.NewTimeEntryHandler(timeEntrySvc)
invoiceH := handlers.NewInvoiceHandler(invoiceSvc)
billingH := handlers.NewBillingRateHandler(billingRateSvc)
templateH := handlers.NewTemplateHandler(templateSvc, caseSvc, partySvc, deadlineSvc, tenantSvc)
// Public routes
mux.HandleFunc("GET /health", handleHealth(db))
@@ -205,6 +215,39 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
scoped.HandleFunc("GET /api/caldav/status", calDAVH.GetStatus)
}
// Reports — billing permission (partners + owners)
scoped.HandleFunc("GET /api/reports/cases", perm(auth.PermManageBilling, reportH.Cases))
scoped.HandleFunc("GET /api/reports/deadlines", perm(auth.PermManageBilling, reportH.Deadlines))
scoped.HandleFunc("GET /api/reports/workload", perm(auth.PermManageBilling, reportH.Workload))
scoped.HandleFunc("GET /api/reports/billing", perm(auth.PermManageBilling, reportH.Billing))
// Time entries — all can view/create, tied to cases
scoped.HandleFunc("GET /api/cases/{id}/time-entries", timeH.ListForCase)
scoped.HandleFunc("GET /api/time-entries", timeH.List)
scoped.HandleFunc("POST /api/cases/{id}/time-entries", timeH.Create)
scoped.HandleFunc("PUT /api/time-entries/{id}", timeH.Update)
scoped.HandleFunc("DELETE /api/time-entries/{id}", timeH.Delete)
scoped.HandleFunc("GET /api/time-entries/summary", timeH.Summary)
// Invoices — billing permission required
scoped.HandleFunc("GET /api/invoices", perm(auth.PermManageBilling, invoiceH.List))
scoped.HandleFunc("GET /api/invoices/{id}", perm(auth.PermManageBilling, invoiceH.Get))
scoped.HandleFunc("POST /api/invoices", perm(auth.PermManageBilling, invoiceH.Create))
scoped.HandleFunc("PUT /api/invoices/{id}", perm(auth.PermManageBilling, invoiceH.Update))
scoped.HandleFunc("PATCH /api/invoices/{id}/status", perm(auth.PermManageBilling, invoiceH.UpdateStatus))
// Billing rates — billing permission required
scoped.HandleFunc("GET /api/billing-rates", perm(auth.PermManageBilling, billingH.List))
scoped.HandleFunc("PUT /api/billing-rates", perm(auth.PermManageBilling, billingH.Upsert))
// Document templates — all can view/use, manage needs case creation permission
scoped.HandleFunc("GET /api/templates", templateH.List)
scoped.HandleFunc("GET /api/templates/{id}", templateH.Get)
scoped.HandleFunc("POST /api/templates", perm(auth.PermCreateCase, templateH.Create))
scoped.HandleFunc("PUT /api/templates/{id}", perm(auth.PermCreateCase, templateH.Update))
scoped.HandleFunc("DELETE /api/templates/{id}", perm(auth.PermCreateCase, templateH.Delete))
scoped.HandleFunc("POST /api/templates/{id}/render", templateH.Render)
// Wire: auth -> tenant routes go directly, scoped routes get tenant resolver
api.Handle("/api/", tenantResolver.Resolve(scoped))

View File

@@ -4,6 +4,14 @@ import type { ApiError } from "@/lib/types";
class ApiClient {
private baseUrl = "/api";
/** Strip leading /api/ if accidentally included — baseUrl already provides it */
private normalizePath(path: string): string {
if (path.startsWith("/api/")) {
return path.slice(4); // "/api/foo" -> "/foo"
}
return path;
}
private async getHeaders(): Promise<HeadersInit> {
const supabase = createClient();
const {
@@ -29,9 +37,10 @@ class ApiClient {
}
private async request<T>(
path: string,
rawPath: string,
options: RequestInit = {},
): Promise<T> {
const path = this.normalizePath(rawPath);
const headers = await this.getHeaders();
const res = await fetch(`${this.baseUrl}${path}`, {
...options,
@@ -80,7 +89,8 @@ class ApiClient {
return this.request<T>(path, { method: "DELETE" });
}
async postFormData<T>(path: string, formData: FormData): Promise<T> {
async postFormData<T>(rawPath: string, formData: FormData): Promise<T> {
const path = this.normalizePath(rawPath);
const supabase = createClient();
const {
data: { session },

View File

@@ -353,31 +353,6 @@ export interface DashboardData {
recent_activity?: RecentActivity[];
}
// Notes
export interface Note {
id: string;
tenant_id: string;
case_id?: string;
deadline_id?: string;
appointment_id?: string;
case_event_id?: string;
content: string;
created_by?: string;
created_at: string;
updated_at: string;
}
// Recent Activity
export interface RecentActivity {
id: string;
event_type?: string;
title: string;
case_id: string;
case_number: string;
event_date?: string;
created_at: string;
}
// AI Extraction types
export interface ExtractedDeadline {
@@ -427,6 +402,13 @@ export const TEMPLATE_TYPES: Record<string, string> = {
upc_injunction: "UPC Provisional Measures",
};
export const TEMPLATE_CATEGORY_LABELS: Record<string, string> = {
schriftsatz: "Schriftsatz",
vertrag: "Vertrag",
korrespondenz: "Korrespondenz",
intern: "Intern",
};
// AI Case Strategy
export interface StrategyStep {
@@ -472,3 +454,244 @@ export interface SimilarCasesResponse {
cases: SimilarCase[];
count: number;
}
// Time Tracking
export interface TimeEntry {
id: string;
tenant_id: string;
case_id: string;
user_id: string;
description: string;
duration_minutes: number;
hourly_rate: number;
billable: boolean;
billed?: boolean;
activity?: string;
date: string;
created_at: string;
updated_at: string;
}
// Billing
export interface InvoiceItem {
description: string;
amount: number;
duration_minutes?: number;
hourly_rate?: number;
}
export interface Invoice {
id: string;
tenant_id: string;
case_id: string;
invoice_number: string;
client_name: string;
client_address?: string;
items: InvoiceItem[];
subtotal: number;
tax_rate: number;
tax_amount: number;
total: number;
status: string;
notes?: string;
issued_at?: string;
due_at?: string;
paid_at?: string;
created_at: string;
updated_at: string;
}
export interface BillingRate {
id: string;
tenant_id: string;
user_id?: string;
rate: number;
currency: string;
valid_from: string;
valid_to?: string;
created_at: string;
updated_at: string;
}
// Reports
export interface BillingReportMonthly {
period: string;
cases_new: number;
cases_closed: number;
cases_active: number;
}
export interface BillingReportByType {
case_type: string;
total: number;
active: number;
closed: number;
}
export interface BillingReport {
total_revenue: number;
outstanding: number;
billable_hours: number;
non_billable_hours: number;
monthly: BillingReportMonthly[];
by_type: BillingReportByType[];
}
export interface CaseReportTotal {
opened: number;
closed: number;
active: number;
}
export interface CaseReportMonthly {
period: string;
opened: number;
closed: number;
active: number;
}
export interface CaseReportByType {
case_type: string;
count: number;
active: number;
closed: number;
total: number;
}
export interface CaseReportByCourt {
court: string;
count: number;
}
export interface CaseReport {
opened: number;
closed: number;
active: number;
total: CaseReportTotal;
monthly: CaseReportMonthly[];
by_type: CaseReportByType[];
by_court: CaseReportByCourt[];
}
export interface DeadlineReportTotal {
total: number;
met: number;
missed: number;
compliance_rate: number;
}
export interface DeadlineReportMonthly {
period: string;
total: number;
met: number;
missed: number;
pending: number;
compliance_rate: number;
}
export interface MissedDeadline {
id: string;
title: string;
case_id: string;
case_number: string;
case_title: string;
due_date: string;
days_overdue: number;
}
export interface DeadlineReport {
compliance_rate: number;
met: number;
total: DeadlineReportTotal;
monthly: DeadlineReportMonthly[];
missed: MissedDeadline[];
by_case: Record<string, number>;
}
export interface WorkloadUser {
name: string;
user_id: string;
hours: number;
utilization: number;
active_cases: number;
deadlines: number;
overdue: number;
completed: number;
}
export interface WorkloadReport {
users: WorkloadUser[];
}
// Document Templates
export interface DocumentTemplate {
id: string;
tenant_id: string;
name: string;
description?: string;
category: string;
content: string;
variables: string[];
is_system: boolean;
created_at: string;
updated_at: string;
}
export interface RenderResponse {
rendered_content: string;
content: string;
name: string;
}
// Notifications
export interface Notification {
id: string;
tenant_id: string;
type: string;
entity_type: string;
entity_id: string;
title: string;
body: string;
sent_at?: string;
read_at?: string;
created_at: string;
updated_at: string;
}
export interface NotificationPreferences {
deadline_reminder_days: number[];
email_enabled: boolean;
daily_digest: boolean;
}
export interface NotificationListResponse {
notifications: Notification[];
data: Notification[];
total: number;
unread_count: number;
}
// Audit Log
export interface AuditLogEntry {
id: string;
tenant_id: string;
user_id: string;
action: string;
entity_type: string;
entity_id: string;
old_values?: Record<string, unknown>;
new_values?: Record<string, unknown>;
ip_address?: string;
created_at: string;
}
export interface AuditLogResponse {
entries: AuditLogEntry[];
total: number;
}