feat: basic POS & invoicing (closes groombook/groombook#5) (#26)

- Add invoice_status and payment_method enums to schema
- Add invoices table: appointmentId, clientId, subtotal/tax/tip/total cents,
  status (draft/pending/paid/void), paymentMethod, paidAt, notes
- Add invoice_line_items table: invoiceId, description, qty, unitPrice, total
- Migration 0002_invoices.sql with FK constraints and journal entry
- POST /api/invoices — create invoice with line items
- POST /api/invoices/from-appointment/:id — one-click invoice from appointment,
  pre-populated with service name and price; returns 409 if already invoiced
- GET /api/invoices — list with optional ?status/clientId/appointmentId filters
- GET /api/invoices/:id — invoice with line items
- PATCH /api/invoices/:id — update status, payment method, tip, notes; auto-sets
  paidAt when marking paid; blocks edits on voided invoices
- Add Invoice/InvoiceLineItem types to @groombook/types
- InvoicesPage: list view with status filter, create from appointment modal,
  detail modal with tip input, payment method selector, Mark as Paid/Void actions
- Add Invoices nav link in App.tsx

Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #26.
This commit is contained in:
groombook-paperclip[bot]
2026-03-17 20:02:04 +00:00
committed by GitHub
parent eb9255eee0
commit b767a00b5f
8 changed files with 884 additions and 0 deletions
+30
View File
@@ -69,6 +69,36 @@ export interface Appointment {
updatedAt: string;
}
export type InvoiceStatus = "draft" | "pending" | "paid" | "void";
export type PaymentMethod = "cash" | "card" | "check" | "other";
export interface InvoiceLineItem {
id: string;
invoiceId: string;
description: string;
quantity: number;
unitPriceCents: number;
totalCents: number;
createdAt: string;
}
export interface Invoice {
id: string;
appointmentId: string | null;
clientId: string;
subtotalCents: number;
taxCents: number;
tipCents: number;
totalCents: number;
status: InvoiceStatus;
paymentMethod: PaymentMethod | null;
paidAt: string | null;
notes: string | null;
createdAt: string;
updatedAt: string;
lineItems?: InvoiceLineItem[];
}
// Paginated list response
export interface PaginatedList<T> {
items: T[];