From 3373762d97138999c580c21914b4379df51265b1 Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Thu, 25 Jun 2026 11:40:05 +0100 Subject: [PATCH 1/2] feat: add invoice lifecycle schema --- drizzle/0016_outstanding_talkback.sql | 4 + drizzle/meta/0016_snapshot.json | 2106 +++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/lib/schemas/invoices.schema.ts | 4 + src/lib/server/db/invoices.schema.ts | 8 +- 5 files changed, 2128 insertions(+), 1 deletion(-) create mode 100644 drizzle/0016_outstanding_talkback.sql create mode 100644 drizzle/meta/0016_snapshot.json diff --git a/drizzle/0016_outstanding_talkback.sql b/drizzle/0016_outstanding_talkback.sql new file mode 100644 index 0000000..11f9b31 --- /dev/null +++ b/drizzle/0016_outstanding_talkback.sql @@ -0,0 +1,4 @@ +ALTER TABLE `invoices` ADD `status` text DEFAULT 'draft' NOT NULL;--> statement-breakpoint +ALTER TABLE `invoices` ADD `sent_at` integer;--> statement-breakpoint +ALTER TABLE `invoices` ADD `paid_at` integer;--> statement-breakpoint +ALTER TABLE `invoices` ADD `voided_at` integer; \ No newline at end of file diff --git a/drizzle/meta/0016_snapshot.json b/drizzle/meta/0016_snapshot.json new file mode 100644 index 0000000..c872170 --- /dev/null +++ b/drizzle/meta/0016_snapshot.json @@ -0,0 +1,2106 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3e7f2314-8ad9-45ab-a8b1-56bac54a93ba", + "prevId": "945d719e-4e40-434b-b9db-e63e5fbf4301", + "tables": { + "task": { + "name": "task", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_organization_id_organizations_id_fk": { + "name": "clients_organization_id_organizations_id_fk", + "tableFrom": "clients", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "address_line_2": { + "name": "address_line_2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_line_3": { + "name": "address_line_3", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'United Kingdom'" + }, + "bank_name": { + "name": "bank_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bank_account_name": { + "name": "bank_account_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bank_account_number": { + "name": "bank_account_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bank_sort_code": { + "name": "bank_sort_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bank_iban": { + "name": "bank_iban", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bank_swift": { + "name": "bank_swift", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_memberships": { + "name": "organization_memberships", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_memberships_organization_user_unique": { + "name": "organization_memberships_organization_user_unique", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": true + }, + "organization_memberships_organization_id_idx": { + "name": "organization_memberships_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_memberships_user_id_idx": { + "name": "organization_memberships_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organization_memberships_organization_id_organizations_id_fk": { + "name": "organization_memberships_organization_id_organizations_id_fk", + "tableFrom": "organization_memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_memberships_user_id_user_id_fk": { + "name": "organization_memberships_user_id_user_id_fk", + "tableFrom": "organization_memberships", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "addresses": { + "name": "addresses", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'primary'" + }, + "line_1": { + "name": "line_1", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_2": { + "name": "line_2", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_3": { + "name": "line_3", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'United Kingdom'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "addresses_organization_id_idx": { + "name": "addresses_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "addresses_client_id_idx": { + "name": "addresses_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "addresses_organization_id_organizations_id_fk": { + "name": "addresses_organization_id_organizations_id_fk", + "tableFrom": "addresses", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "addresses_client_id_clients_id_fk": { + "name": "addresses_client_id_clients_id_fk", + "tableFrom": "addresses", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contacts": { + "name": "contacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "receives_invoices": { + "name": "receives_invoices", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "receives_contracts": { + "name": "receives_contracts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "contacts_organization_id_idx": { + "name": "contacts_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "contacts_client_id_idx": { + "name": "contacts_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "contacts_organization_id_organizations_id_fk": { + "name": "contacts_organization_id_organizations_id_fk", + "tableFrom": "contacts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "contacts_client_id_clients_id_fk": { + "name": "contacts_client_id_clients_id_fk", + "tableFrom": "contacts", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'meeting_room'" + }, + "sq_ft": { + "name": "sq_ft", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_price_per_hour_gbp": { + "name": "internal_price_per_hour_gbp", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_price_per_hour_gbp": { + "name": "external_price_per_hour_gbp", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_price_per_day_gbp": { + "name": "internal_price_per_day_gbp", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_price_per_day_gbp": { + "name": "external_price_per_day_gbp", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_attendees": { + "name": "max_attendees", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workstations": { + "name": "workstations", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_per_month_gbp": { + "name": "price_per_month_gbp", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_organization_id_organizations_id_fk": { + "name": "rooms_organization_id_organizations_id_fk", + "tableFrom": "rooms", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "services": { + "name": "services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'each'" + }, + "price_gbp": { + "name": "price_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "services_organization_id_organizations_id_fk": { + "name": "services_organization_id_organizations_id_fk", + "tableFrom": "services", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "bookings": { + "name": "bookings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ends_at": { + "name": "ends_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'booked'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookings_organization_id_idx": { + "name": "bookings_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "bookings_client_id_idx": { + "name": "bookings_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "bookings_room_id_idx": { + "name": "bookings_room_id_idx", + "columns": [ + "room_id" + ], + "isUnique": false + }, + "bookings_service_id_idx": { + "name": "bookings_service_id_idx", + "columns": [ + "service_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookings_organization_id_organizations_id_fk": { + "name": "bookings_organization_id_organizations_id_fk", + "tableFrom": "bookings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "bookings_client_id_clients_id_fk": { + "name": "bookings_client_id_clients_id_fk", + "tableFrom": "bookings", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "bookings_room_id_rooms_id_fk": { + "name": "bookings_room_id_rooms_id_fk", + "tableFrom": "bookings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "bookings_service_id_services_id_fk": { + "name": "bookings_service_id_services_id_fk", + "tableFrom": "bookings", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoice_lines": { + "name": "invoice_lines", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period_start": { + "name": "period_start", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period_end": { + "name": "period_end", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "unit_price_gbp": { + "name": "unit_price_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_gbp": { + "name": "total_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "invoice_lines_organization_id_idx": { + "name": "invoice_lines_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invoice_lines_invoice_id_idx": { + "name": "invoice_lines_invoice_id_idx", + "columns": [ + "invoice_id" + ], + "isUnique": false + }, + "invoice_lines_client_id_idx": { + "name": "invoice_lines_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "invoice_lines_source_idx": { + "name": "invoice_lines_source_idx", + "columns": [ + "source_type", + "source_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invoice_lines_organization_id_organizations_id_fk": { + "name": "invoice_lines_organization_id_organizations_id_fk", + "tableFrom": "invoice_lines", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "invoice_lines_invoice_id_invoices_id_fk": { + "name": "invoice_lines_invoice_id_invoices_id_fk", + "tableFrom": "invoice_lines", + "tableTo": "invoices", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invoice_lines_client_id_clients_id_fk": { + "name": "invoice_lines_client_id_clients_id_fk", + "tableFrom": "invoice_lines", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoice_number": { + "name": "invoice_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_at": { + "name": "paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voided_at": { + "name": "voided_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subtotal_gbp": { + "name": "subtotal_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax_gbp": { + "name": "tax_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_gbp": { + "name": "total_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "invoices_invoice_number_unique": { + "name": "invoices_invoice_number_unique", + "columns": [ + "invoice_number" + ], + "isUnique": true + }, + "invoices_organization_id_idx": { + "name": "invoices_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invoices_client_id_idx": { + "name": "invoices_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invoices_organization_id_organizations_id_fk": { + "name": "invoices_organization_id_organizations_id_fk", + "tableFrom": "invoices", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "invoices_client_id_clients_id_fk": { + "name": "invoices_client_id_clients_id_fk", + "tableFrom": "invoices", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contract_rooms": { + "name": "contract_rooms", + "columns": { + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "contract_rooms_room_id_idx": { + "name": "contract_rooms_room_id_idx", + "columns": [ + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "contract_rooms_contract_id_contracts_id_fk": { + "name": "contract_rooms_contract_id_contracts_id_fk", + "tableFrom": "contract_rooms", + "tableTo": "contracts", + "columnsFrom": [ + "contract_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contract_rooms_room_id_rooms_id_fk": { + "name": "contract_rooms_room_id_rooms_id_fk", + "tableFrom": "contract_rooms", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contract_rooms_contract_id_room_id_pk": { + "columns": [ + "contract_id", + "room_id" + ], + "name": "contract_rooms_contract_id_room_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contracts": { + "name": "contracts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "license_fee_gbp": { + "name": "license_fee_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "deposit_gbp": { + "name": "deposit_gbp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "billed_to": { + "name": "billed_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "contracts_organization_id_idx": { + "name": "contracts_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "contracts_client_id_idx": { + "name": "contracts_client_id_idx", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "contracts_service_id_idx": { + "name": "contracts_service_id_idx", + "columns": [ + "service_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "contracts_organization_id_organizations_id_fk": { + "name": "contracts_organization_id_organizations_id_fk", + "tableFrom": "contracts", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "contracts_client_id_clients_id_fk": { + "name": "contracts_client_id_clients_id_fk", + "tableFrom": "contracts", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "contracts_service_id_services_id_fk": { + "name": "contracts_service_id_services_id_fk", + "tableFrom": "contracts", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d461497..384c004 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1782338642144, "tag": "0015_fuzzy_vampiro", "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1782383895120, + "tag": "0016_outstanding_talkback", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/schemas/invoices.schema.ts b/src/lib/schemas/invoices.schema.ts index ee0fdbd..728b190 100644 --- a/src/lib/schemas/invoices.schema.ts +++ b/src/lib/schemas/invoices.schema.ts @@ -1,9 +1,13 @@ import { z } from 'zod'; import { moneyGbp, optionalText, requiredText } from './shared.schema'; +export const invoiceStatusValues = ['draft', 'sent', 'paid', 'void'] as const; +export const invoiceStatusSchema = z.enum(invoiceStatusValues); + export const invoiceCreateSchema = z.object({ clientId: requiredText('Client'), invoiceNumber: requiredText('Invoice number', 80), + status: invoiceStatusSchema.default('draft'), issueDate: requiredText('Issue date', 40), dueDate: requiredText('Due date', 40), subtotalGbp: moneyGbp, diff --git a/src/lib/server/db/invoices.schema.ts b/src/lib/server/db/invoices.schema.ts index 263e982..5a3bee5 100644 --- a/src/lib/server/db/invoices.schema.ts +++ b/src/lib/server/db/invoices.schema.ts @@ -1,8 +1,10 @@ -import { index, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { clients } from './clients.schema'; import { organizations } from './organizations.schema'; import { timestamps } from './shared.schema'; +export const invoiceStatusValues = ['draft', 'sent', 'paid', 'void'] as const; + export const invoices = sqliteTable( 'invoices', { @@ -16,6 +18,10 @@ export const invoices = sqliteTable( .notNull() .references(() => clients.id, { onDelete: 'restrict' }), invoiceNumber: text('invoice_number').notNull().unique(), + status: text('status', { enum: invoiceStatusValues }).notNull().default('draft'), + sentAt: integer('sent_at', { mode: 'timestamp_ms' }), + paidAt: integer('paid_at', { mode: 'timestamp_ms' }), + voidedAt: integer('voided_at', { mode: 'timestamp_ms' }), issueDate: text('issue_date').notNull(), dueDate: text('due_date').notNull(), subtotalGbp: real('subtotal_gbp').notNull().default(0), From e1e2e94bf2ac8b51844f8aa226d55bd32b3e2c83 Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Thu, 25 Jun 2026 11:40:13 +0100 Subject: [PATCH 2/2] feat: manage invoice lifecycle --- .../dashboard/billing/invoices-table.svelte | 37 ++- .../[id]/invoices/invoices-table.svelte | 35 ++- .../dashboard/invoices/[id]/+page.server.ts | 129 ++++++++++ .../dashboard/invoices/[id]/+page.svelte | 238 ++++++++++++++++++ 4 files changed, 426 insertions(+), 13 deletions(-) create mode 100644 src/routes/dashboard/invoices/[id]/+page.server.ts create mode 100644 src/routes/dashboard/invoices/[id]/+page.svelte diff --git a/src/routes/dashboard/billing/invoices-table.svelte b/src/routes/dashboard/billing/invoices-table.svelte index 5cc5e84..e085bcc 100644 --- a/src/routes/dashboard/billing/invoices-table.svelte +++ b/src/routes/dashboard/billing/invoices-table.svelte @@ -1,6 +1,10 @@ {#if data.records.length > 0} @@ -23,6 +34,7 @@ Invoice # Client + Status Issued Due Total @@ -31,8 +43,19 @@ {#each data.records as record (record.id)} - {formatValue(record.invoiceNumber)} + {formatValue(record.invoiceNumber)} {relationLabel(record.clientId, 'clients')} + {formatValue(record.status)} {formatDate(record.issueDate)} {formatDate(record.dueDate)} {formatMoney(record.totalGbp)} diff --git a/src/routes/dashboard/clients/[id]/invoices/invoices-table.svelte b/src/routes/dashboard/clients/[id]/invoices/invoices-table.svelte index e1a83c2..7bdbaf6 100644 --- a/src/routes/dashboard/clients/[id]/invoices/invoices-table.svelte +++ b/src/routes/dashboard/clients/[id]/invoices/invoices-table.svelte @@ -1,17 +1,28 @@ {#if data.records.length > 0} @@ -20,6 +31,7 @@ Invoice # + Status Issued Due Total @@ -28,7 +40,18 @@ {#each data.records as record (record.id)} - {formatValue(record.invoiceNumber)} + {formatValue(record.invoiceNumber)} + {formatValue(record.status)} {formatDate(record.issueDate)} {formatDate(record.dueDate)} {formatMoney(record.totalGbp)} diff --git a/src/routes/dashboard/invoices/[id]/+page.server.ts b/src/routes/dashboard/invoices/[id]/+page.server.ts new file mode 100644 index 0000000..29d09c4 --- /dev/null +++ b/src/routes/dashboard/invoices/[id]/+page.server.ts @@ -0,0 +1,129 @@ +import { error, fail } from '@sveltejs/kit'; +import { and, asc, eq, isNull } from 'drizzle-orm'; +import { db } from '$lib/server/db'; +import { clients, invoiceLines, invoices } from '$lib/server/db/schema'; +import { loadOrganizationContext } from '$lib/server/organizations'; +import type { Actions, PageServerLoad } from './$types'; + +type InvoiceStatus = 'draft' | 'sent' | 'paid' | 'void'; +type Transition = 'send' | 'markPaid' | 'void'; + +const allowedTransitions: Record = { + send: ['draft'], + markPaid: ['sent'], + void: ['draft', 'sent'] +}; + +const transitionStatus: Record = { + send: 'sent', + markPaid: 'paid', + void: 'void' +}; + +const transitionMessages: Record = { + send: 'Only draft invoices can be marked as sent.', + markPaid: 'Only sent invoices can be marked as paid.', + void: 'Only draft or sent invoices can be voided.' +}; + +async function loadInvoice(organizationId: string, invoiceId: string) { + const [invoice] = await db + .select({ + id: invoices.id, + organizationId: invoices.organizationId, + clientId: invoices.clientId, + clientName: clients.name, + invoiceNumber: invoices.invoiceNumber, + status: invoices.status, + sentAt: invoices.sentAt, + paidAt: invoices.paidAt, + voidedAt: invoices.voidedAt, + issueDate: invoices.issueDate, + dueDate: invoices.dueDate, + subtotalGbp: invoices.subtotalGbp, + taxGbp: invoices.taxGbp, + totalGbp: invoices.totalGbp, + notes: invoices.notes, + createdAt: invoices.createdAt, + updatedAt: invoices.updatedAt + }) + .from(invoices) + .innerJoin(clients, eq(invoices.clientId, clients.id)) + .where( + and( + eq(invoices.id, invoiceId), + eq(invoices.organizationId, organizationId), + isNull(invoices.archivedAt) + ) + ) + .limit(1); + + if (!invoice) error(404, 'Invoice not found.'); + return invoice; +} + +async function transitionInvoice( + organizationId: string, + invoiceId: string, + transition: Transition +) { + const invoice = await loadInvoice(organizationId, invoiceId); + + if (!allowedTransitions[transition].includes(invoice.status)) { + return fail(400, { message: transitionMessages[transition] }); + } + + const now = new Date(); + const nextStatus = transitionStatus[transition]; + + await db + .update(invoices) + .set({ + status: nextStatus, + sentAt: transition === 'send' ? now : invoice.sentAt, + paidAt: transition === 'markPaid' ? now : invoice.paidAt, + voidedAt: transition === 'void' ? now : invoice.voidedAt, + updatedAt: now + }) + .where(and(eq(invoices.id, invoiceId), eq(invoices.organizationId, organizationId))); + + return { message: `Invoice ${invoice.invoiceNumber} marked as ${nextStatus}.` }; +} + +export const load: PageServerLoad = async ({ locals, params, parent }) => { + const { activeOrganizationId } = await loadOrganizationContext(locals); + const layoutData = await parent(); + const invoice = await loadInvoice(activeOrganizationId, params.id); + const lines = await db + .select() + .from(invoiceLines) + .where( + and( + eq(invoiceLines.invoiceId, invoice.id), + eq(invoiceLines.organizationId, activeOrganizationId), + isNull(invoiceLines.archivedAt) + ) + ) + .orderBy(asc(invoiceLines.periodStart), asc(invoiceLines.description)); + + return { + invoice, + lines, + organization: layoutData.activeOrganization + }; +}; + +export const actions: Actions = { + send: async ({ locals, params }) => { + const { activeOrganizationId } = await loadOrganizationContext(locals); + return transitionInvoice(activeOrganizationId, params.id, 'send'); + }, + markPaid: async ({ locals, params }) => { + const { activeOrganizationId } = await loadOrganizationContext(locals); + return transitionInvoice(activeOrganizationId, params.id, 'markPaid'); + }, + void: async ({ locals, params }) => { + const { activeOrganizationId } = await loadOrganizationContext(locals); + return transitionInvoice(activeOrganizationId, params.id, 'void'); + } +}; diff --git a/src/routes/dashboard/invoices/[id]/+page.svelte b/src/routes/dashboard/invoices/[id]/+page.svelte new file mode 100644 index 0000000..b285117 --- /dev/null +++ b/src/routes/dashboard/invoices/[id]/+page.svelte @@ -0,0 +1,238 @@ + + + + {data.invoice.invoiceNumber} | Clearity + + +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+
+ + {#if form?.message} +
+ {form.message} +
+ {/if} + +
+
+
+
+

Invoice

+

{data.invoice.invoiceNumber}

+
+ + {formatStatus(data.invoice.status)} + +
+ +
+
+

Issued

+

{formatDate(data.invoice.issueDate)}

+
+
+

Due

+

{formatDate(data.invoice.dueDate)}

+
+
+

Updated

+

{formatDate(data.invoice.updatedAt, true)}

+
+
+ + {#if lifecycleItems.length > 0} +
+ {#each lifecycleItems as item (item.label)} +
+

{item.label}

+

{formatDate(item.value, true)}

+
+ {/each} +
+ {/if} +
+ +
+
+ Subtotal + {formatMoney(data.invoice.subtotalGbp)} +
+
+ Tax + {formatMoney(data.invoice.taxGbp)} +
+
+ Total + {formatMoney(data.invoice.totalGbp)} +
+
+
+ +
+ + +
+

Payment

+
+

{data.organization.name}

+

{formatValue(data.organization.bankName)}

+

{formatValue(data.organization.bankAccountName)}

+

+ {formatValue(data.organization.bankSortCode)} / {formatValue( + data.organization.bankAccountNumber + )} +

+ {#if data.organization.bankIban} +

IBAN {data.organization.bankIban}

+ {/if} + {#if data.organization.bankSwift} +

SWIFT {data.organization.bankSwift}

+ {/if} +
+
+
+ +
+ + + + Description + Period + Qty + Unit + Total + + + + {#each data.lines as line (line.id)} + + {line.description} + {formatDate(line.periodStart)} - {formatDate(line.periodEnd)} + {line.quantity} + {formatMoney(line.unitPriceGbp)} + {formatMoney(line.totalGbp)} + + {/each} + + +
+ + {#if data.invoice.notes} +
+

Notes

+

{data.invoice.notes}

+
+ {/if} +
+ +