From 44bfb083f9a139dfe1ea5acc7f0c54a3bd1dd722 Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Wed, 24 Jun 2026 13:53:52 +0100 Subject: [PATCH] feat: add billing and organization workflows --- AGENTS.md | 28 + drizzle/0007_numerous_invaders.sql | 9 + drizzle/0008_violet_metal_master.sql | 1 + drizzle/0009_safe_speed_demon.sql | 28 + drizzle/0010_dazzling_hairball.sql | 2 + drizzle/0011_billing_flow.sql | 26 + drizzle/meta/0007_snapshot.json | 1695 ++++++++++++++++ drizzle/meta/0008_snapshot.json | 1688 ++++++++++++++++ drizzle/meta/0009_snapshot.json | 1727 ++++++++++++++++ drizzle/meta/0010_snapshot.json | 1743 +++++++++++++++++ drizzle/meta/_journal.json | 37 +- package.json | 1 + pnpm-workspace.yaml | 6 + src/lib/components/app-sidebar.svelte | 8 +- src/lib/components/form-checkbox.svelte | 36 + src/lib/components/form-combobox.svelte | 96 + src/lib/components/form-select.svelte | 50 + src/lib/components/login-form.svelte | 98 +- src/lib/components/money-field.svelte | 56 +- .../organization-form-fields.svelte | 179 ++ .../components/organization-switcher.svelte | 214 +- src/lib/constants/industries.ts | 46 + src/lib/schemas/addresses.schema.ts | 8 +- src/lib/schemas/clients.schema.ts | 10 +- src/lib/schemas/contacts.schema.ts | 3 +- src/lib/schemas/contracts.schema.ts | 45 +- src/lib/server/db/addresses.schema.ts | 6 +- src/lib/server/db/clients.schema.ts | 3 +- src/lib/server/db/contacts.schema.ts | 3 +- src/lib/server/db/contracts.schema.ts | 15 +- src/lib/server/db/invoices.schema.ts | 33 + src/routes/dashboard/+error.svelte | 42 + src/routes/dashboard/+layout.svelte | 3 + .../dashboard/addresses/+page.server.ts | 7 +- .../addresses/addresses-table.svelte | 6 +- .../admins/create-admin-dialog.svelte | 91 - .../dashboard/admins/edit-admin-dialog.svelte | 88 - src/routes/dashboard/billing/+page.server.ts | 526 +++++ src/routes/dashboard/billing/+page.svelte | 128 ++ .../archive-invoice-dialog.svelte | 0 .../invoices-table.svelte | 0 .../billing/run-billing-dialog.svelte | 359 ++++ src/routes/dashboard/bookings/+page.server.ts | 5 +- src/routes/dashboard/clients/+page.server.ts | 66 +- src/routes/dashboard/clients/+page.svelte | 4 +- .../dashboard/clients/[id]/+layout.server.ts | 16 +- .../dashboard/clients/[id]/+layout.svelte | 155 +- .../clients/[id]/addresses/+page.server.ts | 29 +- .../clients/[id]/addresses/+page.svelte | 64 +- .../[id]/addresses/addresses-table.svelte | 10 +- .../addresses/create-address-dialog.svelte | 178 +- .../[id]/addresses/edit-address-dialog.svelte | 178 +- .../clients/[id]/bookings/+page.server.ts | 13 +- .../clients/[id]/bookings/+page.svelte | 75 +- .../[id]/bookings/bookings-table.svelte | 2 - .../bookings/create-booking-dialog.svelte | 168 +- .../[id]/bookings/edit-booking-dialog.svelte | 168 +- .../clients/[id]/contacts/+page.server.ts | 22 +- .../clients/[id]/contacts/+page.svelte | 64 +- .../[id]/contacts/contacts-table.svelte | 8 +- .../contacts/create-contact-dialog.svelte | 145 +- .../[id]/contacts/edit-contact-dialog.svelte | 145 +- .../clients/[id]/contracts/+page.server.ts | 227 ++- .../clients/[id]/contracts/+page.svelte | 101 +- .../contracts/contract-form-fields.svelte | 139 ++ .../[id]/contracts/contracts-table.svelte | 14 +- .../contracts/create-contract-dialog.svelte | 67 +- .../contracts/edit-contract-dialog.svelte | 110 +- .../clients/[id]/invoices/+page.server.ts | 46 +- .../clients/[id]/invoices/+page.svelte | 92 +- .../invoices/create-invoice-dialog.svelte | 118 -- .../[id]/invoices/edit-invoice-dialog.svelte | 124 +- .../[id]/invoices/invoices-table.svelte | 4 - .../dashboard/clients/clients-table.svelte | 17 +- .../clients/create-client-dialog.svelte | 396 ++-- .../clients/edit-client-dialog.svelte | 120 +- src/routes/dashboard/contacts/+page.server.ts | 5 +- .../dashboard/contacts/contacts-table.svelte | 4 + .../dashboard/contracts/+page.server.ts | 23 +- src/routes/dashboard/contracts/+page.svelte | 74 +- .../contracts/contracts-table.svelte | 10 +- src/routes/dashboard/invoices/+page.server.ts | 51 - src/routes/dashboard/invoices/+page.svelte | 89 - src/routes/dashboard/invoices/+server.ts | 6 + .../dashboard/organization/+layout.svelte | 71 + .../dashboard/organization/+page.server.ts | 6 + .../{ => organization}/admins/+page.server.ts | 12 +- .../{ => organization}/admins/+page.svelte | 70 +- .../admins/admins-table.svelte | 0 .../admins/create-admin-dialog.svelte | 105 + .../admins/delete-admin-dialog.svelte | 0 .../admins/edit-admin-dialog.svelte | 102 + .../organization/details/+page.svelte | 57 + .../organization-edit-dialog.svelte | 63 + src/routes/dashboard/rooms/+page.server.ts | 6 +- src/routes/dashboard/services/+page.server.ts | 12 +- src/routes/login/+page.server.ts | 4 +- 97 files changed, 10966 insertions(+), 2044 deletions(-) create mode 100644 AGENTS.md create mode 100644 drizzle/0007_numerous_invaders.sql create mode 100644 drizzle/0008_violet_metal_master.sql create mode 100644 drizzle/0009_safe_speed_demon.sql create mode 100644 drizzle/0010_dazzling_hairball.sql create mode 100644 drizzle/0011_billing_flow.sql create mode 100644 drizzle/meta/0007_snapshot.json create mode 100644 drizzle/meta/0008_snapshot.json create mode 100644 drizzle/meta/0009_snapshot.json create mode 100644 drizzle/meta/0010_snapshot.json create mode 100644 src/lib/components/form-checkbox.svelte create mode 100644 src/lib/components/form-combobox.svelte create mode 100644 src/lib/components/form-select.svelte create mode 100644 src/lib/components/organization-form-fields.svelte create mode 100644 src/lib/constants/industries.ts create mode 100644 src/routes/dashboard/+error.svelte delete mode 100644 src/routes/dashboard/admins/create-admin-dialog.svelte delete mode 100644 src/routes/dashboard/admins/edit-admin-dialog.svelte create mode 100644 src/routes/dashboard/billing/+page.server.ts create mode 100644 src/routes/dashboard/billing/+page.svelte rename src/routes/dashboard/{invoices => billing}/archive-invoice-dialog.svelte (100%) rename src/routes/dashboard/{invoices => billing}/invoices-table.svelte (100%) create mode 100644 src/routes/dashboard/billing/run-billing-dialog.svelte create mode 100644 src/routes/dashboard/clients/[id]/contracts/contract-form-fields.svelte delete mode 100644 src/routes/dashboard/clients/[id]/invoices/create-invoice-dialog.svelte delete mode 100644 src/routes/dashboard/invoices/+page.server.ts delete mode 100644 src/routes/dashboard/invoices/+page.svelte create mode 100644 src/routes/dashboard/invoices/+server.ts create mode 100644 src/routes/dashboard/organization/+layout.svelte create mode 100644 src/routes/dashboard/organization/+page.server.ts rename src/routes/dashboard/{ => organization}/admins/+page.server.ts (92%) rename src/routes/dashboard/{ => organization}/admins/+page.svelte (75%) rename src/routes/dashboard/{ => organization}/admins/admins-table.svelte (100%) create mode 100644 src/routes/dashboard/organization/admins/create-admin-dialog.svelte rename src/routes/dashboard/{ => organization}/admins/delete-admin-dialog.svelte (100%) create mode 100644 src/routes/dashboard/organization/admins/edit-admin-dialog.svelte create mode 100644 src/routes/dashboard/organization/details/+page.svelte create mode 100644 src/routes/dashboard/organization/organization-edit-dialog.svelte diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4fbb63b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# Clearity + +Clearity is a workspace management system offering a comprehensive multi-occupancy management solution for office, lab and workspace portfolio management, and billing in a single, centralised platform. + +This project is based inspired by Clarity Core by RA, for which more information can be found on their website here: https://ra-is.co.uk/clarity-core-workspace-management-system/. However, that application is based on an outdated architecture. + +## Tech Stack + + - Svelte + - SvelteKit + - Drizzle w/ libSQL + - [better-auth for authenticaton](https://better-auth.com/docs/introduction) + - [shadcn-svelte for UI components](https://www.shadcn-svelte.com/docs) + - [runed for helper functions](https://runed.dev/docs) (e.g. useUrlSearchParams) + +## Deployment + +Target for deployment is Cloudflare using wrangler.jsonc and the wrangler cli. + + - Cloudflare Workers + - Cloudflare D1 + - Cloudflare R2 + +## Semantics + +Break large +page.svelte files into several constituent-component-files.svelte using kebab case naming conventions and stored in the same directory as the +page.svelte file unless it's used across several pages. + +Keep abstractions as mimimal as possible to help ensure this project is human-readable. diff --git a/drizzle/0007_numerous_invaders.sql b/drizzle/0007_numerous_invaders.sql new file mode 100644 index 0000000..7d3170f --- /dev/null +++ b/drizzle/0007_numerous_invaders.sql @@ -0,0 +1,9 @@ +ALTER TABLE `clients` ADD `industry` text;--> statement-breakpoint +ALTER TABLE `clients` DROP COLUMN `type`;--> statement-breakpoint +ALTER TABLE `clients` DROP COLUMN `status`;--> statement-breakpoint +ALTER TABLE `addresses` ADD `type` text DEFAULT 'primary' NOT NULL;--> statement-breakpoint +ALTER TABLE `addresses` ADD `line_3` text;--> statement-breakpoint +ALTER TABLE `addresses` DROP COLUMN `label`;--> statement-breakpoint +ALTER TABLE `addresses` DROP COLUMN `is_primary`;--> statement-breakpoint +ALTER TABLE `contacts` ADD `receives_invoices` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `contacts` ADD `receives_contracts` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/0008_violet_metal_master.sql b/drizzle/0008_violet_metal_master.sql new file mode 100644 index 0000000..35b1600 --- /dev/null +++ b/drizzle/0008_violet_metal_master.sql @@ -0,0 +1 @@ +ALTER TABLE `contacts` DROP COLUMN `notes`; \ No newline at end of file diff --git a/drizzle/0009_safe_speed_demon.sql b/drizzle/0009_safe_speed_demon.sql new file mode 100644 index 0000000..376aa85 --- /dev/null +++ b/drizzle/0009_safe_speed_demon.sql @@ -0,0 +1,28 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_contracts` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `client_id` text NOT NULL, + `room_id` text, + `service_id` text, + `start_date` text NOT NULL, + `end_date` text NOT NULL, + `status` text DEFAULT 'draft' NOT NULL, + `notes` text, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `archived_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`service_id`) REFERENCES `services`(`id`) ON UPDATE no action ON DELETE restrict +); +--> statement-breakpoint +INSERT INTO `__new_contracts`("id", "organization_id", "client_id", "room_id", "service_id", "start_date", "end_date", "status", "notes", "created_at", "updated_at", "archived_at") SELECT "id", "organization_id", "client_id", NULL, NULL, "start_date", COALESCE(NULLIF("end_date", ''), "start_date"), CASE WHEN "status" = 'cancelled' THEN 'void' ELSE "status" END, "notes", "created_at", "updated_at", "archived_at" FROM `contracts`;--> statement-breakpoint +DROP TABLE `contracts`;--> statement-breakpoint +ALTER TABLE `__new_contracts` RENAME TO `contracts`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `contracts_organization_id_idx` ON `contracts` (`organization_id`);--> statement-breakpoint +CREATE INDEX `contracts_client_id_idx` ON `contracts` (`client_id`);--> statement-breakpoint +CREATE INDEX `contracts_room_id_idx` ON `contracts` (`room_id`);--> statement-breakpoint +CREATE INDEX `contracts_service_id_idx` ON `contracts` (`service_id`); diff --git a/drizzle/0010_dazzling_hairball.sql b/drizzle/0010_dazzling_hairball.sql new file mode 100644 index 0000000..2d6f0cf --- /dev/null +++ b/drizzle/0010_dazzling_hairball.sql @@ -0,0 +1,2 @@ +ALTER TABLE `contracts` ADD `license_fee_gbp` real DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `contracts` ADD `deposit_gbp` real DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/drizzle/0011_billing_flow.sql b/drizzle/0011_billing_flow.sql new file mode 100644 index 0000000..fb00fda --- /dev/null +++ b/drizzle/0011_billing_flow.sql @@ -0,0 +1,26 @@ +ALTER TABLE `contracts` ADD `billed_to` text;--> statement-breakpoint +CREATE TABLE `invoice_lines` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `invoice_id` text NOT NULL, + `client_id` text NOT NULL, + `source_type` text NOT NULL, + `source_id` text NOT NULL, + `period_start` text NOT NULL, + `period_end` text NOT NULL, + `description` text NOT NULL, + `quantity` real DEFAULT 1 NOT NULL, + `unit_price_gbp` real DEFAULT 0 NOT NULL, + `total_gbp` real DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL, + `archived_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`invoice_id`) REFERENCES `invoices`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict +); +--> statement-breakpoint +CREATE INDEX `invoice_lines_organization_id_idx` ON `invoice_lines` (`organization_id`);--> statement-breakpoint +CREATE INDEX `invoice_lines_invoice_id_idx` ON `invoice_lines` (`invoice_id`);--> statement-breakpoint +CREATE INDEX `invoice_lines_client_id_idx` ON `invoice_lines` (`client_id`);--> statement-breakpoint +CREATE INDEX `invoice_lines_source_idx` ON `invoice_lines` (`source_type`,`source_id`); diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..88b6e90 --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,1695 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4ae7ce5f-c9a9-4605-8dba-024ef9737bb2", + "prevId": "adeae551-67e9-483d-98d7-0bdbb7475f7f", + "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 + }, + "workspace": { + "name": "workspace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Default Workspace'" + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_line_2": { + "name": "address_line_2", + "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": {} + }, + "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 + }, + "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": { + "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": {} + }, + "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 + }, + "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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "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": {} + }, + "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 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "value_gbp": { + "name": "value_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": { + "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 + } + }, + "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" + } + }, + "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/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..36d4a39 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,1688 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b24a4afe-e783-4d6f-92ee-0519181a5639", + "prevId": "4ae7ce5f-c9a9-4605-8dba-024ef9737bb2", + "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 + }, + "workspace": { + "name": "workspace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Default Workspace'" + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_line_2": { + "name": "address_line_2", + "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": {} + }, + "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": {} + }, + "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 + }, + "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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "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": {} + }, + "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 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "value_gbp": { + "name": "value_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": { + "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 + } + }, + "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" + } + }, + "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/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..4416c54 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1727 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "779f7527-d3e3-4bc4-9c68-99ab6dda97ce", + "prevId": "b24a4afe-e783-4d6f-92ee-0519181a5639", + "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 + }, + "workspace": { + "name": "workspace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Default Workspace'" + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_line_2": { + "name": "address_line_2", + "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": {} + }, + "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": {} + }, + "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 + }, + "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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "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": {} + }, + "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 + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "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": { + "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_room_id_idx": { + "name": "contracts_room_id_idx", + "columns": [ + "room_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_room_id_rooms_id_fk": { + "name": "contracts_room_id_rooms_id_fk", + "tableFrom": "contracts", + "tableTo": "rooms", + "columnsFrom": [ + "room_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/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..40c594b --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1743 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2a8c8b90-bbf2-48a0-8185-630b9d3be582", + "prevId": "779f7527-d3e3-4bc4-9c68-99ab6dda97ce", + "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 + }, + "workspace": { + "name": "workspace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Default Workspace'" + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address_line_2": { + "name": "address_line_2", + "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": {} + }, + "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": {} + }, + "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 + }, + "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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "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": {} + }, + "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 + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "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 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "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": { + "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_room_id_idx": { + "name": "contracts_room_id_idx", + "columns": [ + "room_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_room_id_rooms_id_fk": { + "name": "contracts_room_id_rooms_id_fk", + "tableFrom": "contracts", + "tableTo": "rooms", + "columnsFrom": [ + "room_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 b752670..fee8759 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,41 @@ "when": 1780750080744, "tag": "0006_cloudy_catseye", "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1781787818482, + "tag": "0007_numerous_invaders", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1781795846034, + "tag": "0008_violet_metal_master", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1781814809169, + "tag": "0009_safe_speed_demon", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1781816209764, + "tag": "0010_dazzling_hairball", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1782291600000, + "tag": "0011_billing_flow", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 8c49099..abfa2ee 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "clearity", "private": true, "version": "0.0.1", + "packageManager": "pnpm@11.9.0", "type": "module", "scripts": { "dev": "vite dev", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fa745f7..d768b31 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,9 @@ +allowBuilds: + '@prisma/client': true + better-sqlite3: true + esbuild: true + sharp: true + workerd: true onlyBuiltDependencies: - '@tailwindcss/oxide' - workerd diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index 70b70b6..62d8374 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -9,10 +9,6 @@ title: 'Dashboard', url: '/dashboard' }, - { - title: 'Admins', - url: '/dashboard/admins' - }, { title: 'Clients', url: '/dashboard/clients' @@ -38,8 +34,8 @@ url: '/dashboard/services' }, { - title: 'Invoices', - url: '/dashboard/invoices' + title: 'Billing', + url: '/dashboard/billing' }, { title: 'Contracts', diff --git a/src/lib/components/form-checkbox.svelte b/src/lib/components/form-checkbox.svelte new file mode 100644 index 0000000..a0021b3 --- /dev/null +++ b/src/lib/components/form-checkbox.svelte @@ -0,0 +1,36 @@ + + + + diff --git a/src/lib/components/form-combobox.svelte b/src/lib/components/form-combobox.svelte new file mode 100644 index 0000000..4602bd9 --- /dev/null +++ b/src/lib/components/form-combobox.svelte @@ -0,0 +1,96 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + {emptyMessage} + + {#each options as option (option.value)} + { + value = option.value; + onValueChange?.(option.value); + closeAndFocusTrigger(); + }} + > + {option.label} + + {/each} + + + + + + + diff --git a/src/lib/components/form-select.svelte b/src/lib/components/form-select.svelte new file mode 100644 index 0000000..3833117 --- /dev/null +++ b/src/lib/components/form-select.svelte @@ -0,0 +1,50 @@ + + + { + value = nextValue === emptyValue ? '' : nextValue; + }} +> + + {selectedLabel} + + + {#each options as option (option.value)} + {option.label} + {/each} + + + + diff --git a/src/lib/components/login-form.svelte b/src/lib/components/login-form.svelte index b2ab9d2..a64fe4d 100644 --- a/src/lib/components/login-form.svelte +++ b/src/lib/components/login-form.svelte @@ -1,7 +1,7 @@
@@ -58,46 +67,49 @@ Clearity

