diff --git a/Dockerfile b/Dockerfile index e4c22fc..470273b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ WORKDIR /app RUN npm i -g pnpm COPY --from=builder /app/dist ./dist +COPY --from=builder /app/drizzle ./drizzle +COPY --from=builder /app/scripts ./scripts COPY package.json pnpm-lock.yaml ./ RUN pnpm install --prod @@ -28,4 +30,4 @@ ENV PORT=4321 ENV DATABASE_URL=/app/data/chronus.db EXPOSE 4321 -CMD ["node", "./dist/server/entry.mjs"] +CMD ["sh", "-c", "pnpm run migrate && node ./dist/server/entry.mjs"] diff --git a/drizzle/0000_mixed_morlocks.sql b/drizzle/0000_mixed_morlocks.sql new file mode 100644 index 0000000..9c6a1d9 --- /dev/null +++ b/drizzle/0000_mixed_morlocks.sql @@ -0,0 +1,140 @@ +CREATE TABLE `api_tokens` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `token` text NOT NULL, + `scopes` text DEFAULT '*' NOT NULL, + `last_used_at` integer, + `created_at` integer, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `api_tokens_token_unique` ON `api_tokens` (`token`);--> statement-breakpoint +CREATE TABLE `categories` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `name` text NOT NULL, + `color` text, + `created_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `clients` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `name` text NOT NULL, + `email` text, + `created_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `invoice_items` ( + `id` text PRIMARY KEY NOT NULL, + `invoice_id` text NOT NULL, + `description` text NOT NULL, + `quantity` real DEFAULT 1 NOT NULL, + `unit_price` integer DEFAULT 0 NOT NULL, + `amount` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`invoice_id`) REFERENCES `invoices`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `invoices` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `client_id` text NOT NULL, + `number` text NOT NULL, + `type` text DEFAULT 'invoice' NOT NULL, + `status` text DEFAULT 'draft' NOT NULL, + `issue_date` integer NOT NULL, + `due_date` integer NOT NULL, + `notes` text, + `currency` text DEFAULT 'USD' NOT NULL, + `subtotal` integer DEFAULT 0 NOT NULL, + `tax_rate` real DEFAULT 0, + `tax_amount` integer DEFAULT 0 NOT NULL, + `total` integer DEFAULT 0 NOT NULL, + `created_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `members` ( + `user_id` text NOT NULL, + `organization_id` text NOT NULL, + `role` text DEFAULT 'member' NOT NULL, + `joined_at` integer, + PRIMARY KEY(`user_id`, `organization_id`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `organizations` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `street` text, + `city` text, + `state` text, + `zip` text, + `country` text, + `created_at` integer +); +--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `site_settings` ( + `id` text PRIMARY KEY NOT NULL, + `key` text NOT NULL, + `value` text NOT NULL, + `updated_at` integer +); +--> statement-breakpoint +CREATE UNIQUE INDEX `site_settings_key_unique` ON `site_settings` (`key`);--> statement-breakpoint +CREATE TABLE `tags` ( + `id` text PRIMARY KEY NOT NULL, + `organization_id` text NOT NULL, + `name` text NOT NULL, + `color` text, + `created_at` integer, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `time_entries` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `organization_id` text NOT NULL, + `client_id` text NOT NULL, + `category_id` text NOT NULL, + `start_time` integer NOT NULL, + `end_time` integer, + `description` text, + `is_manual` integer DEFAULT false, + `created_at` integer, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `time_entry_tags` ( + `time_entry_id` text NOT NULL, + `tag_id` text NOT NULL, + PRIMARY KEY(`time_entry_id`, `tag_id`), + FOREIGN KEY (`time_entry_id`) REFERENCES `time_entries`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `email` text NOT NULL, + `password_hash` text NOT NULL, + `name` text NOT NULL, + `is_site_admin` integer DEFAULT false, + `created_at` integer +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..f67b7bb --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,980 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "cfa98c92-215e-4dbc-b8d4-23a655684d1b", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "api_tokens": { + "name": "api_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'*'" + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "api_tokens_token_unique": { + "name": "api_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "api_tokens_user_id_users_id_fk": { + "name": "api_tokens_user_id_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "categories": { + "name": "categories", + "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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "categories_organization_id_organizations_id_fk": { + "name": "categories_organization_id_organizations_id_fk", + "tableFrom": "categories", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "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 + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoice_items": { + "name": "invoice_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "unit_price": { + "name": "unit_price", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "invoice_items_invoice_id_invoices_id_fk": { + "name": "invoice_items_invoice_id_invoices_id_fk", + "tableFrom": "invoice_items", + "tableTo": "invoices", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "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 + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'invoice'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'USD'" + }, + "subtotal": { + "name": "subtotal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax_rate": { + "name": "tax_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "tax_amount": { + "name": "tax_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invoices_organization_id_organizations_id_fk": { + "name": "invoices_organization_id_organizations_id_fk", + "tableFrom": "invoices", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "members": { + "name": "members", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "members_user_id_organization_id_pk": { + "columns": [ + "user_id", + "organization_id" + ], + "name": "members_user_id_organization_id_pk" + } + }, + "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 + }, + "street": { + "name": "street", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "zip": { + "name": "zip", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "site_settings": { + "name": "site_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "site_settings_key_unique": { + "name": "site_settings_key_unique", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tags_organization_id_organizations_id_fk": { + "name": "tags_organization_id_organizations_id_fk", + "tableFrom": "tags", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "time_entries": { + "name": "time_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "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 + }, + "category_id": { + "name": "category_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_time": { + "name": "end_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_manual": { + "name": "is_manual", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "time_entries_user_id_users_id_fk": { + "name": "time_entries_user_id_users_id_fk", + "tableFrom": "time_entries", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_entries_organization_id_organizations_id_fk": { + "name": "time_entries_organization_id_organizations_id_fk", + "tableFrom": "time_entries", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_entries_client_id_clients_id_fk": { + "name": "time_entries_client_id_clients_id_fk", + "tableFrom": "time_entries", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_entries_category_id_categories_id_fk": { + "name": "time_entries_category_id_categories_id_fk", + "tableFrom": "time_entries", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "time_entry_tags": { + "name": "time_entry_tags", + "columns": { + "time_entry_id": { + "name": "time_entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "time_entry_tags_time_entry_id_time_entries_id_fk": { + "name": "time_entry_tags_time_entry_id_time_entries_id_fk", + "tableFrom": "time_entry_tags", + "tableTo": "time_entries", + "columnsFrom": [ + "time_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_entry_tags_tag_id_tags_id_fk": { + "name": "time_entry_tags_tag_id_tags_id_fk", + "tableFrom": "time_entry_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "time_entry_tags_time_entry_id_tag_id_pk": { + "columns": [ + "time_entry_id", + "tag_id" + ], + "name": "time_entry_tags_time_entry_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_site_admin": { + "name": "is_site_admin", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "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 new file mode 100644 index 0000000..802283f --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1768609277648, + "tag": "0000_mixed_morlocks", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index d73049c..686a23d 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "preview": "astro preview", "astro": "astro", "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "migrate": "node scripts/migrate.js" }, "dependencies": { "@astrojs/check": "^0.9.6", "@astrojs/node": "^9.5.2", "@astrojs/vue": "^5.1.4", + "@ceereals/vue-pdf": "^0.2.1", "@tailwindcss/vite": "^4.1.18", "astro": "^5.16.11", "astro-icon": "^1.1.5", @@ -31,6 +33,7 @@ "devDependencies": { "@catppuccin/daisyui": "^2.1.1", "@iconify-json/heroicons": "^1.2.3", + "@react-pdf/types": "^2.9.2", "@types/better-sqlite3": "^7.6.13", "drizzle-kit": "0.31.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3992c4..0c55e57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@astrojs/vue': specifier: ^5.1.4 version: 5.1.4(@types/node@25.0.9)(astro@5.16.11(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) + '@ceereals/vue-pdf': + specifier: ^0.2.1 + version: 0.2.1(vue@3.5.26(typescript@5.9.3)) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@6.4.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) @@ -63,6 +66,9 @@ importers: '@iconify-json/heroicons': specifier: ^1.2.3 version: 1.2.3 + '@react-pdf/types': + specifier: ^2.9.2 + version: 2.9.2 '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 @@ -259,6 +265,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -283,6 +293,11 @@ packages: '@catppuccin/palette@1.7.1': resolution: {integrity: sha512-aRc1tbzrevOTV7nFTT9SRdF26w/MIwT4Jwt4fDMc9itRZUDXCuEDBLyz4TQMlqO9ZP8mf5Hu4Jr6D03NLFc6Gw==} + '@ceereals/vue-pdf@0.2.1': + resolution: {integrity: sha512-E7Y2GyHTYEmZ2U5ZlVuJrOWdHhco49ZTdKVOo/wcOhlfNFG+W5pAZ6rOcaua+owspC4BgGzAxlmqj/jdEM9ehA==} + peerDependencies: + vue: ^3.4.38 + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -784,6 +799,39 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@react-pdf/fns@3.1.2': + resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==} + + '@react-pdf/font@4.0.4': + resolution: {integrity: sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==} + + '@react-pdf/image@3.0.4': + resolution: {integrity: sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==} + + '@react-pdf/layout@4.4.2': + resolution: {integrity: sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==} + + '@react-pdf/pdfkit@4.1.0': + resolution: {integrity: sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==} + + '@react-pdf/png-js@3.0.0': + resolution: {integrity: sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==} + + '@react-pdf/primitives@4.1.1': + resolution: {integrity: sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==} + + '@react-pdf/render@4.3.2': + resolution: {integrity: sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==} + + '@react-pdf/stylesheet@6.1.2': + resolution: {integrity: sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==} + + '@react-pdf/textkit@6.1.0': + resolution: {integrity: sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==} + + '@react-pdf/types@2.9.2': + resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} + '@rolldown/pluginutils@1.0.0-beta.60': resolution: {integrity: sha512-Jz4aqXRPVtqkH1E3jRDzLO5cgN5JwW+WG0wXGE4NiJd25nougv/AHzxmKCzmVQUYnxLmTM0M4wrZp+LlC2FKLg==} @@ -949,6 +997,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -1070,6 +1121,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1144,6 +1198,9 @@ packages: '@vue/compiler-ssr@3.5.26': resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + '@vue/devtools-api@8.0.5': + resolution: {integrity: sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==} + '@vue/devtools-core@7.7.9': resolution: {integrity: sha512-48jrBSwG4GVQRvVeeXn9p9+dlx+ISgasM7SxZZKczseohB0cBz+ITKr4YbLWjmJdy45UHL7UMPlR4Y0CWTRcSQ==} peerDependencies: @@ -1152,9 +1209,15 @@ packages: '@vue/devtools-kit@7.7.9': resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + '@vue/devtools-kit@8.0.5': + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} + '@vue/devtools-shared@7.7.9': resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + '@vue/devtools-shared@8.0.5': + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} + '@vue/reactivity@3.5.26': resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} @@ -1172,6 +1235,25 @@ packages: '@vue/shared@3.5.26': resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + abs-svg-path@0.1.1: + resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1239,6 +1321,10 @@ packages: base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1254,6 +1340,9 @@ packages: resolution: {integrity: sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1270,6 +1359,16 @@ packages: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1288,6 +1387,18 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} @@ -1298,6 +1409,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1337,6 +1452,10 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -1349,6 +1468,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1360,6 +1483,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1401,6 +1527,9 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -1463,6 +1592,10 @@ packages: resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} engines: {node: '>=18'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -1495,6 +1628,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + diff@8.0.3: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} @@ -1615,6 +1751,10 @@ packages: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1624,6 +1764,9 @@ packages: emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1659,9 +1802,21 @@ packages: error-stack-parser-es@0.1.5: resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -1745,6 +1900,13 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-yarn-workspace-root@2.0.0: + resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -1752,6 +1914,9 @@ packages: fontace@0.4.0: resolution: {integrity: sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg==} + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + fontkitten@1.0.2: resolution: {integrity: sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q==} engines: {node: '>=20'} @@ -1763,6 +1928,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -1772,6 +1941,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1784,6 +1956,14 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -1805,12 +1985,31 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} h3@1.15.5: resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -1844,6 +2043,12 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hsl-to-hex@1.0.0: + resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==} + + hsl-to-rgb-for-reals@1.1.1: + resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -1864,6 +2069,9 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + hyphen@1.14.1: + resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1883,6 +2091,14 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1897,6 +2113,10 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -1909,17 +2129,30 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jay-peg@1.1.1: + resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1939,6 +2172,10 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1953,6 +2190,12 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + + klaw-sync@6.0.0: + resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -2034,6 +2277,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + local-pkg@1.1.2: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} @@ -2060,6 +2306,10 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-definitions@6.0.0: resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} @@ -2108,6 +2358,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-engine@1.0.3: + resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -2192,6 +2445,10 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} @@ -2271,6 +2528,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-svg-path@1.1.0: + resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} + npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} @@ -2278,6 +2538,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -2301,6 +2565,10 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + p-limit@6.2.0: resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} engines: {node: '>=18'} @@ -2316,6 +2584,12 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} @@ -2323,6 +2597,9 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-svg-path@0.1.2: + resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -2332,6 +2609,11 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + patch-package@8.0.1: + resolution: {integrity: sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==} + engines: {node: '>=14', npm: '>5'} + hasBin: true + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2352,6 +2634,9 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + piccolore@0.1.3: resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} @@ -2372,6 +2657,9 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2407,6 +2695,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -2484,6 +2775,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -2534,6 +2828,10 @@ packages: server-destroy@1.0.1: resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -2562,6 +2860,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -2569,6 +2870,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + smol-toml@1.6.0: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} @@ -2629,6 +2934,13 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svg-arc-to-cubic-bezier@3.2.0: + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + svgo@3.3.2: resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} engines: {node: '>=14.0.0'} @@ -2653,8 +2965,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar@7.5.2: - resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + tar@7.5.3: + resolution: {integrity: sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==} engines: {node: '>=18'} tiny-inflate@1.0.3: @@ -2668,6 +2980,14 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -2729,6 +3049,12 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -2850,6 +3176,10 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-compatible-readable-stream@3.6.1: + resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} + engines: {node: '>= 6'} + vite-hot-client@2.1.0: resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} peerDependencies: @@ -3121,6 +3451,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -3437,6 +3770,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.28.6': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.28.6 @@ -3471,6 +3806,22 @@ snapshots: '@catppuccin/palette@1.7.1': {} + '@ceereals/vue-pdf@0.2.1(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/font': 4.0.4 + '@react-pdf/layout': 4.4.2 + '@react-pdf/pdfkit': 4.1.0 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/render': 4.3.2 + '@vue/devtools-api': 8.0.5 + '@vueuse/core': 13.9.0(vue@3.5.26(typescript@5.9.3)) + defu: 6.1.4 + patch-package: 8.0.1 + vue: 3.5.26(typescript@5.9.3) + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@drizzle-team/brocli@0.10.2': {} '@emmetio/abbreviation@2.3.3': @@ -3669,7 +4020,7 @@ snapshots: local-pkg: 1.1.2 pathe: 2.0.3 svgo: 3.3.2 - tar: 7.5.2 + tar: 7.5.3 transitivePeerDependencies: - supports-color @@ -3814,6 +4165,84 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@react-pdf/fns@3.1.2': {} + + '@react-pdf/font@4.0.4': + dependencies: + '@react-pdf/pdfkit': 4.1.0 + '@react-pdf/types': 2.9.2 + fontkit: 2.0.4 + is-url: 1.2.4 + + '@react-pdf/image@3.0.4': + dependencies: + '@react-pdf/png-js': 3.0.0 + jay-peg: 1.1.1 + + '@react-pdf/layout@4.4.2': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/image': 3.0.4 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/stylesheet': 6.1.2 + '@react-pdf/textkit': 6.1.0 + '@react-pdf/types': 2.9.2 + emoji-regex-xs: 1.0.0 + queue: 6.0.2 + yoga-layout: 3.2.1 + + '@react-pdf/pdfkit@4.1.0': + dependencies: + '@babel/runtime': 7.28.6 + '@react-pdf/png-js': 3.0.0 + browserify-zlib: 0.2.0 + crypto-js: 4.2.0 + fontkit: 2.0.4 + jay-peg: 1.1.1 + linebreak: 1.1.0 + vite-compatible-readable-stream: 3.6.1 + + '@react-pdf/png-js@3.0.0': + dependencies: + browserify-zlib: 0.2.0 + + '@react-pdf/primitives@4.1.1': {} + + '@react-pdf/render@4.3.2': + dependencies: + '@babel/runtime': 7.28.6 + '@react-pdf/fns': 3.1.2 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/textkit': 6.1.0 + '@react-pdf/types': 2.9.2 + abs-svg-path: 0.1.1 + color-string: 1.9.1 + normalize-svg-path: 1.1.0 + parse-svg-path: 0.1.2 + svg-arc-to-cubic-bezier: 3.2.0 + + '@react-pdf/stylesheet@6.1.2': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/types': 2.9.2 + color-string: 1.9.1 + hsl-to-hex: 1.0.0 + media-engine: 1.0.3 + postcss-value-parser: 4.2.0 + + '@react-pdf/textkit@6.1.0': + dependencies: + '@react-pdf/fns': 3.1.2 + bidi-js: 1.0.3 + hyphen: 1.14.1 + unicode-properties: 1.4.1 + + '@react-pdf/types@2.9.2': + dependencies: + '@react-pdf/font': 4.0.4 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/stylesheet': 6.1.2 + '@rolldown/pluginutils@1.0.0-beta.60': {} '@rollup/pluginutils@5.3.0(rollup@4.55.1)': @@ -3936,6 +4365,10 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@swc/helpers@0.5.18': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4036,6 +4469,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/web-bluetooth@0.0.21': {} + '@types/yauzl@2.10.3': dependencies: '@types/node': 25.0.9 @@ -4168,6 +4603,10 @@ snapshots: '@vue/compiler-dom': 3.5.26 '@vue/shared': 3.5.26 + '@vue/devtools-api@8.0.5': + dependencies: + '@vue/devtools-kit': 8.0.5 + '@vue/devtools-core@7.7.9(vite@6.4.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.9 @@ -4190,10 +4629,24 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.6 + '@vue/devtools-kit@8.0.5': + dependencies: + '@vue/devtools-shared': 8.0.5 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 2.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + '@vue/devtools-shared@7.7.9': dependencies: rfdc: 1.4.1 + '@vue/devtools-shared@8.0.5': + dependencies: + rfdc: 1.4.1 + '@vue/reactivity@3.5.26': dependencies: '@vue/shared': 3.5.26 @@ -4218,6 +4671,23 @@ snapshots: '@vue/shared@3.5.26': {} + '@vueuse/core@13.9.0(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.26(typescript@5.9.3)) + vue: 3.5.26(typescript@5.9.3) + + '@vueuse/metadata@13.9.0': {} + + '@vueuse/shared@13.9.0(vue@3.5.26(typescript@5.9.3))': + dependencies: + vue: 3.5.26(typescript@5.9.3) + + '@yarnpkg/lockfile@1.1.0': {} + + abs-svg-path@0.1.1: {} + acorn@8.15.0: {} ajv-draft-04@1.0.0(ajv@8.17.1): @@ -4372,6 +4842,8 @@ snapshots: base-64@1.0.0: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.15: {} @@ -4383,6 +4855,10 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -4408,6 +4884,18 @@ snapshots: widest-line: 5.0.0 wrap-ansi: 9.0.2 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.15 @@ -4429,12 +4917,34 @@ snapshots: dependencies: run-applescript: 7.1.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + camelcase@8.0.0: {} caniuse-lite@1.0.30001764: {} ccount@2.0.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -4482,6 +4992,8 @@ snapshots: chownr@3.0.0: {} + ci-info@3.9.0: {} + ci-info@4.3.1: {} cli-boxes@3.0.0: {} @@ -4492,6 +5004,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@2.1.2: {} + clsx@2.1.1: {} color-convert@2.0.1: @@ -4500,6 +5014,11 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + comma-separated-tokens@2.0.3: {} commander@11.1.0: {} @@ -4532,6 +5051,8 @@ snapshots: dependencies: uncrypto: 0.1.3 + crypto-js@4.2.0: {} + css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -4588,6 +5109,12 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@3.0.0: {} defu@6.1.4: {} @@ -4610,6 +5137,8 @@ snapshots: dependencies: dequal: 2.0.3 + dfa@1.2.0: {} + diff@8.0.3: {} dlv@1.1.3: {} @@ -4648,6 +5177,12 @@ snapshots: dset@3.1.4: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -4657,6 +5192,8 @@ snapshots: '@emmetio/abbreviation': 2.3.3 '@emmetio/css-abbreviation': 2.1.8 + emoji-regex-xs@1.0.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -4685,8 +5222,16 @@ snapshots: error-stack-parser-es@0.1.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild-register@3.6.0(esbuild@0.25.12): dependencies: debug: 4.4.3 @@ -4813,12 +5358,32 @@ snapshots: file-uri-to-path@1.0.0: {} + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-yarn-workspace-root@2.0.0: + dependencies: + micromatch: 4.0.8 + flattie@1.1.1: {} fontace@0.4.0: dependencies: fontkitten: 1.0.2 + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.18 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + fontkitten@1.0.2: dependencies: tiny-inflate: 1.0.3 @@ -4827,6 +5392,12 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -4836,12 +5407,32 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@5.2.0: dependencies: pump: 3.0.3 @@ -4861,6 +5452,8 @@ snapshots: globals@15.15.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} h3@1.15.5: @@ -4875,6 +5468,18 @@ snapshots: ufo: 1.6.3 uncrypto: 0.1.3 + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-from-html@2.0.3: dependencies: '@types/hast': 3.0.4 @@ -4964,6 +5569,12 @@ snapshots: hookable@5.5.3: {} + hsl-to-hex@1.0.0: + dependencies: + hsl-to-rgb-for-reals: 1.1.1 + + hsl-to-rgb-for-reals@1.1.1: {} + html-escaper@3.0.3: {} html-void-elements@3.0.0: {} @@ -4987,6 +5598,8 @@ snapshots: human-signals@8.0.1: {} + hyphen@1.14.1: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -5001,6 +5614,10 @@ snapshots: iron-webcrypto@1.2.1: {} + is-arrayish@0.3.4: {} + + is-docker@2.2.1: {} + is-docker@3.0.0: {} is-fullwidth-code-point@3.0.0: {} @@ -5009,20 +5626,34 @@ snapshots: dependencies: is-docker: 3.0.0 + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} is-stream@4.0.1: {} is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} + is-what@5.5.0: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 + isarray@2.0.5: {} + isexe@2.0.0: {} + jay-peg@1.1.1: + dependencies: + restructure: 3.0.2 + jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -5035,6 +5666,14 @@ snapshots: json-schema-traverse@1.0.0: {} + json-stable-stringify@1.3.0: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + json5@2.2.3: {} jsonc-parser@2.3.1: {} @@ -5047,6 +5686,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonify@0.0.1: {} + + klaw-sync@6.0.0: + dependencies: + graceful-fs: 4.2.11 + kleur@3.0.3: {} kleur@4.1.5: {} @@ -5102,6 +5747,11 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + local-pkg@1.1.2: dependencies: mlly: 1.8.0 @@ -5130,6 +5780,8 @@ snapshots: markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-definitions@6.0.0: dependencies: '@types/mdast': 4.0.4 @@ -5256,6 +5908,8 @@ snapshots: mdn-data@2.12.2: {} + media-engine@1.0.3: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -5447,6 +6101,11 @@ snapshots: transitivePeerDependencies: - supports-color + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.54.0: {} mime-types@3.0.2: @@ -5504,6 +6163,10 @@ snapshots: normalize-path@3.0.0: {} + normalize-svg-path@1.1.0: + dependencies: + svg-arc-to-cubic-bezier: 3.2.0 + npm-run-path@6.0.0: dependencies: path-key: 4.0.0 @@ -5513,6 +6176,8 @@ snapshots: dependencies: boolbase: 1.0.0 + object-keys@1.1.1: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -5544,6 +6209,11 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + p-limit@6.2.0: dependencies: yocto-queue: 1.2.2 @@ -5557,6 +6227,10 @@ snapshots: package-manager-detector@1.6.0: {} + pako@0.2.9: {} + + pako@1.0.11: {} + parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -5568,6 +6242,8 @@ snapshots: parse-ms@4.0.0: {} + parse-svg-path@0.1.2: {} + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -5581,6 +6257,23 @@ snapshots: dependencies: entities: 6.0.1 + patch-package@8.0.1: + dependencies: + '@yarnpkg/lockfile': 1.1.0 + chalk: 4.1.2 + ci-info: 3.9.0 + cross-spawn: 7.0.6 + find-yarn-workspace-root: 2.0.0 + fs-extra: 10.1.0 + json-stable-stringify: 1.3.0 + klaw-sync: 6.0.0 + minimist: 1.2.8 + open: 7.4.2 + semver: 7.7.3 + slash: 2.0.0 + tmp: 0.2.5 + yaml: 2.8.2 + path-browserify@1.0.1: {} path-key@3.1.1: {} @@ -5593,6 +6286,8 @@ snapshots: perfect-debounce@1.0.0: {} + perfect-debounce@2.0.0: {} + piccolore@0.1.3: {} picocolors@1.1.1: {} @@ -5613,6 +6308,8 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + postcss-value-parser@4.2.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5656,6 +6353,10 @@ snapshots: quansync@0.2.11: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + radix3@1.1.2: {} range-parser@1.2.1: {} @@ -5763,6 +6464,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + restructure@3.0.2: {} + retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -5851,6 +6554,15 @@ snapshots: server-destroy@1.0.1: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} sharp@0.34.5: @@ -5912,6 +6624,10 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -5920,6 +6636,8 @@ snapshots: sisteransi@1.0.5: {} + slash@2.0.0: {} + smol-toml@1.6.0: {} source-map-js@1.2.1: {} @@ -5974,6 +6692,12 @@ snapshots: dependencies: copy-anything: 4.0.5 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svg-arc-to-cubic-bezier@3.2.0: {} + svgo@3.3.2: dependencies: '@trysound/sax': 0.2.0 @@ -6013,7 +6737,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tar@7.5.2: + tar@7.5.3: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -6030,6 +6754,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tmp@0.2.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toidentifier@1.0.1: {} totalist@3.0.1: {} @@ -6042,8 +6772,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tunnel-agent@0.6.0: dependencies: @@ -6069,6 +6798,16 @@ snapshots: undici@7.18.2: {} + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -6165,6 +6904,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite-compatible-readable-stream@3.6.1: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + vite-hot-client@2.1.0(vite@6.4.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): dependencies: vite: 6.4.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) @@ -6435,6 +7180,8 @@ snapshots: yoctocolors@2.1.2: {} + yoga-layout@3.2.1: {} + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/scripts/migrate.js b/scripts/migrate.js new file mode 100644 index 0000000..baecf4a --- /dev/null +++ b/scripts/migrate.js @@ -0,0 +1,45 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import path from "path"; +import fs from "fs"; + +const runMigrations = () => { + console.log("Starting database migrations..."); + + const dbUrl = + process.env.DATABASE_URL || path.resolve(process.cwd(), "chronus.db"); + const dbDir = path.dirname(dbUrl); + + if (!fs.existsSync(dbDir)) { + console.log(`Creating directory for database: ${dbDir}`); + fs.mkdirSync(dbDir, { recursive: true }); + } + + console.log(`Using database at: ${dbUrl}`); + + const sqlite = new Database(dbUrl); + const db = drizzle(sqlite); + + const migrationsFolder = path.resolve(process.cwd(), "drizzle"); + + if (!fs.existsSync(migrationsFolder)) { + console.error(`Migrations folder not found at: ${migrationsFolder}`); + console.error( + "Did you run `drizzle-kit generate` and copy the folder to the container?", + ); + process.exit(1); + } + + try { + migrate(db, { migrationsFolder }); + console.log("Migrations completed successfully"); + } catch (error) { + console.error("Migration failed:", error); + process.exit(1); + } + + sqlite.close(); +}; + +runMigrations(); diff --git a/src/db/schema.ts b/src/db/schema.ts index ceb313e..de34ddb 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -2,6 +2,7 @@ import { sqliteTable, text, integer, + real, primaryKey, foreignKey, } from "drizzle-orm/sqlite-core"; @@ -25,6 +26,11 @@ export const organizations = sqliteTable("organizations", { .primaryKey() .$defaultFn(() => nanoid()), name: text("name").notNull(), + street: text("street"), + city: text("city"), + state: text("state"), + zip: text("zip"), + country: text("country"), createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn( () => new Date(), ), @@ -221,3 +227,58 @@ export const apiTokens = sqliteTable( }), }), ); + +export const invoices = sqliteTable( + "invoices", + { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + organizationId: text("organization_id").notNull(), + clientId: text("client_id").notNull(), + number: text("number").notNull(), + type: text("type").notNull().default("invoice"), // 'invoice' or 'quote' + status: text("status").notNull().default("draft"), // 'draft', 'sent', 'paid', 'void', 'accepted', 'declined' + issueDate: integer("issue_date", { mode: "timestamp" }).notNull(), + dueDate: integer("due_date", { mode: "timestamp" }).notNull(), + notes: text("notes"), + currency: text("currency").default("USD").notNull(), + subtotal: integer("subtotal").notNull().default(0), // in cents + taxRate: real("tax_rate").default(0), // percentage + taxAmount: integer("tax_amount").notNull().default(0), // in cents + total: integer("total").notNull().default(0), // in cents + createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn( + () => new Date(), + ), + }, + (table: any) => ({ + orgFk: foreignKey({ + columns: [table.organizationId], + foreignColumns: [organizations.id], + }), + clientFk: foreignKey({ + columns: [table.clientId], + foreignColumns: [clients.id], + }), + }), +); + +export const invoiceItems = sqliteTable( + "invoice_items", + { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + invoiceId: text("invoice_id").notNull(), + description: text("description").notNull(), + quantity: real("quantity").notNull().default(1), + unitPrice: integer("unit_price").notNull().default(0), // in cents + amount: integer("amount").notNull().default(0), // in cents + }, + (table: any) => ({ + invoiceFk: foreignKey({ + columns: [table.invoiceId], + foreignColumns: [invoices.id], + }), + }), +); diff --git a/src/layouts/DashboardLayout.astro b/src/layouts/DashboardLayout.astro index 7b03ac3..607c631 100644 --- a/src/layouts/DashboardLayout.astro +++ b/src/layouts/DashboardLayout.astro @@ -125,6 +125,13 @@ const currentTeam = userMemberships.find(m => m.organization.id === currentTeamI Time Tracker +
  • + + Invoices & Quotes +
  • { + try { + const { id } = params; + const user = locals.user; + + if (!user || !id) { + return new Response("Unauthorized", { status: 401 }); + } + + // Fetch invoice with related data + const invoiceResult = await db + .select({ + invoice: invoices, + client: clients, + organization: organizations, + }) + .from(invoices) + .leftJoin(clients, eq(invoices.clientId, clients.id)) + .innerJoin(organizations, eq(invoices.organizationId, organizations.id)) + .where(eq(invoices.id, id)) + .get(); + + if (!invoiceResult) { + return new Response("Invoice not found", { status: 404 }); + } + + const { invoice, client, organization } = invoiceResult; + + // Verify access + const membership = await db + .select() + .from(members) + .where( + and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId), + ), + ) + .get(); + + if (!membership) { + return new Response("Forbidden", { status: 403 }); + } + + // Fetch items + const items = await db + .select() + .from(invoiceItems) + .where(eq(invoiceItems.invoiceId, invoice.id)) + .all(); + + if (!client) { + return new Response("Client not found", { status: 404 }); + } + + // Generate PDF using Vue PDF + // Suppress verbose logging from PDF renderer + const originalConsoleLog = console.log; + console.log = () => {}; + + try { + const pdfDocument = createInvoiceDocument({ + invoice, + items, + client, + organization, + }); + + const stream = await renderToStream(pdfDocument); + + // Restore console.log + console.log = originalConsoleLog; + + const filename = `${invoice.type}_${invoice.number.replace(/[^a-zA-Z0-9]/g, "_")}.pdf`; + + return new Response(stream as any, { + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="${filename}"`, + }, + }); + } catch (pdfError) { + // Restore console.log on error + console.log = originalConsoleLog; + throw pdfError; + } + } catch (error) { + console.error("Error generating PDF:", error); + return new Response("Error generating PDF", { status: 500 }); + } +}; diff --git a/src/pages/api/invoices/[id]/items/add.ts b/src/pages/api/invoices/[id]/items/add.ts new file mode 100644 index 0000000..4ad5c24 --- /dev/null +++ b/src/pages/api/invoices/[id]/items/add.ts @@ -0,0 +1,88 @@ +import type { APIRoute } from "astro"; +import { db } from "../../../../../db"; +import { invoiceItems, invoices, members } from "../../../../../db/schema"; +import { eq, and } from "drizzle-orm"; +import { recalculateInvoiceTotals } from "../../../../../utils/invoice"; + +export const POST: APIRoute = async ({ + request, + redirect, + locals, + params, +}) => { + const user = locals.user; + if (!user) { + return redirect("/login"); + } + + const { id: invoiceId } = params; + if (!invoiceId) { + return new Response("Invoice ID required", { status: 400 }); + } + + // Fetch invoice to verify existence and check status + const invoice = await db + .select() + .from(invoices) + .where(eq(invoices.id, invoiceId)) + .get(); + + if (!invoice) { + return new Response("Invoice not found", { status: 404 }); + } + + // Verify membership + const membership = await db + .select() + .from(members) + .where( + and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + ) + ) + .get(); + + if (!membership) { + return new Response("Unauthorized", { status: 401 }); + } + + // Only allow editing if draft + if (invoice.status !== "draft") { + return new Response("Cannot edit a finalized invoice", { status: 400 }); + } + + const formData = await request.formData(); + const description = formData.get("description") as string; + const quantityStr = formData.get("quantity") as string; + const unitPriceStr = formData.get("unitPrice") as string; + + if (!description || !quantityStr || !unitPriceStr) { + return new Response("Missing required fields", { status: 400 }); + } + + const quantity = parseFloat(quantityStr); + const unitPriceMajor = parseFloat(unitPriceStr); + + // Convert to cents + const unitPrice = Math.round(unitPriceMajor * 100); + const amount = Math.round(quantity * unitPrice); + + try { + await db.insert(invoiceItems).values({ + invoiceId, + description, + quantity, + unitPrice, + amount, + }); + + // Update invoice totals + await recalculateInvoiceTotals(invoiceId); + + return redirect(`/dashboard/invoices/${invoiceId}`); + } catch (error) { + console.error("Error adding invoice item:", error); + return new Response("Internal Server Error", { status: 500 }); + } +}; diff --git a/src/pages/api/invoices/[id]/items/delete.ts b/src/pages/api/invoices/[id]/items/delete.ts new file mode 100644 index 0000000..c31aeb3 --- /dev/null +++ b/src/pages/api/invoices/[id]/items/delete.ts @@ -0,0 +1,84 @@ +import type { APIRoute } from "astro"; +import { db } from "../../../../../db"; +import { invoiceItems, invoices, members } from "../../../../../db/schema"; +import { eq, and } from "drizzle-orm"; +import { recalculateInvoiceTotals } from "../../../../../utils/invoice"; + +export const POST: APIRoute = async ({ + request, + redirect, + locals, + params, +}) => { + const user = locals.user; + if (!user) { + return redirect("/login"); + } + + const { id: invoiceId } = params; + if (!invoiceId) { + return new Response("Invoice ID required", { status: 400 }); + } + + // Fetch invoice to verify existence and check status + const invoice = await db + .select() + .from(invoices) + .where(eq(invoices.id, invoiceId)) + .get(); + + if (!invoice) { + return new Response("Invoice not found", { status: 404 }); + } + + // Verify membership + const membership = await db + .select() + .from(members) + .where( + and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + ) + ) + .get(); + + if (!membership) { + return new Response("Unauthorized", { status: 401 }); + } + + // Only allow editing if draft + if (invoice.status !== "draft") { + return new Response("Cannot edit a finalized invoice", { status: 400 }); + } + + const formData = await request.formData(); + const itemId = formData.get("itemId") as string; + + if (!itemId) { + return new Response("Item ID required", { status: 400 }); + } + + // Verify item belongs to invoice + const item = await db + .select() + .from(invoiceItems) + .where(and(eq(invoiceItems.id, itemId), eq(invoiceItems.invoiceId, invoiceId))) + .get(); + + if (!item) { + return new Response("Item not found", { status: 404 }); + } + + try { + await db.delete(invoiceItems).where(eq(invoiceItems.id, itemId)); + + // Update invoice totals + await recalculateInvoiceTotals(invoiceId); + + return redirect(`/dashboard/invoices/${invoiceId}`); + } catch (error) { + console.error("Error deleting invoice item:", error); + return new Response("Internal Server Error", { status: 500 }); + } +}; diff --git a/src/pages/api/invoices/[id]/status.ts b/src/pages/api/invoices/[id]/status.ts new file mode 100644 index 0000000..8756a0c --- /dev/null +++ b/src/pages/api/invoices/[id]/status.ts @@ -0,0 +1,76 @@ +import type { APIRoute } from "astro"; +import { db } from "../../../../db"; +import { invoices, members } from "../../../../db/schema"; +import { eq, and } from "drizzle-orm"; + +export const POST: APIRoute = async ({ + request, + redirect, + locals, + params, +}) => { + const user = locals.user; + if (!user) { + return redirect("/login"); + } + + const { id: invoiceId } = params; + if (!invoiceId) { + return new Response("Invoice ID required", { status: 400 }); + } + + const formData = await request.formData(); + const status = formData.get("status") as string; + + const validStatuses = [ + "draft", + "sent", + "paid", + "void", + "accepted", + "declined", + ]; + + if (!status || !validStatuses.includes(status)) { + return new Response("Invalid status", { status: 400 }); + } + + // Fetch invoice to verify existence and check ownership + const invoice = await db + .select() + .from(invoices) + .where(eq(invoices.id, invoiceId)) + .get(); + + if (!invoice) { + return new Response("Invoice not found", { status: 404 }); + } + + // Verify membership + const membership = await db + .select() + .from(members) + .where( + and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + ) + ) + .get(); + + if (!membership) { + return new Response("Unauthorized", { status: 401 }); + } + + try { + await db + .update(invoices) + .set({ status: status as any }) + .where(eq(invoices.id, invoiceId)); + + return redirect(`/dashboard/invoices/${invoiceId}`); + } catch (error) { + console.error("Error updating invoice status:", error); + return new Response("Internal Server Error", { status: 500 }); + } +}; diff --git a/src/pages/api/invoices/[id]/update.ts b/src/pages/api/invoices/[id]/update.ts new file mode 100644 index 0000000..4baf548 --- /dev/null +++ b/src/pages/api/invoices/[id]/update.ts @@ -0,0 +1,87 @@ +import type { APIRoute } from "astro"; +import { db } from "../../../../db"; +import { invoices, members } from "../../../../db/schema"; +import { eq, and } from "drizzle-orm"; +import { recalculateInvoiceTotals } from "../../../../utils/invoice"; + +export const POST: APIRoute = async ({ + request, + redirect, + locals, + params, +}) => { + const user = locals.user; + if (!user) { + return redirect("/login"); + } + + const { id: invoiceId } = params; + if (!invoiceId) { + return new Response("Invoice ID required", { status: 400 }); + } + + // Fetch invoice to verify existence + const invoice = await db + .select() + .from(invoices) + .where(eq(invoices.id, invoiceId)) + .get(); + + if (!invoice) { + return new Response("Invoice not found", { status: 404 }); + } + + // Verify membership + const membership = await db + .select() + .from(members) + .where( + and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + ) + ) + .get(); + + if (!membership) { + return new Response("Unauthorized", { status: 401 }); + } + + const formData = await request.formData(); + const number = formData.get("number") as string; + const currency = formData.get("currency") as string; + const issueDateStr = formData.get("issueDate") as string; + const dueDateStr = formData.get("dueDate") as string; + const taxRateStr = formData.get("taxRate") as string; + const notes = formData.get("notes") as string; + + if (!number || !currency || !issueDateStr || !dueDateStr) { + return new Response("Missing required fields", { status: 400 }); + } + + try { + const issueDate = new Date(issueDateStr); + const dueDate = new Date(dueDateStr); + const taxRate = taxRateStr ? parseFloat(taxRateStr) : 0; + + await db + .update(invoices) + .set({ + number, + currency, + issueDate, + dueDate, + taxRate, + notes: notes || null, + }) + .where(eq(invoices.id, invoiceId)); + + // Recalculate totals in case tax rate changed + await recalculateInvoiceTotals(invoiceId); + + return redirect(`/dashboard/invoices/${invoiceId}`); + } catch (error) { + console.error("Error updating invoice:", error); + return new Response("Internal Server Error", { status: 500 }); + } +}; diff --git a/src/pages/api/invoices/create.ts b/src/pages/api/invoices/create.ts new file mode 100644 index 0000000..6decfab --- /dev/null +++ b/src/pages/api/invoices/create.ts @@ -0,0 +1,74 @@ +import type { APIRoute } from "astro"; +import { db } from "../../../db"; +import { invoices, members } from "../../../db/schema"; +import { eq, and } from "drizzle-orm"; + +export const POST: APIRoute = async ({ request, redirect, locals, cookies }) => { + const user = locals.user; + if (!user) { + return redirect("/login"); + } + + const formData = await request.formData(); + const type = formData.get("type") as string; + const clientId = formData.get("clientId") as string; + const number = formData.get("number") as string; + const issueDateStr = formData.get("issueDate") as string; + const dueDateStr = formData.get("dueDate") as string; + const currency = formData.get("currency") as string; + + if (!type || !clientId || !number || !issueDateStr || !dueDateStr) { + return new Response("Missing required fields", { status: 400 }); + } + + // Get current team context + const currentTeamId = cookies.get("currentTeamId")?.value; + + // Verify membership + const userMemberships = await db + .select() + .from(members) + .where(eq(members.userId, user.id)) + .all(); + + if (userMemberships.length === 0) { + return redirect("/dashboard"); + } + + const membership = currentTeamId + ? userMemberships.find((m) => m.organizationId === currentTeamId) + : userMemberships[0]; + + if (!membership) { + return new Response("Unauthorized", { status: 401 }); + } + + const organizationId = membership.organizationId; + + try { + const issueDate = new Date(issueDateStr); + const dueDate = new Date(dueDateStr); + + const [newInvoice] = await db + .insert(invoices) + .values({ + organizationId, + clientId, + number, + type: type as "invoice" | "quote", + status: "draft", + issueDate, + dueDate, + currency: currency || "USD", + subtotal: 0, + taxAmount: 0, + total: 0, + }) + .returning(); + + return redirect(`/dashboard/invoices/${newInvoice.id}`); + } catch (error) { + console.error("Error creating invoice:", error); + return new Response("Internal Server Error", { status: 500 }); + } +}; diff --git a/src/pages/api/invoices/delete.ts b/src/pages/api/invoices/delete.ts new file mode 100644 index 0000000..4aacdc2 --- /dev/null +++ b/src/pages/api/invoices/delete.ts @@ -0,0 +1,58 @@ +import type { APIRoute } from "astro"; +import { db } from "../../../db"; +import { invoices, invoiceItems, members } from "../../../db/schema"; +import { eq, and } from "drizzle-orm"; + +export const POST: APIRoute = async ({ request, redirect, locals }) => { + const user = locals.user; + if (!user) { + return redirect("/login"); + } + + const formData = await request.formData(); + const invoiceId = formData.get("id") as string; + + if (!invoiceId) { + return new Response("Invoice ID required", { status: 400 }); + } + + // Fetch invoice to verify existence and check ownership + const invoice = await db + .select() + .from(invoices) + .where(eq(invoices.id, invoiceId)) + .get(); + + if (!invoice) { + return new Response("Invoice not found", { status: 404 }); + } + + // Verify membership + const membership = await db + .select() + .from(members) + .where( + and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + ) + ) + .get(); + + if (!membership) { + return new Response("Unauthorized", { status: 401 }); + } + + try { + // Delete invoice items first (manual cascade) + await db.delete(invoiceItems).where(eq(invoiceItems.invoiceId, invoiceId)); + + // Delete the invoice + await db.delete(invoices).where(eq(invoices.id, invoiceId)); + + return redirect("/dashboard/invoices"); + } catch (error) { + console.error("Error deleting invoice:", error); + return new Response("Internal Server Error", { status: 500 }); + } +}; diff --git a/src/pages/api/organizations/update-name.ts b/src/pages/api/organizations/update-name.ts index 5cd1c67..cb3637e 100644 --- a/src/pages/api/organizations/update-name.ts +++ b/src/pages/api/organizations/update-name.ts @@ -12,6 +12,11 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { const formData = await request.formData(); const organizationId = formData.get("organizationId") as string; const name = formData.get("name") as string; + const street = formData.get("street") as string | null; + const city = formData.get("city") as string | null; + const state = formData.get("state") as string | null; + const zip = formData.get("zip") as string | null; + const country = formData.get("country") as string | null; if (!organizationId || !name || name.trim().length === 0) { return new Response("Organization ID and name are required", { @@ -44,16 +49,23 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { ); } - // Update organization name + // Update organization information await db .update(organizations) - .set({ name: name.trim() }) + .set({ + name: name.trim(), + street: street?.trim() || null, + city: city?.trim() || null, + state: state?.trim() || null, + zip: zip?.trim() || null, + country: country?.trim() || null, + }) .where(eq(organizations.id, organizationId)) .run(); return redirect("/dashboard/team/settings?success=org-name"); } catch (error) { - console.error("Error updating organization name:", error); - return new Response("Failed to update organization name", { status: 500 }); + console.error("Error updating organization:", error); + return new Response("Failed to update organization", { status: 500 }); } }; diff --git a/src/pages/dashboard/invoices/[id].astro b/src/pages/dashboard/invoices/[id].astro new file mode 100644 index 0000000..da11a65 --- /dev/null +++ b/src/pages/dashboard/invoices/[id].astro @@ -0,0 +1,312 @@ +--- +import DashboardLayout from '../../../layouts/DashboardLayout.astro'; +import { Icon } from 'astro-icon/components'; +import { db } from '../../../db'; +import { invoices, invoiceItems, clients, members, organizations } from '../../../db/schema'; +import { eq, and } from 'drizzle-orm'; + +const { id } = Astro.params; +const user = Astro.locals.user; + +if (!user || !id) { + return Astro.redirect('/dashboard/invoices'); +} + +// Fetch invoice with related data +const invoiceResult = await db.select({ + invoice: invoices, + client: clients, + organization: organizations, +}) + .from(invoices) + .leftJoin(clients, eq(invoices.clientId, clients.id)) + .innerJoin(organizations, eq(invoices.organizationId, organizations.id)) + .where(eq(invoices.id, id)) + .get(); + +if (!invoiceResult) { + return Astro.redirect('/404'); +} + +const { invoice, client, organization } = invoiceResult; + +// Verify access +const membership = await db.select() + .from(members) + .where(and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + )) + .get(); + +if (!membership) { + return Astro.redirect('/dashboard'); +} + +// Fetch items +const items = await db.select() + .from(invoiceItems) + .where(eq(invoiceItems.invoiceId, invoice.id)) + .all(); + +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: invoice.currency, + }).format(amount / 100); +}; + +const isDraft = invoice.status === 'draft'; +--- + + +
    + +
    +
    +
    + + + +
    + {invoice.status} +
    +
    +

    {invoice.number}

    +
    + +
    + {isDraft && ( +
    + + +
    + )} + {(invoice.status === 'sent' && invoice.type === 'invoice') && ( +
    + + +
    + )} + {(invoice.status === 'sent' && invoice.type === 'quote') && ( +
    + + +
    + )} + +
    +
    + + +
    +
    + +
    +
    +

    {organization.name}

    + {(organization.street || organization.city || organization.state || organization.zip || organization.country) && ( +
    + {organization.street &&
    {organization.street}
    } + {(organization.city || organization.state || organization.zip) && ( +
    + {[organization.city, organization.state, organization.zip].filter(Boolean).join(', ')} +
    + )} + {organization.country &&
    {organization.country}
    } +
    + )} +
    +
    +
    + {invoice.type} +
    +
    +
    Number:
    +
    {invoice.number}
    +
    Date:
    +
    {invoice.issueDate.toLocaleDateString()}
    +
    Due Date:
    +
    {invoice.dueDate.toLocaleDateString()}
    +
    +
    +
    + + +
    +
    Bill To
    + {client ? ( +
    +
    {client.name}
    +
    {client.email}
    +
    + ) : ( +
    Client deleted
    + )} +
    + + +
    + + + + + + + + {isDraft && } + + + + {items.map(item => ( + + + + + + {isDraft && ( + + )} + + ))} + {items.length === 0 && ( + + + + )} + +
    DescriptionQtyPriceAmount
    {item.description}{item.quantity}{formatCurrency(item.unitPrice)}{formatCurrency(item.amount)} +
    + + +
    +
    + No items added yet. +
    +
    + + + {isDraft && ( +
    +

    Add Item

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + )} + + +
    +
    +
    + Subtotal + {formatCurrency(invoice.subtotal)} +
    + {(invoice.taxRate ?? 0) > 0 && ( +
    + Tax ({invoice.taxRate}%) + {formatCurrency(invoice.taxAmount)} +
    + )} +
    +
    + Total + {formatCurrency(invoice.total)} +
    +
    +
    + + + {invoice.notes && ( +
    +
    Notes
    +
    {invoice.notes}
    +
    + )} + + {/* Edit Notes (Draft Only) - Simplistic approach */} + {isDraft && !invoice.notes && ( +
    + Add Notes +
    + )} +
    +
    +
    + diff --git a/src/pages/dashboard/invoices/[id]/edit.astro b/src/pages/dashboard/invoices/[id]/edit.astro new file mode 100644 index 0000000..047fdd1 --- /dev/null +++ b/src/pages/dashboard/invoices/[id]/edit.astro @@ -0,0 +1,153 @@ +--- +import DashboardLayout from '../../../../layouts/DashboardLayout.astro'; +import { Icon } from 'astro-icon/components'; +import { db } from '../../../../db'; +import { invoices, members } from '../../../../db/schema'; +import { eq, and } from 'drizzle-orm'; + +const { id } = Astro.params; +const user = Astro.locals.user; + +if (!user || !id) { + return Astro.redirect('/dashboard/invoices'); +} + +// Fetch invoice +const invoice = await db.select() + .from(invoices) + .where(eq(invoices.id, id)) + .get(); + +if (!invoice) { + return Astro.redirect('/404'); +} + +// Verify membership +const membership = await db.select() + .from(members) + .where(and( + eq(members.userId, user.id), + eq(members.organizationId, invoice.organizationId) + )) + .get(); + +if (!membership) { + return Astro.redirect('/dashboard'); +} + +// Format dates for input[type="date"] +const issueDateStr = invoice.issueDate.toISOString().split('T')[0]; +const dueDateStr = invoice.dueDate.toISOString().split('T')[0]; +--- + + +
    +
    + + + Back to Invoice + +

    Edit Details

    +
    + +
    +
    + +
    + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    +
    + + +
    + + +
    + +
    + +
    + Cancel + +
    +
    +
    +
    +
    diff --git a/src/pages/dashboard/invoices/index.astro b/src/pages/dashboard/invoices/index.astro new file mode 100644 index 0000000..b549b1c --- /dev/null +++ b/src/pages/dashboard/invoices/index.astro @@ -0,0 +1,215 @@ +--- +import DashboardLayout from '../../../layouts/DashboardLayout.astro'; +import { Icon } from 'astro-icon/components'; +import { db } from '../../../db'; +import { invoices, clients, members } from '../../../db/schema'; +import { eq, desc, and } from 'drizzle-orm'; + +const user = Astro.locals.user; +if (!user) return Astro.redirect('/login'); + +// Get current team from cookie +const currentTeamId = Astro.cookies.get('currentTeamId')?.value; + +const userMemberships = await db.select() + .from(members) + .where(eq(members.userId, user.id)) + .all(); + +if (userMemberships.length === 0) return Astro.redirect('/dashboard'); + +// Use current team or fallback to first membership +const userMembership = currentTeamId + ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] + : userMemberships[0]; + +const currentTeamIdResolved = userMembership.organizationId; + +// Fetch invoices and quotes +const allInvoices = await db.select({ + invoice: invoices, + client: clients, +}) + .from(invoices) + .leftJoin(clients, eq(invoices.clientId, clients.id)) + .where(eq(invoices.organizationId, currentTeamIdResolved)) + .orderBy(desc(invoices.issueDate)) + .all(); + +const formatCurrency = (amount: number, currency: string) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency, + }).format(amount / 100); +}; + +const getStatusColor = (status: string) => { + switch (status) { + case 'paid': return 'badge-success'; + case 'accepted': return 'badge-success'; + case 'sent': return 'badge-info'; + case 'draft': return 'badge-ghost'; + case 'void': return 'badge-error'; + case 'declined': return 'badge-error'; + default: return 'badge-ghost'; + } +}; +--- + + +
    +
    +

    Invoices & Quotes

    +

    Manage your billing and estimates

    +
    + + + Create New + +
    + +
    +
    +
    +
    + +
    +
    Total Invoices
    +
    {allInvoices.filter(i => i.invoice.type === 'invoice').length}
    +
    All time
    +
    +
    + +
    +
    +
    + +
    +
    Open Quotes
    +
    {allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length}
    +
    Waiting for approval
    +
    +
    + +
    +
    +
    + +
    +
    Total Revenue
    +
    + {formatCurrency(allInvoices + .filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid') + .reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')} +
    +
    Paid invoices
    +
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + {allInvoices.length === 0 ? ( + + + + ) : ( + allInvoices.map(({ invoice, client }) => ( + + + + + + + + + + + )) + )} + +
    NumberClientDateDue DateAmountStatusType
    + No invoices or quotes found. Create one to get started. +
    + + {invoice.number} + + + {client ? ( +
    {client.name}
    + ) : ( + Deleted Client + )} +
    {invoice.issueDate.toLocaleDateString()}{invoice.dueDate.toLocaleDateString()} + {formatCurrency(invoice.total, invoice.currency)} + +
    + {invoice.status} +
    +
    + {invoice.type} + + +
    +
    +
    +
    diff --git a/src/pages/dashboard/invoices/new.astro b/src/pages/dashboard/invoices/new.astro new file mode 100644 index 0000000..a4dd199 --- /dev/null +++ b/src/pages/dashboard/invoices/new.astro @@ -0,0 +1,233 @@ +--- +import DashboardLayout from '../../../layouts/DashboardLayout.astro'; +import { Icon } from 'astro-icon/components'; +import { db } from '../../../db'; +import { clients, members, invoices } from '../../../db/schema'; +import { eq, desc, and } from 'drizzle-orm'; + +const user = Astro.locals.user; +if (!user) return Astro.redirect('/login'); + +// Get current team from cookie +const currentTeamId = Astro.cookies.get('currentTeamId')?.value; + +const userMemberships = await db.select() + .from(members) + .where(eq(members.userId, user.id)) + .all(); + +if (userMemberships.length === 0) return Astro.redirect('/dashboard'); + +// Use current team or fallback to first membership +const userMembership = currentTeamId + ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] + : userMemberships[0]; + +const currentTeamIdResolved = userMembership.organizationId; + +// Fetch clients for dropdown +const teamClients = await db.select() + .from(clients) + .where(eq(clients.organizationId, currentTeamIdResolved)) + .all(); + +// Generate next invoice number (INV-) +const lastInvoice = await db.select() + .from(invoices) + .where(and( + eq(invoices.organizationId, currentTeamIdResolved), + eq(invoices.type, 'invoice') + )) + .orderBy(desc(invoices.createdAt)) + .limit(1) + .get(); + +let nextInvoiceNumber = 'INV-001'; +if (lastInvoice) { + const match = lastInvoice.number.match(/(\d+)$/); + if (match) { + const num = parseInt(match[1]) + 1; + const prefix = lastInvoice.number.replace(match[0], ''); + nextInvoiceNumber = prefix + num.toString().padStart(match[0].length, '0'); + } +} + +// Generate next quote number (EST-) +const lastQuote = await db.select() + .from(invoices) + .where(and( + eq(invoices.organizationId, currentTeamIdResolved), + eq(invoices.type, 'quote') + )) + .orderBy(desc(invoices.createdAt)) + .limit(1) + .get(); + +let nextQuoteNumber = 'EST-001'; +if (lastQuote) { + const match = lastQuote.number.match(/(\d+)$/); + if (match) { + const num = parseInt(match[1]) + 1; + const prefix = lastQuote.number.replace(match[0], ''); + nextQuoteNumber = prefix + num.toString().padStart(match[0].length, '0'); + } +} + +const today = new Date().toISOString().split('T')[0]; +const nextMonth = new Date(); +nextMonth.setDate(nextMonth.getDate() + 30); +const defaultDueDate = nextMonth.toISOString().split('T')[0]; +--- + + +
    +
    + + + Back to Invoices + +

    Create New Document

    +
    + + {teamClients.length === 0 ? ( + + ) : ( +
    +
    + + +
    + +
    + + +
    +
    + +
    + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    +
    + +
    + +
    + Cancel + +
    +
    +
    + )} +
    +
    + + diff --git a/src/pages/dashboard/reports.astro b/src/pages/dashboard/reports.astro index 375ca25..d6eb44a 100644 --- a/src/pages/dashboard/reports.astro +++ b/src/pages/dashboard/reports.astro @@ -5,7 +5,7 @@ import CategoryChart from '../../components/CategoryChart.vue'; import ClientChart from '../../components/ClientChart.vue'; import MemberChart from '../../components/MemberChart.vue'; import { db } from '../../db'; -import { timeEntries, members, users, clients, categories } from '../../db/schema'; +import { timeEntries, members, users, clients, categories, invoices } from '../../db/schema'; import { eq, and, gte, lte, sql, desc } from 'drizzle-orm'; import { formatDuration, formatTimeRange } from '../../lib/formatTime'; @@ -23,7 +23,7 @@ const userMemberships = await db.select() if (userMemberships.length === 0) return Astro.redirect('/dashboard'); // Use current team or fallback to first membership -const userMembership = currentTeamId +const userMembership = currentTeamId ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] : userMemberships[0]; @@ -120,7 +120,7 @@ const statsByMember = teamMembers.map(member => { } return sum; }, 0); - + return { member, totalTime, @@ -136,7 +136,7 @@ const statsByCategory = allCategories.map(category => { } return sum; }, 0); - + return { category, totalTime, @@ -152,7 +152,7 @@ const statsByClient = allClients.map(client => { } return sum; }, 0); - + return { client, totalTime, @@ -167,6 +167,81 @@ const totalTime = entries.reduce((sum, e) => { return sum; }, 0); +// Fetch invoices and quotes for the same time period +const invoiceConditions = [ + eq(invoices.organizationId, userMembership.organizationId), + gte(invoices.issueDate, startDate), + lte(invoices.issueDate, endDate), +]; + +if (selectedClientId) { + invoiceConditions.push(eq(invoices.clientId, selectedClientId)); +} + +const allInvoices = await db.select({ + invoice: invoices, + client: clients, +}) + .from(invoices) + .leftJoin(clients, eq(invoices.clientId, clients.id)) + .where(and(...invoiceConditions)) + .orderBy(desc(invoices.issueDate)) + .all(); + +// Invoice statistics +const invoiceStats = { + total: allInvoices.filter(i => i.invoice.type === 'invoice').length, + paid: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid').length, + sent: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'sent').length, + draft: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'draft').length, + void: allInvoices.filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'void').length, +}; + +// Quote statistics +const quoteStats = { + total: allInvoices.filter(i => i.invoice.type === 'quote').length, + accepted: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'accepted').length, + sent: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length, + declined: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'declined').length, + draft: allInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'draft').length, +}; + +// Revenue statistics +const revenueStats = { + total: allInvoices + .filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid') + .reduce((sum, i) => sum + i.invoice.total, 0), + pending: allInvoices + .filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'sent') + .reduce((sum, i) => sum + i.invoice.total, 0), + quotedValue: allInvoices + .filter(i => i.invoice.type === 'quote' && (i.invoice.status === 'sent' || i.invoice.status === 'accepted')) + .reduce((sum, i) => sum + i.invoice.total, 0), +}; + +// Revenue by client +const revenueByClient = allClients.map(client => { + const clientInvoices = allInvoices.filter(i => + i.client?.id === client.id && + i.invoice.type === 'invoice' && + i.invoice.status === 'paid' + ); + const revenue = clientInvoices.reduce((sum, i) => sum + i.invoice.total, 0); + + return { + client, + revenue, + invoiceCount: clientInvoices.length, + }; +}).filter(s => s.revenue > 0).sort((a, b) => b.revenue - a.revenue); + +function formatCurrency(amount: number, currency: string = 'USD') { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency, + }).format(amount / 100); +} + function getTimeRangeLabel(range: string) { switch (range) { case 'today': return 'Today'; @@ -247,7 +322,7 @@ function getTimeRangeLabel(range: string) { -
    +
    @@ -270,6 +345,17 @@ function getTimeRangeLabel(range: string) {
    +
    +
    +
    + +
    +
    Revenue
    +
    {formatCurrency(revenueStats.total)}
    +
    {invoiceStats.paid} paid invoices
    +
    +
    +
    @@ -282,6 +368,121 @@ function getTimeRangeLabel(range: string) {
    + +
    +
    +
    +

    + + Invoices Overview +

    +
    +
    +
    Total Invoices
    +
    {invoiceStats.total}
    +
    +
    +
    Paid
    +
    {invoiceStats.paid}
    +
    +
    +
    Sent
    +
    {invoiceStats.sent}
    +
    +
    +
    Draft
    +
    {invoiceStats.draft}
    +
    +
    +
    +
    + Revenue (Paid) + {formatCurrency(revenueStats.total)} +
    +
    + Pending (Sent) + {formatCurrency(revenueStats.pending)} +
    +
    +
    + +
    +
    +

    + + Quotes Overview +

    +
    +
    +
    Total Quotes
    +
    {quoteStats.total}
    +
    +
    +
    Accepted
    +
    {quoteStats.accepted}
    +
    +
    +
    Pending
    +
    {quoteStats.sent}
    +
    +
    +
    Declined
    +
    {quoteStats.declined}
    +
    +
    +
    +
    + Quoted Value + {formatCurrency(revenueStats.quotedValue)} +
    +
    + Conversion Rate + + {quoteStats.total > 0 ? Math.round((quoteStats.accepted / quoteStats.total) * 100) : 0}% + +
    +
    +
    +
    + + + {!selectedClientId && revenueByClient.length > 0 && ( +
    +
    +

    + + Revenue by Client +

    +
    + + + + + + + + + + + {revenueByClient.slice(0, 10).map(stat => ( + + + + + + + ))} + +
    ClientRevenueInvoicesAvg Invoice
    +
    {stat.client.name}
    +
    {formatCurrency(stat.revenue)}{stat.invoiceCount} + {stat.invoiceCount > 0 ? formatCurrency(stat.revenue / stat.invoiceCount) : formatCurrency(0)} +
    +
    +
    +
    + )} + {/* Charts Section - Only show if there's data */} {totalTime > 0 && ( <> @@ -295,8 +496,8 @@ function getTimeRangeLabel(range: string) { Category Distribution
    - s.totalTime > 0).map(s => ({ name: s.category.name, totalTime: s.totalTime, @@ -317,8 +518,8 @@ function getTimeRangeLabel(range: string) { Time by Client
    - s.totalTime > 0).map(s => ({ name: s.client.name, totalTime: s.totalTime @@ -339,8 +540,8 @@ function getTimeRangeLabel(range: string) { Time by Team Member
    - s.totalTime > 0).map(s => ({ name: s.member.name, totalTime: s.totalTime @@ -427,9 +628,9 @@ function getTimeRangeLabel(range: string) { {stat.entryCount}
    - @@ -472,9 +673,9 @@ function getTimeRangeLabel(range: string) { {stat.entryCount}
    - @@ -532,7 +733,7 @@ function getTimeRangeLabel(range: string) { {e.entry.description || '-'} - {e.entry.endTime + {e.entry.endTime ? formatDuration(e.entry.endTime.getTime() - e.entry.startTime.getTime()) : 'Running...' } diff --git a/src/pages/dashboard/team/settings.astro b/src/pages/dashboard/team/settings.astro index d8ffcda..a65cef0 100644 --- a/src/pages/dashboard/team/settings.astro +++ b/src/pages/dashboard/team/settings.astro @@ -63,7 +63,7 @@ const successType = url.searchParams.get('success'); {successType === 'org-name' && (
    - Team name updated successfully! + Team information updated successfully!
    )} @@ -87,6 +87,83 @@ const successType = url.searchParams.get('success');
    +
    Address Information
    + + + +
    + + + +
    + +
    + + + +
    + +
    + + Address information appears on invoices and quotes + +
    +