Change ticket IDs from UUID to sequential integers

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
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-07 23:23:05 +02:00
parent 7da52dfff6
commit 04b4e28d21
12 changed files with 979 additions and 22 deletions

View File

@@ -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");

View File

@@ -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": {}
}
}

View File

@@ -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
}
]
}

View File

@@ -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) => ({

View File

@@ -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),

View File

@@ -13,7 +13,7 @@ export interface ActionPayload {
scripName: string;
actionType: string;
actionConfig: Record<string, unknown>;
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;

View File

@@ -35,7 +35,7 @@ export class ScripEngine {
}
async prepare(
ticketId: string,
ticketId: number,
transactions: Transaction[],
): Promise<PreparedScrip[]> {
const ticketRecord = await this.db.query.tickets.findFirst({

View File

@@ -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<string, string> = {
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 (

View File

@@ -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<string, string> = {
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<Ticket | null>(null);
@@ -328,7 +329,7 @@ export default function TicketDetailPage({
{ticket.subject}
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
<span className="font-mono">{ticket.id.slice(0, 8)}</span> · {queue?.name || ticket.queue_id}
<span className="font-mono">{formatTicketId(ticket.id)}</span> · {queue?.name || ticket.queue_id}
</p>
</div>

View File

@@ -36,7 +36,7 @@ export async function getTickets(params?: { queue_id?: string; status?: string }
return request<Ticket[]>(`/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<Ticket>(`/tickets/${id}`);
}
@@ -44,15 +44,15 @@ export async function createTicket(data: { subject: string; queue_id: string }):
return request<Ticket>("/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<UpdateResult>(`/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<PreviewResult>(`/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<Transaction[]>(`/tickets/${id}/transactions`);
}

View File

@@ -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;

View File

@@ -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")}`
}