Sign in to Clearity

- - Use the administrator account created for this workspace. - - - - {#snippet children({ props })} - Email - - {/snippet} - - - - - - {#snippet children({ props })} - Password - - {/snippet} - - - - New users are created manually by an administrator; public registration is disabled. - - - + + + + {#snippet children({ props })} + Email + + {/snippet} + + {#if $tainted?.email} + + {/if} + + + + + + {#snippet children({ props })} + Password + + {/snippet} + + {#if $tainted?.password} + + {/if} + + + + + - - Business and sport centre management for clients, bookings, rooms, services, and invoices. - diff --git a/src/lib/components/money-field.svelte b/src/lib/components/money-field.svelte index 9f9db77..d46476e 100644 --- a/src/lib/components/money-field.svelte +++ b/src/lib/components/money-field.svelte @@ -1,6 +1,7 @@ - - - {#snippet children({ props })} - {label} - - - £ - - - - {/snippet} - - - + + + + {#snippet children({ props })} + {label} + + + £ + + + + {/snippet} + + {#if description} + {description} + {/if} + + + diff --git a/src/lib/components/organization-form-fields.svelte b/src/lib/components/organization-form-fields.svelte new file mode 100644 index 0000000..c57226a --- /dev/null +++ b/src/lib/components/organization-form-fields.svelte @@ -0,0 +1,179 @@ + + +
+
+
+ + Name + + + + Workspace + + +
+ + + Address + + + + Address line 2 + + +
+ + City + + + + Region + + +
+
+ + Postcode + + + + Country + + +
+
+ +
+
+ + Bank details +
+
+ + Bank name + + + + Account name + + +
+
+ + Account number + + + + Sort code + + +
+
+ + IBAN + + + + SWIFT/BIC + + +
+
+
diff --git a/src/lib/components/organization-switcher.svelte b/src/lib/components/organization-switcher.svelte index 26e36a8..89b6c3b 100644 --- a/src/lib/components/organization-switcher.svelte +++ b/src/lib/components/organization-switcher.svelte @@ -2,15 +2,14 @@ import Building2Icon from '@lucide/svelte/icons/building-2'; import CheckIcon from '@lucide/svelte/icons/check'; import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down'; - import LandmarkIcon from '@lucide/svelte/icons/landmark'; import PlusIcon from '@lucide/svelte/icons/plus'; import SettingsIcon from '@lucide/svelte/icons/settings'; + import { resolve } from '$app/paths'; import { page } from '$app/state'; + import OrganizationFormFields from './organization-form-fields.svelte'; import { Button } from '$lib/components/ui/button/index.js'; import * as Dialog from '$lib/components/ui/dialog/index.js'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; - import * as Field from '$lib/components/ui/field/index.js'; - import { Input } from '$lib/components/ui/input/index.js'; import * as Sidebar from '$lib/components/ui/sidebar/index.js'; type Organization = { @@ -39,162 +38,11 @@ activeOrganization: Organization; } = $props(); - let settingsOpen = $state(false); let createOpen = $state(false); const redirectTo = $derived(page.url.pathname + page.url.search); + const organizationSettingsUrl = resolve('/dashboard/organization/details'); -{#snippet organizationFields(prefix: string, organization?: Organization)} -
-
- - Name - - - - Workspace - - -
- - - Address - - - - Address line 2 - - -
- - City - - - - Region - - -
-
- - Postcode - - - - Country - - -
-
-{/snippet} - -{#snippet bankFields(prefix: string, organization?: Organization)} -
-
- - Bank details -
-
- - Bank name - - - - Account name - - -
-
- - Account number - - - - Sort code - - -
-
- - IBAN - - - - SWIFT/BIC - - -
-
-{/snippet} - @@ -225,11 +73,12 @@ + - - - - Organization settings - Update details for the active organization and invoice payments. - - -
- - - - {@render organizationFields('organization-settings', activeOrganization)} - {@render bankFields('organization-settings', activeOrganization)} - - - - {#snippet child({ props })} - - {/snippet} - - - -
-
-
- @@ -334,8 +149,7 @@
- {@render organizationFields('organization-create')} - {@render bankFields('organization-create')} + @@ -348,3 +162,13 @@
+ + diff --git a/src/lib/constants/industries.ts b/src/lib/constants/industries.ts new file mode 100644 index 0000000..3d9808c --- /dev/null +++ b/src/lib/constants/industries.ts @@ -0,0 +1,46 @@ +export const industries = [ + 'agriculture', + 'mining-and-quarrying', + 'manufacturing', + 'utilities', + 'construction', + 'retail-and-wholesale', + 'transport-and-storage', + 'accommodation-and-food', + 'information-and-communication', + 'financial-services', + 'real-estate', + 'professional-and-technical', + 'administration-and-support', + 'public-administration-and-defence', + 'education', + 'health-and-social-care', + 'arts-entertainment-and-recreation', + 'other-services' +] as const; + +export const industryOptions = [ + { value: '', label: 'Not set' }, + { value: 'agriculture', label: 'Agriculture' }, + { value: 'mining-and-quarrying', label: 'Mining and quarrying' }, + { value: 'manufacturing', label: 'Manufacturing' }, + { value: 'utilities', label: 'Utilities' }, + { value: 'construction', label: 'Construction' }, + { value: 'retail-and-wholesale', label: 'Retail and wholesale' }, + { value: 'transport-and-storage', label: 'Transport and storage' }, + { value: 'accommodation-and-food', label: 'Accommodation and food' }, + { value: 'information-and-communication', label: 'Information and communication' }, + { value: 'financial-services', label: 'Financial services' }, + { value: 'real-estate', label: 'Real estate' }, + { value: 'professional-and-technical', label: 'Professional and technical' }, + { value: 'administration-and-support', label: 'Administration and support' }, + { value: 'public-administration-and-defence', label: 'Public administration and defence' }, + { value: 'education', label: 'Education' }, + { value: 'health-and-social-care', label: 'Health and social care' }, + { value: 'arts-entertainment-and-recreation', label: 'Arts, entertainment and recreation' }, + { value: 'other-services', label: 'Other services' } +] as const; + +export function industryLabel(value: string | null | undefined) { + return industryOptions.find((option) => option.value === value)?.label ?? 'Not set'; +} diff --git a/src/lib/schemas/addresses.schema.ts b/src/lib/schemas/addresses.schema.ts index 388f2a9..c91f743 100644 --- a/src/lib/schemas/addresses.schema.ts +++ b/src/lib/schemas/addresses.schema.ts @@ -1,16 +1,16 @@ import { z } from 'zod'; -import { booleanString, idSchema, optionalText, requiredText } from './shared.schema'; +import { idSchema, optionalText, requiredText } from './shared.schema'; export const addressCreateSchema = z.object({ clientId: requiredText('Client'), - label: requiredText('Label', 80), + type: z.enum(['primary', 'invoicing', 'contract']).default('primary'), line1: requiredText('Address line 1'), line2: optionalText(160), + line3: optionalText(160), city: requiredText('City', 100), region: optionalText(100), postcode: requiredText('Postcode', 24), - country: requiredText('Country', 80).default('United Kingdom'), - isPrimary: booleanString + country: requiredText('Country', 80).default('United Kingdom') }); export const addressEditSchema = addressCreateSchema.extend(idSchema.shape); diff --git a/src/lib/schemas/clients.schema.ts b/src/lib/schemas/clients.schema.ts index 2998926..f82c641 100644 --- a/src/lib/schemas/clients.schema.ts +++ b/src/lib/schemas/clients.schema.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; +import { industries } from '$lib/constants/industries'; import { idSchema, optionalEmail, optionalText, optionalUrl, requiredText } from './shared.schema'; export const clientCreateSchema = z.object({ name: requiredText('Client name'), - type: z.enum(['business', 'sport', 'individual']).default('business'), - status: z.enum(['active', 'prospect', 'paused']).default('active'), + industry: z.enum([...industries, '']).default(''), website: optionalUrl, notes: optionalText(1000) }); @@ -16,10 +16,12 @@ export const clientOnboardingCreateSchema = clientCreateSchema.extend({ primaryContactRole: optionalText(120), primaryContactEmail: optionalEmail, primaryContactPhone: optionalText(80), - primaryContactNotes: optionalText(1000), - primaryAddressLabel: requiredText('Address label', 80).default('Primary'), + primaryContactReceivesInvoices: z.enum(['true', 'false']).default('false'), + primaryContactReceivesContracts: z.enum(['true', 'false']).default('false'), + primaryAddressType: z.enum(['primary', 'invoicing', 'contract']).default('primary'), primaryAddressLine1: requiredText('Address line 1'), primaryAddressLine2: optionalText(160), + primaryAddressLine3: optionalText(160), primaryAddressCity: requiredText('City', 100), primaryAddressRegion: optionalText(100), primaryAddressPostcode: requiredText('Postcode', 24), diff --git a/src/lib/schemas/contacts.schema.ts b/src/lib/schemas/contacts.schema.ts index 7efaa92..015dab5 100644 --- a/src/lib/schemas/contacts.schema.ts +++ b/src/lib/schemas/contacts.schema.ts @@ -14,7 +14,8 @@ export const contactCreateSchema = z.object({ email: optionalEmail, phone: optionalText(80), isPrimary: booleanString, - notes: optionalText(1000) + receivesInvoices: booleanString, + receivesContracts: booleanString }); export const contactEditSchema = contactCreateSchema.extend(idSchema.shape); diff --git a/src/lib/schemas/contracts.schema.ts b/src/lib/schemas/contracts.schema.ts index 0fd5cc5..a3ff756 100644 --- a/src/lib/schemas/contracts.schema.ts +++ b/src/lib/schemas/contracts.schema.ts @@ -1,14 +1,39 @@ import { z } from 'zod'; import { idSchema, moneyGbp, optionalText, requiredText } from './shared.schema'; -export const contractCreateSchema = z.object({ - clientId: requiredText('Client'), - title: requiredText('Contract title'), - startDate: requiredText('Start date', 40), - endDate: optionalText(40), - status: z.enum(['draft', 'active', 'expired', 'cancelled']).default('draft'), - valueGbp: moneyGbp, - notes: optionalText(1000) -}); +export const contractStatuses = ['draft', 'active', 'expired', 'void'] as const; -export const contractEditSchema = contractCreateSchema.extend(idSchema.shape); +export const contractCreateSchema = z + .object({ + clientId: requiredText('Client'), + roomId: optionalText(80), + serviceId: optionalText(80), + licenseFeeGbp: moneyGbp, + depositGbp: moneyGbp, + startDate: requiredText('Start date', 40), + endDate: requiredText('End date', 40), + notes: optionalText(1000) + }) + .superRefine((data, ctx) => { + if (!data.roomId && !data.serviceId) { + ctx.addIssue({ + code: 'custom', + path: ['roomId'], + message: 'Select a room or service.' + }); + } + + if (data.startDate && data.endDate && data.endDate < data.startDate) { + ctx.addIssue({ + code: 'custom', + path: ['endDate'], + message: 'End date must be on or after the start date.' + }); + } + }); + +export const contractEditSchema = contractCreateSchema.safeExtend(idSchema.shape); + +export const contractTransitionSchema = idSchema.extend({ + targetStatus: z.enum(['active', 'expired', 'void']) +}); diff --git a/src/lib/server/db/addresses.schema.ts b/src/lib/server/db/addresses.schema.ts index a097a10..7f5ddba 100644 --- a/src/lib/server/db/addresses.schema.ts +++ b/src/lib/server/db/addresses.schema.ts @@ -1,4 +1,4 @@ -import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { clients } from './clients.schema'; import { organizations } from './organizations.schema'; import { timestamps } from './shared.schema'; @@ -15,14 +15,14 @@ export const addresses = sqliteTable( clientId: text('client_id') .notNull() .references(() => clients.id, { onDelete: 'restrict' }), - label: text('label').notNull(), + type: text('type').notNull().default('primary'), line1: text('line_1').notNull(), line2: text('line_2'), + line3: text('line_3'), city: text('city').notNull(), region: text('region'), postcode: text('postcode').notNull(), country: text('country').notNull().default('United Kingdom'), - isPrimary: integer('is_primary', { mode: 'boolean' }).notNull().default(false), ...timestamps }, (table) => [ diff --git a/src/lib/server/db/clients.schema.ts b/src/lib/server/db/clients.schema.ts index 1105ea6..92b4d25 100644 --- a/src/lib/server/db/clients.schema.ts +++ b/src/lib/server/db/clients.schema.ts @@ -10,8 +10,7 @@ export const clients = sqliteTable('clients', { .notNull() .references(() => organizations.id, { onDelete: 'restrict' }), name: text('name').notNull(), - type: text('type').notNull().default('business'), - status: text('status').notNull().default('active'), + industry: text('industry'), website: text('website'), notes: text('notes'), ...timestamps diff --git a/src/lib/server/db/contacts.schema.ts b/src/lib/server/db/contacts.schema.ts index ce18e50..db3cdcd 100644 --- a/src/lib/server/db/contacts.schema.ts +++ b/src/lib/server/db/contacts.schema.ts @@ -20,7 +20,8 @@ export const contacts = sqliteTable( email: text('email'), phone: text('phone'), isPrimary: integer('is_primary', { mode: 'boolean' }).notNull().default(false), - notes: text('notes'), + receivesInvoices: integer('receives_invoices', { mode: 'boolean' }).notNull().default(false), + receivesContracts: integer('receives_contracts', { mode: 'boolean' }).notNull().default(false), ...timestamps }, (table) => [ diff --git a/src/lib/server/db/contracts.schema.ts b/src/lib/server/db/contracts.schema.ts index 5708061..e3fd157 100644 --- a/src/lib/server/db/contracts.schema.ts +++ b/src/lib/server/db/contracts.schema.ts @@ -1,6 +1,8 @@ import { index, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { clients } from './clients.schema'; import { organizations } from './organizations.schema'; +import { rooms } from './rooms.schema'; +import { services } from './services.schema'; import { timestamps } from './shared.schema'; export const contracts = sqliteTable( @@ -15,16 +17,21 @@ export const contracts = sqliteTable( clientId: text('client_id') .notNull() .references(() => clients.id, { onDelete: 'restrict' }), - title: text('title').notNull(), + roomId: text('room_id').references(() => rooms.id, { onDelete: 'restrict' }), + serviceId: text('service_id').references(() => services.id, { onDelete: 'restrict' }), + licenseFeeGbp: real('license_fee_gbp').notNull().default(0), + depositGbp: real('deposit_gbp').notNull().default(0), startDate: text('start_date').notNull(), - endDate: text('end_date'), + endDate: text('end_date').notNull(), + billedTo: text('billed_to'), status: text('status').notNull().default('draft'), - valueGbp: real('value_gbp').notNull().default(0), notes: text('notes'), ...timestamps }, (table) => [ index('contracts_organization_id_idx').on(table.organizationId), - index('contracts_client_id_idx').on(table.clientId) + index('contracts_client_id_idx').on(table.clientId), + index('contracts_room_id_idx').on(table.roomId), + index('contracts_service_id_idx').on(table.serviceId) ] ); diff --git a/src/lib/server/db/invoices.schema.ts b/src/lib/server/db/invoices.schema.ts index d8ae2f4..040dc0d 100644 --- a/src/lib/server/db/invoices.schema.ts +++ b/src/lib/server/db/invoices.schema.ts @@ -30,3 +30,36 @@ export const invoices = sqliteTable( index('invoices_client_id_idx').on(table.clientId) ] ); + +export const invoiceLines = sqliteTable( + 'invoice_lines', + { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + organizationId: text('organization_id') + .notNull() + .references(() => organizations.id, { onDelete: 'restrict' }), + invoiceId: text('invoice_id') + .notNull() + .references(() => invoices.id, { onDelete: 'cascade' }), + clientId: text('client_id') + .notNull() + .references(() => clients.id, { onDelete: 'restrict' }), + sourceType: text('source_type').notNull(), + sourceId: text('source_id').notNull(), + periodStart: text('period_start').notNull(), + periodEnd: text('period_end').notNull(), + description: text('description').notNull(), + quantity: real('quantity').notNull().default(1), + unitPriceGbp: real('unit_price_gbp').notNull().default(0), + totalGbp: real('total_gbp').notNull().default(0), + ...timestamps + }, + (table) => [ + index('invoice_lines_organization_id_idx').on(table.organizationId), + index('invoice_lines_invoice_id_idx').on(table.invoiceId), + index('invoice_lines_client_id_idx').on(table.clientId), + index('invoice_lines_source_idx').on(table.sourceType, table.sourceId) + ] +); diff --git a/src/routes/dashboard/+error.svelte b/src/routes/dashboard/+error.svelte new file mode 100644 index 0000000..9f373f2 --- /dev/null +++ b/src/routes/dashboard/+error.svelte @@ -0,0 +1,42 @@ + + + + {status} | Clearity + + +
+
+
+

{status}

+

{title}

+

{description}

+
+ +
+ + +
+
+
diff --git a/src/routes/dashboard/+layout.svelte b/src/routes/dashboard/+layout.svelte index b10aa25..763dc2b 100644 --- a/src/routes/dashboard/+layout.svelte +++ b/src/routes/dashboard/+layout.svelte @@ -14,6 +14,8 @@ const segmentLabels: Record = { dashboard: 'Dashboard', + organization: 'Organization', + details: 'Details', admins: 'Admins', clients: 'Clients', addresses: 'Addresses', @@ -21,6 +23,7 @@ bookings: 'Bookings', rooms: 'Rooms', services: 'Services', + billing: 'Billing', invoices: 'Invoices', contracts: 'Contracts' }; diff --git a/src/routes/dashboard/addresses/+page.server.ts b/src/routes/dashboard/addresses/+page.server.ts index c255305..ac1bfb9 100644 --- a/src/routes/dashboard/addresses/+page.server.ts +++ b/src/routes/dashboard/addresses/+page.server.ts @@ -25,12 +25,15 @@ export const load: PageServerLoad = async ({ locals }) => { .select() .from(addresses) .where(and(eq(addresses.organizationId, activeOrganizationId), isNull(addresses.archivedAt))) - .orderBy(asc(addresses.label)); + .orderBy(asc(addresses.type)); return { records, options: await loadOptions(activeOrganizationId), - archiveForm: await superValidate(zod4(archiveSchema), { id: 'addresses-archive' }) + archiveForm: await superValidate(zod4(archiveSchema), { + id: 'addresses-archive', + errors: false + }) }; }; diff --git a/src/routes/dashboard/addresses/addresses-table.svelte b/src/routes/dashboard/addresses/addresses-table.svelte index 9874acb..2bfe623 100644 --- a/src/routes/dashboard/addresses/addresses-table.svelte +++ b/src/routes/dashboard/addresses/addresses-table.svelte @@ -20,11 +20,10 @@ Client - Label + Type Address City Postcode - Primary Actions @@ -32,11 +31,10 @@ {#each data.records as record (record.id)} {relationLabel(record.clientId, 'clients')} - {formatValue(record.label)} + {formatValue(record.type)} {formatValue(record.line1)} {formatValue(record.city)} {formatValue(record.postcode)} - {record.isPrimary ? 'Yes' : 'No'} - /* eslint-disable @typescript-eslint/no-explicit-any */ - import PlusIcon from '@lucide/svelte/icons/plus'; - import * as Form from '$lib/components/ui/form/index.js'; - import { Button } from '$lib/components/ui/button/index.js'; - import * as Dialog from '$lib/components/ui/dialog/index.js'; - import { Input } from '$lib/components/ui/input/index.js'; - import * as NativeSelect from '$lib/components/ui/native-select/index.js'; - let { - open = $bindable(false), - createForm, - createData, - enhanceCreate - }: { - open: boolean; - createForm: any; - createData: any; - enhanceCreate: any; - } = $props(); - - - - - {#snippet child({ props })} - - {/snippet} - - - - Create user - Add a manually managed Better Auth user. - -
- - - {#snippet children({ props })} - Name - - {/snippet} - - - - - - {#snippet children({ props })} - Email - - {/snippet} - - - - - - {#snippet children({ props })} - Password - - {/snippet} - - - - - - {#snippet children({ props })} - Role - - Admin - User - - {/snippet} - - - - - - {#snippet child({ props })} - - {/snippet} - - - -
-
-
diff --git a/src/routes/dashboard/admins/edit-admin-dialog.svelte b/src/routes/dashboard/admins/edit-admin-dialog.svelte deleted file mode 100644 index dcc9419..0000000 --- a/src/routes/dashboard/admins/edit-admin-dialog.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - - !open && onClose()}> - - - Edit user - Update account details or set a new password. - - {#if editingUser} -
- - - - {#snippet children({ props })} - Name - - {/snippet} - - - - - - {#snippet children({ props })} - Email - - {/snippet} - - - - - - {#snippet children({ props })} - New password - - {/snippet} - - - - - - {#snippet children({ props })} - Role - - Admin - User - - {/snippet} - - - - - - {#snippet child({ props })} - - {/snippet} - - - -
- {/if} -
-
diff --git a/src/routes/dashboard/billing/+page.server.ts b/src/routes/dashboard/billing/+page.server.ts new file mode 100644 index 0000000..f32965b --- /dev/null +++ b/src/routes/dashboard/billing/+page.server.ts @@ -0,0 +1,526 @@ +import { fail } from '@sveltejs/kit'; +import { and, asc, desc, eq, gte, isNull, like, lte, ne } from 'drizzle-orm'; +import { createSearchParamsSchema, validateSearchParams } from 'runed/kit'; +import { message, superValidate } from 'sveltekit-superforms/server'; +import { zod4 } from 'sveltekit-superforms/adapters'; +import { db } from '$lib/server/db'; +import { + bookings, + clients, + contracts, + invoiceLines, + invoices, + rooms, + services +} from '$lib/server/db/schema'; +import { loadOrganizationContext } from '$lib/server/organizations'; +import { archiveSchema } from '$lib/schemas/shared.schema'; +import type { Actions, PageServerLoad } from './$types'; + +type BillingLine = { + id: string; + clientId: string; + clientName: string; + sourceType: 'booking-room' | 'booking-service' | 'contract'; + sourceId: string; + periodStart: string; + periodEnd: string; + description: string; + quantity: number; + unitPriceGbp: number; + totalGbp: number; +}; + +type BillingClient = { + clientId: string; + clientName: string; + totalGbp: number; + lineCount: number; + lines: BillingLine[]; +}; + +const dayMs = 24 * 60 * 60 * 1000; +const billingTermDays = 12; + +async function loadOptions(organizationId: string) { + const clientRows = await db + .select({ id: clients.id, name: clients.name }) + .from(clients) + .where(and(eq(clients.organizationId, organizationId), isNull(clients.archivedAt))) + .orderBy(asc(clients.name)); + + return { + clients: clientRows.map((client) => ({ label: client.name, value: client.id })) + }; +} + +function toDateOnly(date: Date) { + return date.toISOString().slice(0, 10); +} + +function parseDateOnly(value: string) { + const [year, month, day] = value.split('-').map(Number); + return new Date(Date.UTC(year, month - 1, day)); +} + +function addMonths(date: Date, months: number) { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1)); +} + +function endOfMonth(date: Date) { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)); +} + +function startOfMonth(date: Date) { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)); +} + +function minDate(a: Date, b: Date) { + return a.getTime() <= b.getTime() ? a : b; +} + +function maxDate(a: Date, b: Date) { + return a.getTime() >= b.getTime() ? a : b; +} + +function daysInclusive(start: Date, end: Date) { + return Math.floor((end.getTime() - start.getTime()) / dayMs) + 1; +} + +function roundMoney(value: number) { + return Math.round((value + Number.EPSILON) * 100) / 100; +} + +function roundQuantity(value: number) { + return Math.round((value + Number.EPSILON) * 100) / 100; +} + +function monthLabel(date: Date) { + return new Intl.DateTimeFormat('en-GB', { + month: 'short', + year: 'numeric', + timeZone: 'UTC' + }).format(date); +} + +function defaultInvoiceDate(today = new Date()) { + return toDateOnly(new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 2))); +} + +function billingSearchParamsSchema() { + return createSearchParamsSchema({ + billingDate: { + type: 'date', + default: parseDateOnly(defaultInvoiceDate()), + dateFormat: 'date' + } + }); +} + +function isDateOnly(value: string) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; + return toDateOnly(parseDateOnly(value)) === value; +} + +function normalizeInvoiceDate(value: FormDataEntryValue | string | null) { + if (typeof value !== 'string' || !isDateOnly(value)) return defaultInvoiceDate(); + return value; +} + +function invoiceDateFromUrl(url: URL) { + const { data } = validateSearchParams(url, billingSearchParamsSchema()); + return toDateOnly(data.billingDate); +} + +function billingCutoffs(invoiceDate: string) { + const selectedDate = parseDateOnly(invoiceDate); + const thisMonth = new Date( + Date.UTC(selectedDate.getUTCFullYear(), selectedDate.getUTCMonth(), 1) + ); + const contractsCutoff = endOfMonth(addMonths(thisMonth, 1)); + const servicesCutoff = endOfMonth(addMonths(thisMonth, -1)); + + return { + contractsCutoff: toDateOnly(contractsCutoff), + servicesCutoff: toDateOnly(servicesCutoff) + }; +} + +function billingDueDate(invoiceDate: string) { + const selectedDate = parseDateOnly(invoiceDate); + return toDateOnly(new Date(selectedDate.getTime() + billingTermDays * dayMs)); +} + +function sourceKey(line: { + sourceType: string; + sourceId: string; + periodStart: string; + periodEnd: string; +}) { + return `${line.sourceType}:${line.sourceId}:${line.periodStart}:${line.periodEnd}`; +} + +function lineId(line: Pick) { + return sourceKey(line); +} + +function groupBillingLines(lines: BillingLine[]) { + const clientsById = new Map(); + + for (const line of lines) { + const client = clientsById.get(line.clientId) ?? { + clientId: line.clientId, + clientName: line.clientName, + totalGbp: 0, + lineCount: 0, + lines: [] + }; + + client.lines.push(line); + client.lineCount += 1; + client.totalGbp = roundMoney(client.totalGbp + line.totalGbp); + clientsById.set(line.clientId, client); + } + + return [...clientsById.values()].sort((a, b) => a.clientName.localeCompare(b.clientName)); +} + +function bookingPrice(row: { + startsAt: string; + endsAt: string; + internalPricePerHourGbp: number | null; + externalPricePerHourGbp: number | null; + internalPricePerDayGbp: number | null; + externalPricePerDayGbp: number | null; +}) { + const startsAt = new Date(row.startsAt); + const endsAt = new Date(row.endsAt); + const durationHours = Math.max((endsAt.getTime() - startsAt.getTime()) / (60 * 60 * 1000), 0); + const dayRate = row.externalPricePerDayGbp ?? row.internalPricePerDayGbp ?? 0; + const hourRate = row.externalPricePerHourGbp ?? row.internalPricePerHourGbp ?? 0; + + if (dayRate > 0 && durationHours >= 7) { + const quantity = Math.max(1, Math.ceil(durationHours / 24)); + return { quantity, unitPriceGbp: dayRate, totalGbp: roundMoney(quantity * dayRate) }; + } + + const quantity = roundQuantity(durationHours); + return { quantity, unitPriceGbp: hourRate, totalGbp: roundMoney(quantity * hourRate) }; +} + +async function buildBillingPreview(organizationId: string, invoiceDate: string) { + const cutoffs = billingCutoffs(invoiceDate); + const existingLines = await db + .select({ + sourceType: invoiceLines.sourceType, + sourceId: invoiceLines.sourceId, + periodStart: invoiceLines.periodStart, + periodEnd: invoiceLines.periodEnd + }) + .from(invoiceLines) + .where(and(eq(invoiceLines.organizationId, organizationId), isNull(invoiceLines.archivedAt))); + + const billedSources = new Set(existingLines.map(sourceKey)); + const lines: BillingLine[] = []; + + const bookingRows = await db + .select({ + id: bookings.id, + clientId: bookings.clientId, + clientName: clients.name, + roomName: rooms.name, + roomType: rooms.type, + startsAt: bookings.startsAt, + endsAt: bookings.endsAt, + serviceId: bookings.serviceId, + serviceName: services.name, + servicePriceGbp: services.priceGbp, + internalPricePerHourGbp: rooms.internalPricePerHourGbp, + externalPricePerHourGbp: rooms.externalPricePerHourGbp, + internalPricePerDayGbp: rooms.internalPricePerDayGbp, + externalPricePerDayGbp: rooms.externalPricePerDayGbp + }) + .from(bookings) + .innerJoin(clients, eq(bookings.clientId, clients.id)) + .innerJoin(rooms, eq(bookings.roomId, rooms.id)) + .leftJoin(services, eq(bookings.serviceId, services.id)) + .where( + and( + eq(bookings.organizationId, organizationId), + lte(bookings.startsAt, `${cutoffs.servicesCutoff}T23:59`), + ne(bookings.status, 'cancelled'), + isNull(bookings.archivedAt) + ) + ) + .orderBy(asc(bookings.startsAt)); + + for (const booking of bookingRows) { + const periodStart = booking.startsAt.slice(0, 10); + const periodEnd = booking.endsAt.slice(0, 10); + + if (booking.roomType === 'meeting_room') { + const price = bookingPrice(booking); + const line = { + id: '', + clientId: booking.clientId, + clientName: booking.clientName, + sourceType: 'booking-room' as const, + sourceId: booking.id, + periodStart, + periodEnd, + description: `Meeting room: ${booking.roomName} (${periodStart})`, + ...price + }; + + line.id = lineId(line); + if (!billedSources.has(sourceKey(line)) && line.totalGbp > 0) lines.push(line); + } + + if (booking.serviceId && booking.serviceName) { + const line = { + id: '', + clientId: booking.clientId, + clientName: booking.clientName, + sourceType: 'booking-service' as const, + sourceId: booking.id, + periodStart, + periodEnd, + description: `Service: ${booking.serviceName} (${periodStart})`, + quantity: 1, + unitPriceGbp: booking.servicePriceGbp ?? 0, + totalGbp: roundMoney(booking.servicePriceGbp ?? 0) + }; + + line.id = lineId(line); + if (!billedSources.has(sourceKey(line)) && line.totalGbp > 0) lines.push(line); + } + } + + const contractRows = await db + .select({ + id: contracts.id, + clientId: contracts.clientId, + clientName: clients.name, + roomName: rooms.name, + serviceName: services.name, + licenseFeeGbp: contracts.licenseFeeGbp, + startDate: contracts.startDate, + endDate: contracts.endDate + }) + .from(contracts) + .innerJoin(clients, eq(contracts.clientId, clients.id)) + .leftJoin(rooms, eq(contracts.roomId, rooms.id)) + .leftJoin(services, eq(contracts.serviceId, services.id)) + .where( + and( + eq(contracts.organizationId, organizationId), + eq(contracts.status, 'active'), + lte(contracts.startDate, cutoffs.contractsCutoff), + gte(contracts.endDate, '1900-01-01'), + isNull(contracts.archivedAt) + ) + ) + .orderBy(asc(contracts.startDate)); + + const contractCutoff = parseDateOnly(cutoffs.contractsCutoff); + + for (const contract of contractRows) { + const contractStart = parseDateOnly(contract.startDate); + const contractEnd = minDate(parseDateOnly(contract.endDate), contractCutoff); + if (contractEnd.getTime() < contractStart.getTime() || contract.licenseFeeGbp <= 0) continue; + + for ( + let month = startOfMonth(contractStart); + month.getTime() <= contractEnd.getTime(); + month = addMonths(month, 1) + ) { + const periodStartDate = maxDate(month, contractStart); + const periodEndDate = minDate(endOfMonth(month), contractEnd); + if (periodEndDate.getTime() < periodStartDate.getTime()) continue; + + const periodStart = toDateOnly(periodStartDate); + const periodEnd = toDateOnly(periodEndDate); + const monthDays = daysInclusive(startOfMonth(month), endOfMonth(month)); + const billedDays = daysInclusive(periodStartDate, periodEndDate); + const quantity = roundQuantity(billedDays / monthDays); + const labelParts = [contract.roomName, contract.serviceName].filter(Boolean); + const line = { + id: '', + clientId: contract.clientId, + clientName: contract.clientName, + sourceType: 'contract' as const, + sourceId: contract.id, + periodStart, + periodEnd, + description: `Contract: ${labelParts.join(' + ') || 'monthly fee'} (${monthLabel(month)})`, + quantity, + unitPriceGbp: contract.licenseFeeGbp, + totalGbp: roundMoney(quantity * contract.licenseFeeGbp) + }; + + line.id = lineId(line); + if (!billedSources.has(sourceKey(line)) && line.totalGbp > 0) lines.push(line); + } + } + + lines.sort( + (a, b) => a.clientName.localeCompare(b.clientName) || a.periodStart.localeCompare(b.periodStart) + ); + + return { + ...cutoffs, + invoiceDate, + dueDate: billingDueDate(invoiceDate), + lines, + clients: groupBillingLines(lines), + totalGbp: roundMoney(lines.reduce((total, line) => total + line.totalGbp, 0)) + }; +} + +function invoiceNumberPrefix(invoiceDate: string) { + const date = parseDateOnly(invoiceDate); + return `INV-${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, '0')}`; +} + +async function latestInvoiceSequence(organizationId: string, prefix: string) { + const [latest] = await db + .select({ invoiceNumber: invoices.invoiceNumber }) + .from(invoices) + .where( + and(eq(invoices.organizationId, organizationId), like(invoices.invoiceNumber, `${prefix}-%`)) + ) + .orderBy(desc(invoices.invoiceNumber)) + .limit(1); + + return latest?.invoiceNumber ? Number(latest.invoiceNumber.split('-').at(-1) ?? 0) : 0; +} + +export const load: PageServerLoad = async ({ locals, url }) => { + const { activeOrganizationId } = await loadOrganizationContext(locals); + const invoiceDate = invoiceDateFromUrl(url); + const records = await db + .select() + .from(invoices) + .where(and(eq(invoices.organizationId, activeOrganizationId), isNull(invoices.archivedAt))) + .orderBy(asc(invoices.invoiceNumber)); + + return { + records, + options: await loadOptions(activeOrganizationId), + billingPreview: await buildBillingPreview(activeOrganizationId, invoiceDate), + archiveForm: await superValidate(zod4(archiveSchema), { + id: 'invoices-archive', + errors: false + }) + }; +}; + +export const actions: Actions = { + runBilling: async ({ locals, request }) => { + const { activeOrganizationId } = await loadOrganizationContext(locals); + const formData = await request.formData(); + const invoiceDate = normalizeInvoiceDate(formData.get('billingDate')); + let includedLineIds: unknown; + + try { + includedLineIds = JSON.parse(String(formData.get('includedLineIds') ?? '[]')); + } catch { + return fail(400, { message: 'Choose valid billing lines.' }); + } + + if (!Array.isArray(includedLineIds) || includedLineIds.some((id) => typeof id !== 'string')) { + return fail(400, { message: 'Choose valid billing lines.' }); + } + + const includedIds = new Set(includedLineIds); + const preview = await buildBillingPreview(activeOrganizationId, invoiceDate); + const includedLines = preview.lines.filter((line) => includedIds.has(line.id)); + + if (includedLines.length === 0) return fail(400, { message: 'There is nothing to bill.' }); + + const groupedClients = groupBillingLines(includedLines); + const issueDate = invoiceDate; + const dueDate = billingDueDate(invoiceDate); + const invoicePrefix = invoiceNumberPrefix(invoiceDate); + const latestSequence = await latestInvoiceSequence(activeOrganizationId, invoicePrefix); + + await db.transaction(async (tx) => { + let invoiceOffset = 0; + for (const client of groupedClients) { + const invoiceId = crypto.randomUUID(); + const invoiceNumber = `${invoicePrefix}-${String(latestSequence + invoiceOffset + 1).padStart(3, '0')}`; + invoiceOffset += 1; + + await tx.insert(invoices).values({ + id: invoiceId, + organizationId: activeOrganizationId, + clientId: client.clientId, + invoiceNumber, + issueDate, + dueDate, + status: 'draft', + subtotalGbp: client.totalGbp, + taxGbp: 0, + totalGbp: client.totalGbp, + notes: `Generated from billing run. Contract billing through ${preview.contractsCutoff}; services through ${preview.servicesCutoff}.`, + createdAt: new Date(), + updatedAt: new Date() + }); + + await tx.insert(invoiceLines).values( + client.lines.map((line) => ({ + id: crypto.randomUUID(), + organizationId: activeOrganizationId, + invoiceId, + clientId: client.clientId, + sourceType: line.sourceType, + sourceId: line.sourceId, + periodStart: line.periodStart, + periodEnd: line.periodEnd, + description: line.description, + quantity: line.quantity, + unitPriceGbp: line.unitPriceGbp, + totalGbp: line.totalGbp, + createdAt: new Date(), + updatedAt: new Date() + })) + ); + } + + const contractBilledTo = new Map(); + for (const line of includedLines) { + if (line.sourceType !== 'contract') continue; + const current = contractBilledTo.get(line.sourceId); + if (!current || line.periodEnd > current) + contractBilledTo.set(line.sourceId, line.periodEnd); + } + + for (const [contractId, billedTo] of contractBilledTo) { + await tx + .update(contracts) + .set({ billedTo, updatedAt: new Date() }) + .where( + and(eq(contracts.id, contractId), eq(contracts.organizationId, activeOrganizationId)) + ); + } + }); + + return { + message: `Billing run complete. Created ${groupedClients.length} invoice${groupedClients.length === 1 ? '' : 's'}.` + }; + }, + + archive: async (event) => { + const { activeOrganizationId } = await loadOrganizationContext(event.locals); + const form = await superValidate(event, zod4(archiveSchema), { id: 'invoices-archive' }); + + if (!form.valid) return message(form, 'Invoice id is required.', { status: 400 }); + + await db + .update(invoices) + .set({ archivedAt: new Date(), updatedAt: new Date() }) + .where(and(eq(invoices.id, form.data.id), eq(invoices.organizationId, activeOrganizationId))); + + return message(form, 'Invoice archived.'); + } +}; diff --git a/src/routes/dashboard/billing/+page.svelte b/src/routes/dashboard/billing/+page.svelte new file mode 100644 index 0000000..905fe14 --- /dev/null +++ b/src/routes/dashboard/billing/+page.svelte @@ -0,0 +1,128 @@ + + + + Billing | Clearity + + +
+
+
+

Billing

+

Run billing and manage generated invoices.

+
+ formatDate(value)} + /> +
+ + +
+ + (archivingId = null)} +/> diff --git a/src/routes/dashboard/invoices/archive-invoice-dialog.svelte b/src/routes/dashboard/billing/archive-invoice-dialog.svelte similarity index 100% rename from src/routes/dashboard/invoices/archive-invoice-dialog.svelte rename to src/routes/dashboard/billing/archive-invoice-dialog.svelte diff --git a/src/routes/dashboard/invoices/invoices-table.svelte b/src/routes/dashboard/billing/invoices-table.svelte similarity index 100% rename from src/routes/dashboard/invoices/invoices-table.svelte rename to src/routes/dashboard/billing/invoices-table.svelte diff --git a/src/routes/dashboard/billing/run-billing-dialog.svelte b/src/routes/dashboard/billing/run-billing-dialog.svelte new file mode 100644 index 0000000..6f92954 --- /dev/null +++ b/src/routes/dashboard/billing/run-billing-dialog.svelte @@ -0,0 +1,359 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + Run billing + + Contracts through {formatDate(preview.contractsCutoff)}. Bookings and services through {formatDate( + preview.servicesCutoff + )}. + + + +
+
+
+
Invoicing date
+ + + {#snippet child({ props })} + + {/snippet} + + + + + +
Due {formatDate(preview.dueDate)}
+
+
+
+ {includedClients.length} client{includedClients.length === 1 ? '' : 's'} · {includedLineIds.length} + line{includedLineIds.length === 1 ? '' : 's'} +
+
{formatMoney(includedTotal)}
+
+
+ +
+
+ {includedClients.length} client{includedClients.length === 1 ? '' : 's'} · {includedLineIds.length} + line{includedLineIds.length === 1 ? '' : 's'} +
+
{formatMoney(includedTotal)}
+
+ + {#if includedClients.length > 0} +
+ + + + Client + Lines + Total + Remove + + + + {#each includedClients as client (client.clientId)} + openClient(client.clientId)}> + {client.clientName} + {client.lineCount} + {formatMoney(client.totalGbp)} + + + + + {/each} + + +
+ {:else} +
+ No billable clients are currently selected. +
+ {/if} +
+ + + + {#snippet child({ props })} + + {/snippet} + +
+ + + +
+
+
+
+ + + + + {selectedClient?.clientName ?? 'Billing details'} + + {selectedClient?.lineCount ?? 0} line{selectedClient?.lineCount === 1 ? '' : 's'} · {formatMoney( + selectedClient?.totalGbp ?? 0 + )} + + + + {#if selectedClient} +
+ + + + Description + Period + Qty + Unit + Total + Remove + + + + {#each selectedClient.lines as line (line.id)} + + {line.description} + {formatDate(line.periodStart)} - {formatDate(line.periodEnd)} + {line.quantity} + {formatMoney(line.unitPriceGbp)} + {formatMoney(line.totalGbp)} + + + + + {/each} + + +
+ {/if} + + + + {#snippet child({ props })} + + {/snippet} + + +
+
diff --git a/src/routes/dashboard/bookings/+page.server.ts b/src/routes/dashboard/bookings/+page.server.ts index f00fd82..a7df93b 100644 --- a/src/routes/dashboard/bookings/+page.server.ts +++ b/src/routes/dashboard/bookings/+page.server.ts @@ -47,7 +47,10 @@ export const load: PageServerLoad = async ({ locals }) => { return { records, options: await loadOptions(activeOrganizationId), - archiveForm: await superValidate(zod4(archiveSchema), { id: 'bookings-archive' }) + archiveForm: await superValidate(zod4(archiveSchema), { + id: 'bookings-archive', + errors: false + }) }; }; diff --git a/src/routes/dashboard/clients/+page.server.ts b/src/routes/dashboard/clients/+page.server.ts index 931b37f..20d7f01 100644 --- a/src/routes/dashboard/clients/+page.server.ts +++ b/src/routes/dashboard/clients/+page.server.ts @@ -11,26 +11,57 @@ import type { Actions, PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals }) => { const { activeOrganizationId } = await loadOrganizationContext(locals); const records = await db - .select() + .select({ + client: clients, + primaryContactName: contacts.name, + primaryContactEmail: contacts.email, + primaryAddressLine1: addresses.line1 + }) .from(clients) + .leftJoin( + contacts, + and( + eq(contacts.clientId, clients.id), + eq(contacts.organizationId, activeOrganizationId), + eq(contacts.isPrimary, true), + isNull(contacts.archivedAt) + ) + ) + .leftJoin( + addresses, + and( + eq(addresses.clientId, clients.id), + eq(addresses.organizationId, activeOrganizationId), + eq(addresses.type, 'primary'), + isNull(addresses.archivedAt) + ) + ) .where(and(eq(clients.organizationId, activeOrganizationId), isNull(clients.archivedAt))) .orderBy(asc(clients.name)); return { records: records.map((record) => ({ - ...record, + ...record.client, + primaryContactName: record.primaryContactName, + primaryContactEmail: record.primaryContactEmail, + primaryAddressLine1: record.primaryAddressLine1, formValues: { - id: record.id, - name: record.name ?? '', - type: record.type ?? '', - status: record.status ?? '', - website: record.website ?? '', - notes: record.notes ?? '' + id: record.client.id, + name: record.client.name ?? '', + industry: record.client.industry ?? '', + website: record.client.website ?? '', + notes: record.client.notes ?? '' } })), - createForm: await superValidate(zod4(clientOnboardingCreateSchema), { id: 'clients-create' }), - editForm: await superValidate(zod4(clientEditSchema), { id: 'clients-edit' }), - archiveForm: await superValidate(zod4(archiveSchema), { id: 'clients-archive' }) + createForm: await superValidate(zod4(clientOnboardingCreateSchema), { + id: 'clients-create', + errors: false + }), + editForm: await superValidate(zod4(clientEditSchema), { id: 'clients-edit', errors: false }), + archiveForm: await superValidate(zod4(archiveSchema), { + id: 'clients-archive', + errors: false + }) }; }; @@ -52,8 +83,7 @@ export const actions: Actions = { id: clientId, organizationId: activeOrganizationId, name: form.data.name, - type: form.data.type, - status: form.data.status, + industry: form.data.industry || null, website: form.data.website || null, notes: form.data.notes || null, updatedAt: now, @@ -69,7 +99,8 @@ export const actions: Actions = { email: form.data.primaryContactEmail || null, phone: form.data.primaryContactPhone || null, isPrimary: true, - notes: form.data.primaryContactNotes || null, + receivesInvoices: form.data.primaryContactReceivesInvoices === 'true', + receivesContracts: form.data.primaryContactReceivesContracts === 'true', createdAt: now, updatedAt: now }); @@ -78,14 +109,14 @@ export const actions: Actions = { id: crypto.randomUUID(), organizationId: activeOrganizationId, clientId, - label: form.data.primaryAddressLabel, + type: form.data.primaryAddressType, line1: form.data.primaryAddressLine1, line2: form.data.primaryAddressLine2 || null, + line3: form.data.primaryAddressLine3 || null, city: form.data.primaryAddressCity, region: form.data.primaryAddressRegion || null, postcode: form.data.primaryAddressPostcode, country: form.data.primaryAddressCountry, - isPrimary: true, createdAt: now, updatedAt: now }); @@ -108,8 +139,7 @@ export const actions: Actions = { .update(clients) .set({ name: form.data.name, - type: form.data.type, - status: form.data.status, + industry: form.data.industry || null, website: form.data.website || null, notes: form.data.notes || null, updatedAt: new Date() diff --git a/src/routes/dashboard/clients/+page.svelte b/src/routes/dashboard/clients/+page.svelte index 6e07f6d..63d37ab 100644 --- a/src/routes/dashboard/clients/+page.svelte +++ b/src/routes/dashboard/clients/+page.svelte @@ -79,9 +79,7 @@

Clients

-

- Manage businesses, sports organisations, and individual clients. -

+

Manage client details, contacts, and addresses.

value === row.industry); + return { id: row.id, name: row.name ?? '', - type: clientTypes.find((type) => type === row.type), - status: clientStatuses.find((status) => status === row.status), + industry: industry ?? ('' as const), website: row.website ?? '', notes: row.notes ?? '' }; @@ -88,7 +87,7 @@ export const load: LayoutServerLoad = async ({ locals, params }) => { isNull(addresses.archivedAt) ) ) - .orderBy(asc(addresses.label)), + .orderBy(asc(addresses.type)), db .select() .from(contacts) @@ -121,7 +120,7 @@ export const load: LayoutServerLoad = async ({ locals, params }) => { isNull(contracts.archivedAt) ) ) - .orderBy(asc(contracts.title)), + .orderBy(asc(contracts.startDate)), db .select() .from(bookings) @@ -139,7 +138,8 @@ export const load: LayoutServerLoad = async ({ locals, params }) => { return { client, editForm: await superValidate(formValues(client), zod4(clientEditSchema), { - id: 'clients-edit' + id: 'clients-edit', + errors: false }), options, addresses: addressRows, diff --git a/src/routes/dashboard/clients/[id]/+layout.svelte b/src/routes/dashboard/clients/[id]/+layout.svelte index 828e427..1f37df8 100644 --- a/src/routes/dashboard/clients/[id]/+layout.svelte +++ b/src/routes/dashboard/clients/[id]/+layout.svelte @@ -2,16 +2,17 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { page } from '$app/state'; - import * as Form from '$lib/components/ui/form/index.js'; + import { Field as FormField, Control, FieldErrors } from 'formsnap'; + import FormSelect from '$lib/components/form-select.svelte'; + import { industryLabel, industryOptions } from '$lib/constants/industries'; + import * as Field from '$lib/components/ui/field/index.js'; import { toast } from 'svelte-sonner'; import { superForm } from 'sveltekit-superforms'; import { zod4Client } from 'sveltekit-superforms/adapters'; import { clientEditSchema } from '$lib/schemas/clients.schema'; - import { handleFormToast } from '$lib/form-feedback'; import { Button } from '$lib/components/ui/button/index.js'; import * as Dialog from '$lib/components/ui/dialog/index.js'; import { Input } from '$lib/components/ui/input/index.js'; - import * as NativeSelect from '$lib/components/ui/native-select/index.js'; import * as Tabs from '$lib/components/ui/tabs/index.js'; import { Textarea } from '$lib/components/ui/textarea/index.js'; import type { LayoutData } from './$types'; @@ -50,6 +51,28 @@ const { form: editData, enhance: enhanceEdit } = editForm; + function handleFormToast( + form: { valid: boolean; message?: string; errors: Record }, + id: string, + onSuccess: () => void + ) { + if (form.valid) { + if (form.message) toast.success(form.message, { id }); + onSuccess(); + return; + } + + toast.error(form.message ?? firstError(form.errors), { id }); + } + + function firstError(errors: Record) { + for (const value of Object.values(errors)) { + if (Array.isArray(value) && typeof value[0] === 'string') return value[0]; + } + + return 'Check the highlighted fields.'; + } + function navigateToTab(tab: TabValue) { goto(resolve(`/dashboard/clients/[id]/${tab}`, { id: data.client.id })); } @@ -81,66 +104,60 @@
- - - {#snippet children({ props })} - Name - - {/snippet} - - - - - - {#snippet children({ props })} - Type - - Not set - Business - Sport - Individual - - {/snippet} - - - - - - {#snippet children({ props })} - Status - - Not set - Active - Prospect - Paused - - {/snippet} - - - - - - {#snippet children({ props })} - Website - - {/snippet} - - - - - - {#snippet children({ props })} - Notes -