From 04b4e28d21e51786a116d709f9787431b49a5135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gjermund=20H=C3=B8s=C3=B8ien=20Wiggen?= Date: Sun, 7 Jun 2026 23:23:05 +0200 Subject: [PATCH] Change ticket IDs from UUID to sequential integers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - tickets.id: uuid → integer GENERATED ALWAYS AS IDENTITY - transactions.ticket_id, custom_field_values.ticket_id: uuid → integer - Routes convert string params to Number() for DB queries - ScripEngine.prepare takes ticketId: number - ActionPayload.ticketId: string → number Frontend: - Ticket.id: string → number, Transaction.ticket_id: string → number - API functions accept number params - formatTicketId() helper returns TKT-0001 format - Ticket rows display TKT-XXXX, detail page uses formatTicketId Migration: drops FKs, clears data, alters column types, re-adds FKs --- .../migrations/0001_lovely_quentin_quire.sql | 29 + drizzle/migrations/meta/0001_snapshot.json | 916 ++++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + src/db/schema.ts | 6 +- src/routes/tickets.ts | 8 +- src/scrip/actions.ts | 6 +- src/scrip/engine.ts | 2 +- web/src/app/page.tsx | 4 +- web/src/app/tickets/[id]/page.tsx | 7 +- web/src/lib/api.ts | 8 +- web/src/lib/types.ts | 4 +- web/src/lib/utils.ts | 4 + 12 files changed, 979 insertions(+), 22 deletions(-) create mode 100644 drizzle/migrations/0001_lovely_quentin_quire.sql create mode 100644 drizzle/migrations/meta/0001_snapshot.json diff --git a/drizzle/migrations/0001_lovely_quentin_quire.sql b/drizzle/migrations/0001_lovely_quentin_quire.sql new file mode 100644 index 0000000..602339c --- /dev/null +++ b/drizzle/migrations/0001_lovely_quentin_quire.sql @@ -0,0 +1,29 @@ +-- Drop foreign key constraints referencing tickets.id +ALTER TABLE "custom_field_values" DROP CONSTRAINT IF EXISTS "custom_field_values_ticket_id_tickets_id_fk"; +ALTER TABLE "transactions" DROP CONSTRAINT IF EXISTS "transactions_ticket_id_tickets_id_fk"; + +-- Drop dependent indexes +DROP INDEX IF EXISTS "custom_field_values_ticket_id_idx"; +DROP INDEX IF EXISTS "transactions_ticket_id_idx"; + +-- Clear all data from affected tables (UUIDs cannot cast to integer) +DELETE FROM "custom_field_values"; +DELETE FROM "transactions"; +DELETE FROM "tickets"; + +-- Alter column types with USING clause for empty tables +ALTER TABLE "custom_field_values" ALTER COLUMN "ticket_id" SET DATA TYPE integer USING (0); +ALTER TABLE "transactions" ALTER COLUMN "ticket_id" SET DATA TYPE integer USING (0); + +-- Alter tickets.id to serial +ALTER TABLE "tickets" ALTER COLUMN "id" DROP DEFAULT; +ALTER TABLE "tickets" ALTER COLUMN "id" SET DATA TYPE integer USING (0); +ALTER TABLE "tickets" ALTER COLUMN "id" ADD GENERATED ALWAYS AS IDENTITY (sequence name "tickets_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1); + +-- Re-add foreign key constraints +ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_ticket_id_tickets_id_fk" FOREIGN KEY ("ticket_id") REFERENCES "public"."tickets"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "transactions" ADD CONSTRAINT "transactions_ticket_id_tickets_id_fk" FOREIGN KEY ("ticket_id") REFERENCES "public"."tickets"("id") ON DELETE cascade ON UPDATE no action; + +-- Re-create indexes +CREATE INDEX "custom_field_values_ticket_id_idx" ON "custom_field_values" USING btree ("ticket_id"); +CREATE INDEX "transactions_ticket_id_idx" ON "transactions" USING btree ("ticket_id"); diff --git a/drizzle/migrations/meta/0001_snapshot.json b/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..a82d535 --- /dev/null +++ b/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,916 @@ +{ + "id": "042752b4-e1ad-4b6d-96ed-81f836028826", + "prevId": "981c2ca0-1a37-4fbd-8624-2e7f43cd8361", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.custom_field_values": { + "name": "custom_field_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "custom_field_id": { + "name": "custom_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "custom_field_values_ticket_id_idx": { + "name": "custom_field_values_ticket_id_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_field_values_custom_field_id_idx": { + "name": "custom_field_values_custom_field_id_idx", + "columns": [ + { + "expression": "custom_field_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_field_values_custom_field_id_custom_fields_id_fk": { + "name": "custom_field_values_custom_field_id_custom_fields_id_fk", + "tableFrom": "custom_field_values", + "tableTo": "custom_fields", + "columnsFrom": [ + "custom_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_field_values_ticket_id_tickets_id_fk": { + "name": "custom_field_values_ticket_id_tickets_id_fk", + "tableFrom": "custom_field_values", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "custom_field_values_cf_id_ticket_id_value_unique": { + "name": "custom_field_values_cf_id_ticket_id_value_unique", + "nullsNotDistinct": false, + "columns": [ + "custom_field_id", + "ticket_id", + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_fields": { + "name": "custom_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "values": { + "name": "values", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_values": { + "name": "max_values", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lifecycles": { + "name": "lifecycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "lifecycles_name_unique": { + "name": "lifecycles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.queue_custom_fields": { + "name": "queue_custom_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "custom_field_id": { + "name": "custom_field_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "queue_custom_fields_queue_id_queues_id_fk": { + "name": "queue_custom_fields_queue_id_queues_id_fk", + "tableFrom": "queue_custom_fields", + "tableTo": "queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "queue_custom_fields_custom_field_id_custom_fields_id_fk": { + "name": "queue_custom_fields_custom_field_id_custom_fields_id_fk", + "tableFrom": "queue_custom_fields", + "tableTo": "custom_fields", + "columnsFrom": [ + "custom_field_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "queue_custom_fields_queue_id_custom_field_id_unique": { + "name": "queue_custom_fields_queue_id_custom_field_id_unique", + "nullsNotDistinct": false, + "columns": [ + "queue_id", + "custom_field_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.queues": { + "name": "queues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle_id": { + "name": "lifecycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "queues_lifecycle_id_lifecycles_id_fk": { + "name": "queues_lifecycle_id_lifecycles_id_fk", + "tableFrom": "queues", + "tableTo": "lifecycles", + "columnsFrom": [ + "lifecycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "queues_name_unique": { + "name": "queues_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrips": { + "name": "scrips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition_type": { + "name": "condition_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "condition_config": { + "name": "condition_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_config": { + "name": "action_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'TransactionCreate'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "scrips_queue_id_idx": { + "name": "scrips_queue_id_idx", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scrips_queue_id_queues_id_fk": { + "name": "scrips_queue_id_queues_id_fk", + "tableFrom": "scrips", + "tableTo": "queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrips_template_id_templates_id_fk": { + "name": "scrips_template_id_templates_id_fk", + "tableFrom": "scrips", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_template": { + "name": "subject_template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_template": { + "name": "body_template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "templates_queue_id_queues_id_fk": { + "name": "templates_queue_id_queues_id_fk", + "tableFrom": "templates", + "tableTo": "queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tickets_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tickets_queue_id_idx": { + "name": "tickets_queue_id_idx", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tickets_queue_id_queues_id_fk": { + "name": "tickets_queue_id_queues_id_fk", + "tableFrom": "tickets", + "tableTo": "queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tickets_owner_id_users_id_fk": { + "name": "tickets_owner_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tickets_creator_id_users_id_fk": { + "name": "tickets_creator_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ticket_id": { + "name": "ticket_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field": { + "name": "field", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "transactions_ticket_id_idx": { + "name": "transactions_ticket_id_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_created_at_idx": { + "name": "transactions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_ticket_id_tickets_id_fk": { + "name": "transactions_ticket_id_tickets_id_fk", + "tableFrom": "transactions", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transactions_creator_id_users_id_fk": { + "name": "transactions_creator_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index a42c3b4..e79fa0f 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1780859982396, "tag": "0000_acoustic_wendell_vaughn", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1780867177929, + "tag": "0001_lovely_quentin_quire", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 945df06..b625949 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -24,7 +24,7 @@ export const lifecycles = pgTable('lifecycles', { }); export const tickets = pgTable('tickets', { - id: uuid('id').primaryKey().defaultRandom(), + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), subject: text('subject').notNull(), queue_id: uuid('queue_id').notNull().references(() => queues.id), status: text('status').notNull(), @@ -41,7 +41,7 @@ export const tickets = pgTable('tickets', { export const transactions = pgTable('transactions', { id: uuid('id').primaryKey().defaultRandom(), - ticket_id: uuid('ticket_id').notNull().references(() => tickets.id, { onDelete: 'cascade' }), + ticket_id: integer('ticket_id').notNull().references(() => tickets.id, { onDelete: 'cascade' }), transaction_type: text('transaction_type').notNull(), field: text('field'), old_value: text('old_value'), @@ -103,7 +103,7 @@ export const queueCustomFields = pgTable('queue_custom_fields', { export const customFieldValues = pgTable('custom_field_values', { id: uuid('id').primaryKey().defaultRandom(), custom_field_id: uuid('custom_field_id').notNull().references(() => customFields.id, { onDelete: 'cascade' }), - ticket_id: uuid('ticket_id').notNull().references(() => tickets.id, { onDelete: 'cascade' }), + ticket_id: integer('ticket_id').notNull().references(() => tickets.id, { onDelete: 'cascade' }), value: text('value').notNull(), created_at: timestamp('created_at', { withTimezone: true }).defaultNow(), }, (table) => ({ diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index f1fbd0e..cb75795 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -61,7 +61,7 @@ export function createTicketsRouter(db: Db): Hono { // GET /:id — get ticket with custom field values router.get('/:id', async (c) => { - const id = c.req.param('id'); + const id = Number(c.req.param('id')); const ticket = await db.query.tickets.findFirst({ where: eq(tickets.id, id), @@ -92,7 +92,7 @@ export function createTicketsRouter(db: Db): Hono { // PATCH /:id — update ticket router.patch('/:id', async (c) => { - const id = c.req.param('id'); + const id = Number(c.req.param('id')); const body = await c.req.json(); const parsed = UpdateTicketSchema.parse(body); @@ -186,7 +186,7 @@ export function createTicketsRouter(db: Db): Hono { // POST /:id/preview — dry-run scrips router.post('/:id/preview', async (c) => { - const id = c.req.param('id'); + const id = Number(c.req.param('id')); const body = await c.req.json(); const parsed = UpdateTicketSchema.parse(body); @@ -221,7 +221,7 @@ export function createTicketsRouter(db: Db): Hono { // GET /:id/transactions — list transactions for ticket router.get('/:id/transactions', async (c) => { - const id = c.req.param('id'); + const id = Number(c.req.param('id')); const result = await db.query.transactions.findMany({ where: eq(transactions.ticket_id, id), diff --git a/src/scrip/actions.ts b/src/scrip/actions.ts index b33006c..d19ebba 100644 --- a/src/scrip/actions.ts +++ b/src/scrip/actions.ts @@ -13,7 +13,7 @@ export interface ActionPayload { scripName: string; actionType: string; actionConfig: Record; - ticketId?: string; + ticketId?: number; recipients?: string[]; subject?: string; body?: string; @@ -97,7 +97,7 @@ export class SetCustomField implements ActionExecutor { async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> { const fieldId = payload.field_id ?? String(payload.actionConfig['field_id'] ?? ''); const value = payload.value ?? String(payload.actionConfig['value'] ?? ''); - const ticketId = payload.ticketId ?? String(payload.actionConfig['ticket_id'] ?? ''); + const ticketId = payload.ticketId ?? Number(payload.actionConfig['ticket_id'] ?? 0); if (!fieldId || !value || !ticketId) { return { success: false, message: 'SetCustomField: missing field_id, value, or ticket_id' }; @@ -121,7 +121,7 @@ export class CreateTransaction implements ActionExecutor { constructor(private db: Db) {} async execute(payload: ActionPayload): Promise<{ success: boolean; message: string }> { - const ticketId = payload.ticketId ?? String(payload.actionConfig['ticket_id'] ?? ''); + const ticketId = payload.ticketId ?? Number(payload.actionConfig['ticket_id'] ?? 0); const transactionType = String(payload.actionConfig['transaction_type'] ?? ''); const field = payload.actionConfig['field'] as string | undefined ?? null; const oldValue = payload.actionConfig['old_value'] as string | undefined ?? null; diff --git a/src/scrip/engine.ts b/src/scrip/engine.ts index 8cc3350..1962a21 100644 --- a/src/scrip/engine.ts +++ b/src/scrip/engine.ts @@ -35,7 +35,7 @@ export class ScripEngine { } async prepare( - ticketId: string, + ticketId: number, transactions: Transaction[], ): Promise { const ticketRecord = await this.db.query.tickets.findFirst({ diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index de5110c..0819e13 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -15,7 +15,7 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; -import { cn } from "@/lib/utils"; +import { cn, formatTicketId } from "@/lib/utils"; const STATUS_COLORS: Record = { new: "#8a8f98", @@ -38,7 +38,7 @@ type FilterKey = (typeof FILTERS)[number]["key"]; function TicketRow({ ticket, onClick }: { ticket: Ticket; onClick: () => void }) { const statusColor = STATUS_COLORS[ticket.status] || STATUS_COLORS.new; - const shortId = ticket.id.slice(0, 8); + const shortId = formatTicketId(ticket.id); const timeAgo = formatDistanceToNow(new Date(ticket.updated_at), { addSuffix: true }); return ( diff --git a/web/src/app/tickets/[id]/page.tsx b/web/src/app/tickets/[id]/page.tsx index 5062d7f..67514ea 100644 --- a/web/src/app/tickets/[id]/page.tsx +++ b/web/src/app/tickets/[id]/page.tsx @@ -20,7 +20,7 @@ import type { UpdateResult, } from "@/lib/types"; import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; +import { cn, formatTicketId } from "@/lib/utils"; const STATUS_COLORS: Record = { new: "#8a8f98", @@ -151,7 +151,8 @@ export default function TicketDetailPage({ }: { params: Promise<{ id: string }>; }) { - const { id } = use(params); + const { id: idParam } = use(params); + const id = Number(idParam); const router = useRouter(); const [ticket, setTicket] = useState(null); @@ -328,7 +329,7 @@ export default function TicketDetailPage({ {ticket.subject}

- {ticket.id.slice(0, 8)} · {queue?.name || ticket.queue_id} + {formatTicketId(ticket.id)} · {queue?.name || ticket.queue_id}

diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index f72e134..128b63c 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -36,7 +36,7 @@ export async function getTickets(params?: { queue_id?: string; status?: string } return request(`/tickets${qs ? `?${qs}` : ""}`); } -export async function getTicket(id: string): Promise<{ data: Ticket | null; error: string | null }> { +export async function getTicket(id: number): Promise<{ data: Ticket | null; error: string | null }> { return request(`/tickets/${id}`); } @@ -44,15 +44,15 @@ export async function createTicket(data: { subject: string; queue_id: string }): return request("/tickets", { method: "POST", body: JSON.stringify(data) }); } -export async function updateTicket(id: string, data: { subject?: string; status?: string }): Promise<{ data: UpdateResult | null; error: string | null }> { +export async function updateTicket(id: number, data: { subject?: string; status?: string }): Promise<{ data: UpdateResult | null; error: string | null }> { return request(`/tickets/${id}`, { method: "PATCH", body: JSON.stringify(data) }); } -export async function previewTicket(id: string, data: { status?: string }): Promise<{ data: PreviewResult | null; error: string | null }> { +export async function previewTicket(id: number, data: { status?: string }): Promise<{ data: PreviewResult | null; error: string | null }> { return request(`/tickets/${id}/preview`, { method: "POST", body: JSON.stringify(data) }); } -export async function getTicketTransactions(id: string): Promise<{ data: Transaction[] | null; error: string | null }> { +export async function getTicketTransactions(id: number): Promise<{ data: Transaction[] | null; error: string | null }> { return request(`/tickets/${id}/transactions`); } diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 7ca36c4..af31a90 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1,5 +1,5 @@ export interface Ticket { - id: string; + id: number; subject: string; queue_id: string; status: string; @@ -21,7 +21,7 @@ export interface Queue { export interface Transaction { id: string; - ticket_id: string; + ticket_id: number; transaction_type: string; field: string | null; old_value: string | null; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index bd0c391..ab4278f 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -4,3 +4,7 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function formatTicketId(id: number): string { + return `TKT-${String(id).padStart(4, "0")}` +}