Compare commits
4 Commits
8f91feee0e
...
mai/brunel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d76ffec758 | ||
|
|
4b0ccac384 | ||
|
|
3030ef1e8b | ||
|
|
2578060638 |
@@ -18,6 +18,7 @@
|
|||||||
- ESLint must pass before committing
|
- ESLint must pass before committing
|
||||||
- Import aliases: `@/` maps to `src/`
|
- Import aliases: `@/` maps to `src/`
|
||||||
- Bun as package manager (not npm/yarn/pnpm)
|
- 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
|
## General
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se
|
|||||||
storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey)
|
storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey)
|
||||||
documentSvc := services.NewDocumentService(db, storageCli, auditSvc)
|
documentSvc := services.NewDocumentService(db, storageCli, auditSvc)
|
||||||
assignmentSvc := services.NewCaseAssignmentService(db)
|
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)
|
// AI service (optional — only if API key is configured)
|
||||||
var aiH *handlers.AIHandler
|
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)
|
eventH := handlers.NewCaseEventHandler(db)
|
||||||
docH := handlers.NewDocumentHandler(documentSvc)
|
docH := handlers.NewDocumentHandler(documentSvc)
|
||||||
assignmentH := handlers.NewCaseAssignmentHandler(assignmentSvc)
|
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
|
// Public routes
|
||||||
mux.HandleFunc("GET /health", handleHealth(db))
|
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)
|
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
|
// Wire: auth -> tenant routes go directly, scoped routes get tenant resolver
|
||||||
api.Handle("/api/", tenantResolver.Resolve(scoped))
|
api.Handle("/api/", tenantResolver.Resolve(scoped))
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import type { ApiError } from "@/lib/types";
|
|||||||
class ApiClient {
|
class ApiClient {
|
||||||
private baseUrl = "/api";
|
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> {
|
private async getHeaders(): Promise<HeadersInit> {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const {
|
const {
|
||||||
@@ -29,9 +37,10 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
path: string,
|
rawPath: string,
|
||||||
options: RequestInit = {},
|
options: RequestInit = {},
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const path = this.normalizePath(rawPath);
|
||||||
const headers = await this.getHeaders();
|
const headers = await this.getHeaders();
|
||||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||||
...options,
|
...options,
|
||||||
@@ -80,7 +89,8 @@ class ApiClient {
|
|||||||
return this.request<T>(path, { method: "DELETE" });
|
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 supabase = createClient();
|
||||||
const {
|
const {
|
||||||
data: { session },
|
data: { session },
|
||||||
|
|||||||
@@ -353,31 +353,6 @@ export interface DashboardData {
|
|||||||
recent_activity?: RecentActivity[];
|
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
|
// AI Extraction types
|
||||||
|
|
||||||
export interface ExtractedDeadline {
|
export interface ExtractedDeadline {
|
||||||
@@ -427,6 +402,13 @@ export const TEMPLATE_TYPES: Record<string, string> = {
|
|||||||
upc_injunction: "UPC Provisional Measures",
|
upc_injunction: "UPC Provisional Measures",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TEMPLATE_CATEGORY_LABELS: Record<string, string> = {
|
||||||
|
schriftsatz: "Schriftsatz",
|
||||||
|
vertrag: "Vertrag",
|
||||||
|
korrespondenz: "Korrespondenz",
|
||||||
|
intern: "Intern",
|
||||||
|
};
|
||||||
|
|
||||||
// AI Case Strategy
|
// AI Case Strategy
|
||||||
|
|
||||||
export interface StrategyStep {
|
export interface StrategyStep {
|
||||||
@@ -472,3 +454,244 @@ export interface SimilarCasesResponse {
|
|||||||
cases: SimilarCase[];
|
cases: SimilarCase[];
|
||||||
count: number;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user