diff --git a/drizzle/migrations/0005_spotty_leader.sql b/drizzle/migrations/0005_spotty_leader.sql new file mode 100644 index 0000000..413127a --- /dev/null +++ b/drizzle/migrations/0005_spotty_leader.sql @@ -0,0 +1,19 @@ +CREATE TABLE "team_members" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "team_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + CONSTRAINT "team_members_team_id_user_id_unique" UNIQUE("team_id","user_id") +); +--> statement-breakpoint +CREATE TABLE "teams" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "description" text, + "created_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "teams_name_unique" UNIQUE("name") +); +--> statement-breakpoint +ALTER TABLE "dashboards" ADD COLUMN "team_id" uuid;--> statement-breakpoint +ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/meta/0005_snapshot.json b/drizzle/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..943692a --- /dev/null +++ b/drizzle/migrations/meta/0005_snapshot.json @@ -0,0 +1,1291 @@ +{ + "id": "aa46da36-6338-4ee0-bafa-77f0e4cb305e", + "prevId": "c5dd1071-68a3-4180-b846-9c7cdd3c1af0", + "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()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "custom_fields_key_unique": { + "name": "custom_fields_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_widgets": { + "name": "dashboard_widgets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "view_id": { + "name": "view_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "widget_type": { + "name": "widget_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"x\":0,\"y\":0,\"w\":4,\"h\":2}'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_widgets_dashboard_id_dashboards_id_fk": { + "name": "dashboard_widgets_dashboard_id_dashboards_id_fk", + "tableFrom": "dashboard_widgets", + "tableTo": "dashboards", + "columnsFrom": [ + "dashboard_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_widgets_view_id_views_id_fk": { + "name": "dashboard_widgets_view_id_views_id_fk", + "tableFrom": "dashboard_widgets", + "tableTo": "views", + "columnsFrom": [ + "view_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "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 + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboards_team_id_teams_id_fk": { + "name": "dashboards_team_id_teams_id_fk", + "tableFrom": "dashboards", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_members_team_id_user_id_unique": { + "name": "team_members_team_id_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_name_unique": { + "name": "teams_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "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 + }, + "public.views": { + "name": "views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filters": { + "name": "filters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'updated'" + }, + "columns": { + "name": "columns", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "creator_id": { + "name": "creator_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": { + "views_creator_id_users_id_fk": { + "name": "views_creator_id_users_id_fk", + "tableFrom": "views", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 5f0d506..37c61ad 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1780996807814, "tag": "0004_sturdy_natasha_romanoff", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1781004398567, + "tag": "0005_spotty_leader", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 84ff23c..50fa477 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,5 +1,5 @@ import { pgTable, uuid, text, jsonb, integer, boolean, timestamp, unique, index } from 'drizzle-orm/pg-core'; -import { sql } from 'drizzle-orm'; +import { relations, sql } from 'drizzle-orm'; export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), @@ -124,10 +124,35 @@ export const views = pgTable('views', { created_at: timestamp('created_at', { withTimezone: true }).defaultNow(), }); +export const teams = pgTable('teams', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull().unique(), + description: text('description'), + created_at: timestamp('created_at', { withTimezone: true }).defaultNow(), +}); + +export const teamsRelations = relations(teams, ({ many }) => ({ + members: many(teamMembers), +})); + +export const teamMembers = pgTable('team_members', { + id: uuid('id').primaryKey().defaultRandom(), + team_id: uuid('team_id').notNull().references(() => teams.id, { onDelete: 'cascade' }), + user_id: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), +}, (table) => ({ + uniqueMember: unique('team_members_team_id_user_id_unique').on(table.team_id, table.user_id), +})); + +export const teamMembersRelations = relations(teamMembers, ({ one }) => ({ + team: one(teams, { fields: [teamMembers.team_id], references: [teams.id] }), + user: one(users, { fields: [teamMembers.user_id], references: [users.id] }), +})); + export const dashboards = pgTable('dashboards', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), description: text('description'), + team_id: uuid('team_id').references(() => teams.id, { onDelete: 'set null' }), layout: jsonb('layout').default('[]'), is_default: boolean('is_default').default(false), created_at: timestamp('created_at', { withTimezone: true }).defaultNow(), diff --git a/src/index.ts b/src/index.ts index b371e2d..18ff469 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { createUsersRouter } from './routes/users.ts'; import { createTemplatesRouter } from './routes/templates.ts'; import { createViewsRouter } from './routes/views.ts'; import { createDashboardsRouter } from './routes/dashboards.ts'; +import { createTeamsRouter } from './routes/teams.ts'; let db: Db | null = null; @@ -39,6 +40,7 @@ app.route('/users', createUsersRouter(getDb())); app.route('/templates', createTemplatesRouter(getDb())); app.route('/views', createViewsRouter(getDb())); app.route('/dashboards', createDashboardsRouter(getDb())); +app.route('/teams', createTeamsRouter(getDb())); export default app; export { app }; diff --git a/src/routes/dashboards.ts b/src/routes/dashboards.ts index ef3aa38..50ba7cd 100644 --- a/src/routes/dashboards.ts +++ b/src/routes/dashboards.ts @@ -42,6 +42,7 @@ export function createDashboardsRouter(db: Db): Hono { const [dashboard] = await db.insert(dashboards).values({ name, description: body.description ?? null, + team_id: body.team_id || null, layout: body.layout ?? [], is_default: body.is_default ?? false, }).returning(); @@ -87,9 +88,9 @@ export function createDashboardsRouter(db: Db): Hono { if (body.name !== undefined) updateData.name = String(body.name).trim(); if (body.description !== undefined) updateData.description = body.description ?? null; if (body.layout !== undefined) updateData.layout = body.layout; + if (body.team_id !== undefined) updateData.team_id = body.team_id || null; if (body.is_default !== undefined) { updateData.is_default = body.is_default; - // If setting this as default, unset others if (body.is_default) { await db.update(dashboards) .set({ is_default: false }) diff --git a/src/routes/teams.ts b/src/routes/teams.ts new file mode 100644 index 0000000..4129c37 --- /dev/null +++ b/src/routes/teams.ts @@ -0,0 +1,98 @@ +import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; +import { and, asc, eq, inArray } from 'drizzle-orm'; +import type { Db } from '../db/index.ts'; +import { teams, teamMembers, users, dashboards } from '../db/schema.ts'; + +export function createTeamsRouter(db: Db): Hono { + const router = new Hono(); + + // GET /teams — list all with member details + router.get('/', async (c) => { + const allTeams = await db.query.teams.findMany({ + orderBy: asc(teams.name), + }); + + const result = await Promise.all(allTeams.map(async (team) => { + const members = await db.query.teamMembers.findMany({ + where: eq(teamMembers.team_id, team.id), + }); + const memberUsers = members.length > 0 + ? await db.query.users.findMany({ + where: (table, { inArray }) => inArray(table.id, members.map((m) => m.user_id)), + }) + : []; + return { ...team, members: memberUsers }; + })); + + return c.json(result); + }); + + // POST /teams + router.post('/', async (c) => { + const body = await c.req.json(); + const name = String(body.name ?? '').trim(); + if (!name) throw new HTTPException(400, { message: 'name is required' }); + + const [team] = await db.insert(teams).values({ + name, + description: body.description ?? null, + }).returning(); + if (!team) throw new HTTPException(500, { message: 'Failed to create team' }); + return c.json(team, 201); + }); + + // PATCH /teams/:id + router.patch('/:id', async (c) => { + const id = c.req.param('id'); + const body = await c.req.json(); + const existing = await db.query.teams.findFirst({ where: eq(teams.id, id) }); + if (!existing) throw new HTTPException(404, { message: 'Team not found' }); + + const updateData: Partial = {}; + if (body.name !== undefined) updateData.name = String(body.name).trim(); + if (body.description !== undefined) updateData.description = body.description ?? null; + + const [updated] = await db.update(teams).set(updateData).where(eq(teams.id, id)).returning(); + return c.json(updated); + }); + + // DELETE /teams/:id + router.delete('/:id', async (c) => { + const id = c.req.param('id'); + const existing = await db.query.teams.findFirst({ where: eq(teams.id, id) }); + if (!existing) throw new HTTPException(404, { message: 'Team not found' }); + await db.delete(teams).where(eq(teams.id, id)); + return c.json({ ok: true }); + }); + + // POST /teams/:id/members — add member + router.post('/:id/members', async (c) => { + const teamId = c.req.param('id'); + const body = await c.req.json(); + const userId = String(body.user_id ?? '').trim(); + if (!userId) throw new HTTPException(400, { message: 'user_id is required' }); + + const team = await db.query.teams.findFirst({ where: eq(teams.id, teamId) }); + if (!team) throw new HTTPException(404, { message: 'Team not found' }); + + const [member] = await db.insert(teamMembers).values({ + team_id: teamId, + user_id: userId, + }).returning(); + if (!member) throw new HTTPException(500, { message: 'Failed to add member' }); + return c.json(member, 201); + }); + + // DELETE /teams/:id/members/:userId + router.delete('/:id/members/:userId', async (c) => { + const teamId = c.req.param('id'); + const userId = c.req.param('userId'); + await db.delete(teamMembers).where( + and(eq(teamMembers.team_id, teamId), eq(teamMembers.user_id, userId)) + ); + return c.json({ ok: true }); + }); + + return router; +} diff --git a/web/src/app/admin/page-content.tsx b/web/src/app/admin/page-content.tsx index c7d05c9..f468c91 100644 --- a/web/src/app/admin/page-content.tsx +++ b/web/src/app/admin/page-content.tsx @@ -65,8 +65,14 @@ import { createUser, updateUser, deleteUser, + getTeams, + createTeam, + updateTeam, + deleteTeam, + addTeamMember, + removeTeamMember, } from "@/lib/api"; -import type { Queue, Ticket, Lifecycle, LifecycleDefinition, Scrip, Template, CustomField, QueueCustomField, TemplatePreview, User } from "@/lib/types"; +import type { Queue, Ticket, Lifecycle, LifecycleDefinition, Scrip, Template, CustomField, QueueCustomField, TemplatePreview, User, Team } from "@/lib/types"; import { cn } from "@/lib/utils"; function AdminHeader() { @@ -158,6 +164,10 @@ export default function AdminPage() { Users + + + Teams +
@@ -179,6 +189,9 @@ export default function AdminPage() { + + +
@@ -2160,6 +2173,194 @@ function UsersTab() { ); } +function TeamsTab() { + const [teams, setTeams] = useState([]); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [editingId, setEditingId] = useState(null); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const [deletingId, setDeletingId] = useState(null); + const [addingMember, setAddingMember] = useState(null); // team id being managed + + const fetchData = useCallback(async () => { + setLoading(true); + setError(null); + const [teamsRes, usersRes] = await Promise.all([getTeams(), getUsers()]); + if (teamsRes.error) setError(teamsRes.error); + else setTeams(teamsRes.data ?? []); + if (usersRes.error) setError((prev) => prev || usersRes.error); + else setUsers(usersRes.data ?? []); + setLoading(false); + }, []); + + useEffect(() => { void Promise.resolve().then(() => fetchData()); }, [fetchData]); + + const resetForm = () => { + setEditingId(null); + setName(""); + setDescription(""); + setSaveError(null); + }; + + const handleSave = async () => { + if (!name.trim()) return; + setSaving(true); + setSaveError(null); + const payload = { name: name.trim(), description: description.trim() || undefined }; + const { error } = editingId + ? await updateTeam(editingId, payload) + : await createTeam(payload); + setSaving(false); + if (error) { setSaveError(error); return; } + resetForm(); + await fetchData(); + }; + + const handleDelete = async (id: string) => { + setDeletingId(id); + await deleteTeam(id); + if (editingId === id) resetForm(); + await fetchData(); + setDeletingId(null); + }; + + const handleAddMember = async (teamId: string, userId: string) => { + await addTeamMember(teamId, userId); + await fetchData(); + }; + + const handleRemoveMember = async (teamId: string, userId: string) => { + await removeTeamMember(teamId, userId); + await fetchData(); + }; + + const selectedTeam = editingId ? teams.find((t) => t.id === editingId) : null; + + return ( +
+
+
+

Teams ({teams.length})

+

Organize users into teams. Assign dashboards to teams.

+
+ +
+ + {loading ? : ( +
+ +
+
+
+ {editingId ? "Editing team" : "New team"} +
+

{name.trim() || "Untitled"}

+
+
+
+ + setName(e.target.value)} /> +
+
+ + setDescription(e.target.value)} /> +
+ {saveError &&
{saveError}
} +
+ + +
+ + {selectedTeam && ( +
+
+

Members

+
+
+ {(selectedTeam.members ?? []).map((user) => ( +
+
+ {user.username} + {user.email ?? "no email"} +
+ +
+ ))} + {(selectedTeam.members ?? []).length === 0 && ( +

No members yet.

+ )} + +
+
+ )} +
+
+
+ )} +
+ ); +} + function CustomFieldsTab() { const [fields, setFields] = useState([]); const [queues, setQueues] = useState([]); diff --git a/web/src/components/app-shell.tsx b/web/src/components/app-shell.tsx index b5e4fad..7388da5 100644 --- a/web/src/components/app-shell.tsx +++ b/web/src/components/app-shell.tsx @@ -15,8 +15,8 @@ import { PanelLeftIcon, CommandIcon, } from "lucide-react"; -import { getTickets, getQueues, getViews, getDashboards, getUsers, createDashboard } from "@/lib/api"; -import type { Dashboard, Queue, SavedView, User } from "@/lib/types"; +import { getTickets, getQueues, getViews, getDashboards, getUsers, getTeams, createDashboard } from "@/lib/api"; +import type { Dashboard, Queue, SavedView, Team, User } from "@/lib/types"; import { CommandPalette } from "@/components/command-palette"; import { ThemeToggle } from "@/components/theme-toggle"; import { cn } from "@/lib/utils"; @@ -100,48 +100,58 @@ function SidebarNav() { const [addingDashboard, setAddingDashboard] = useState(false); useEffect(() => { - // Find current user and compute view counts - Promise.all([getTickets(), getUsers()]).then(([ticketRes, userRes]) => { + async function load() { + // Find current user + const [ticketRes, userRes] = await Promise.all([getTickets(), getUsers()]); const data = ticketRes.data; const users = userRes.data ?? []; const currentUser = users.find((u) => u.username !== 'system') ?? users[0] ?? null; - if (currentUser) setCurrentUserId(currentUser.id); + const myId = currentUser?.id ?? null; + setCurrentUserId(myId); if (data) { - const myId = currentUser?.id; const now = Date.now(); const week = 7 * 24 * 60 * 60 * 1000; setCounts({ all: data.length, my: myId ? data.filter((t) => t.owner_id === myId).length : 0, unassigned: data.filter((t) => !t.owner_id).length, - recent: data.filter( - (t) => new Date(t.updated_at).getTime() > now - week - ).length, + recent: data.filter((t) => new Date(t.updated_at).getTime() > now - week).length, }); } - }); - getQueues().then(({ data }) => { - if (data) { - Promise.all( - data.map((q) => + // Queues + const queueRes = await getQueues(); + if (queueRes.data) { + const qs = await Promise.all( + queueRes.data.map((q) => getTickets({ queue_id: q.id }).then(({ data: tickets }) => ({ ...q, count: tickets?.length ?? 0, })) ) - ).then(setQueues); + ); + setQueues(qs); } - }); - getViews().then(({ data }) => { - if (data) setSavedViews(data); - }); + // Views + const viewRes = await getViews(); + if (viewRes.data) setSavedViews(viewRes.data); - getDashboards().then(({ data }) => { - if (data) setDashboards(data); - }); + // Dashboards scoped to user's teams + const [dashRes, teamRes] = await Promise.all([getDashboards(), getTeams()]); + const allDashboards = dashRes.data ?? []; + const allTeams = teamRes.data ?? []; + const userTeams = allTeams.filter((t) => + (t.members ?? []).some((m) => m.id === myId) + ); + const teamIds = new Set(userTeams.map((t) => t.id)); + const visible = allDashboards.filter((d) => + !d.team_id || teamIds.has(d.team_id) + ); + setDashboards(visible); + } + void load(); }, []); const collapsed = useSidebarCollapsed(); diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 5bcbf6c..a2c2226 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -4,6 +4,7 @@ import type { Dashboard, DashboardWidget, WidgetData, + Team, User, Transaction, SavedView, @@ -335,3 +336,27 @@ export async function deleteWidget(dashboardId: string, widgetId: string): Promi export async function getWidgetData(dashboardId: string, widgetId: string): Promise<{ data: WidgetData | null; error: string | null }> { return request(`/dashboards/${dashboardId}/widgets/${widgetId}/data`); } + +export async function getTeams(): Promise<{ data: Team[] | null; error: string | null }> { + return request("/teams"); +} + +export async function createTeam(data: { name: string; description?: string | null }): Promise<{ data: Team | null; error: string | null }> { + return request("/teams", { method: "POST", body: JSON.stringify(data) }); +} + +export async function updateTeam(id: string, data: { name?: string; description?: string | null }): Promise<{ data: Team | null; error: string | null }> { + return request(`/teams/${id}`, { method: "PATCH", body: JSON.stringify(data) }); +} + +export async function deleteTeam(id: string): Promise<{ data: { ok: boolean } | null; error: string | null }> { + return request<{ ok: boolean }>(`/teams/${id}`, { method: "DELETE" }); +} + +export async function addTeamMember(teamId: string, userId: string): Promise<{ data: unknown | null; error: string | null }> { + return request(`/teams/${teamId}/members`, { method: "POST", body: JSON.stringify({ user_id: userId }) }); +} + +export async function removeTeamMember(teamId: string, userId: string): Promise<{ data: { ok: boolean } | null; error: string | null }> { + return request<{ ok: boolean }>(`/teams/${teamId}/members/${userId}`, { method: "DELETE" }); +} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 3ff069d..10cfb77 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -146,10 +146,19 @@ export interface SavedView { created_at: string; } +export interface Team { + id: string; + name: string; + description: string | null; + created_at: string; + members?: User[]; +} + export interface Dashboard { id: string; name: string; description: string | null; + team_id: string | null; layout: unknown[]; is_default: boolean; created_at: string;