From dc9217f3b6548e95212069fa005da16c0e981ad1 Mon Sep 17 00:00:00 2001 From: grabowski Date: Mon, 29 Jun 2026 16:32:21 +0700 Subject: [PATCH] Add Parts section: consumable stock inventory (caps, resistors, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New parts table with category, MPN, value, voltage, tolerance, power, mounting (THT/SMT), package, quantity, location - /parts list (table view, category + mounting filters, colour-coded stock) - /parts/new and /parts/[id]/edit with dynamic fields per category - /parts/[id] detail with ± stock adjustment - Sidebar nav entry and count badge - Drizzle migration generated (run db:push to apply) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../migrations/0000_clear_sister_grimm.sql | 327 +++ drizzle/migrations/meta/0000_snapshot.json | 2403 +++++++++++++++++ drizzle/migrations/meta/_journal.json | 13 + src/lib/components/layout/Sidebar.svelte | 8 +- src/lib/constants.ts | 17 + src/lib/server/db/schema.ts | 28 + src/routes/(app)/+layout.server.ts | 7 +- src/routes/(app)/parts/+page.server.ts | 61 + src/routes/(app)/parts/+page.svelte | 150 + src/routes/(app)/parts/[id]/+page.server.ts | 59 + src/routes/(app)/parts/[id]/+page.svelte | 138 + .../(app)/parts/[id]/edit/+page.server.ts | 74 + src/routes/(app)/parts/[id]/edit/+page.svelte | 153 ++ src/routes/(app)/parts/new/+page.server.ts | 64 + src/routes/(app)/parts/new/+page.svelte | 162 ++ 15 files changed, 3661 insertions(+), 3 deletions(-) create mode 100644 drizzle/migrations/0000_clear_sister_grimm.sql create mode 100644 drizzle/migrations/meta/0000_snapshot.json create mode 100644 drizzle/migrations/meta/_journal.json create mode 100644 src/routes/(app)/parts/+page.server.ts create mode 100644 src/routes/(app)/parts/+page.svelte create mode 100644 src/routes/(app)/parts/[id]/+page.server.ts create mode 100644 src/routes/(app)/parts/[id]/+page.svelte create mode 100644 src/routes/(app)/parts/[id]/edit/+page.server.ts create mode 100644 src/routes/(app)/parts/[id]/edit/+page.svelte create mode 100644 src/routes/(app)/parts/new/+page.server.ts create mode 100644 src/routes/(app)/parts/new/+page.svelte diff --git a/drizzle/migrations/0000_clear_sister_grimm.sql b/drizzle/migrations/0000_clear_sister_grimm.sql new file mode 100644 index 0000000..a4e5f87 --- /dev/null +++ b/drizzle/migrations/0000_clear_sister_grimm.sql @@ -0,0 +1,327 @@ +CREATE TABLE "checklist_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "checklist_id" uuid NOT NULL, + "text" text NOT NULL, + "item_type" text DEFAULT 'checkbox' NOT NULL, + "unit" text, + "checked" boolean DEFAULT false NOT NULL, + "value" text, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "checklist_templates" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "description" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "component_documents" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "component_id" uuid NOT NULL, + "file_path" text NOT NULL, + "original_filename" text NOT NULL, + "file_type" text, + "description" text, + "uploaded_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "component_images" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "component_id" uuid NOT NULL, + "file_path" text NOT NULL, + "thumbnail_path" text, + "caption" text, + "uploaded_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "component_instances" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "component_id" uuid NOT NULL, + "instance_number" integer DEFAULT 1 NOT NULL, + "serial_number" text, + "condition" text DEFAULT 'Working' NOT NULL, + "firmware_version" text, + "notes" text, + "current_device_id" uuid, + "location_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "instances_condition_check" CHECK ("component_instances"."condition" IN ('Working', 'Faulty', 'Unknown', 'Refurbished')) +); +--> statement-breakpoint +CREATE TABLE "components" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "component_type" text NOT NULL, + "brand" text, + "part_number" text, + "specs" text, + "notes" text, + "default_condition" text DEFAULT 'Working' NOT NULL, + "default_firmware_version" text, + "default_location_id" uuid, + "disabled" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "computer_details" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "os_version" text, + "firmware_version" text, + "installed_software" text, + CONSTRAINT "computer_details_device_id_unique" UNIQUE("device_id") +); +--> statement-breakpoint +CREATE TABLE "device_checklists" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "title" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "device_documents" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "file_path" text NOT NULL, + "original_filename" text NOT NULL, + "file_type" text, + "description" text, + "uploaded_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "device_images" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "file_path" text NOT NULL, + "thumbnail_path" text, + "caption" text, + "sort_order" integer DEFAULT 0, + "uploaded_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "device_log" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "type" text NOT NULL, + "description" text NOT NULL, + "condition_after" text, + "performed_by" text, + "performed_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "device_log_type_check" CHECK ("device_log"."type" IN ('repair', 'inspection', 'cleaning', 'modification', 'diagnostic', 'recap', 'other')) +); +--> statement-breakpoint +CREATE TABLE "device_tags" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "device_id" uuid NOT NULL, + "tag_uid" text, + "payload_url" text NOT NULL, + "written_by" text, + "written_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "devices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "category" text NOT NULL, + "brand" text, + "model" text, + "serial_number" text, + "year" integer, + "condition" text DEFAULT 'Waiting to be Tested' NOT NULL, + "voltage" text, + "frequency" text, + "origin" text, + "fault_description" text, + "repair_notes" text, + "location_id" uuid, + "initial_condition" text, + "general_notes" text, + "disabled" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "devices_category_check" CHECK ("devices"."category" IN ('Computer', 'Audio Equipment', 'Peripheral', 'Other')), + CONSTRAINT "devices_condition_check" CHECK ("devices"."condition" IN ('Working', 'In Repair', 'Waiting for Repair', 'Waiting to be Tested', 'Unrepairable')) +); +--> statement-breakpoint +CREATE TABLE "feature_requests" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "description" text, + "status" text DEFAULT 'open' NOT NULL, + "votes" integer DEFAULT 0 NOT NULL, + "created_by" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "installation_log" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "instance_id" uuid NOT NULL, + "device_id" uuid NOT NULL, + "action" text NOT NULL, + "performed_by" text, + "notes" text, + "performed_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "install_log_action_check" CHECK ("installation_log"."action" IN ('installed', 'removed', 'swapped')) +); +--> statement-breakpoint +CREATE TABLE "locations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "description" text, + "parent_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "parts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text, + "category" text NOT NULL, + "mpn" text, + "value" text, + "voltage" text, + "tolerance" text, + "power" text, + "mounting" text, + "package" text, + "quantity" integer DEFAULT 0 NOT NULL, + "unit" text DEFAULT 'pcs' NOT NULL, + "location_id" uuid, + "notes" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "template_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "template_id" uuid NOT NULL, + "text" text NOT NULL, + "item_type" text DEFAULT 'checkbox' NOT NULL, + "unit" text, + "sort_order" integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE "todos" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "description" text, + "status" text DEFAULT 'todo' NOT NULL, + "priority" integer DEFAULT 2 NOT NULL, + "device_id" uuid, + "due_date" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "todos_status_check" CHECK ("todos"."status" IN ('todo', 'in_progress', 'done')), + CONSTRAINT "todos_priority_check" CHECK ("todos"."priority" IN (0, 1, 2, 3)) +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "display_name" text, + "password_hash" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "wiki_categories" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "description" text, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "wiki_categories_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "wiki_page_tags" ( + "page_id" uuid NOT NULL, + "tag_id" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE "wiki_pages" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "slug" text NOT NULL, + "content" text NOT NULL, + "category_id" uuid, + "created_by" text, + "updated_by" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "wiki_pages_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "wiki_tags" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + CONSTRAINT "wiki_tags_name_unique" UNIQUE("name") +); +--> statement-breakpoint +ALTER TABLE "checklist_items" ADD CONSTRAINT "checklist_items_checklist_id_device_checklists_id_fk" FOREIGN KEY ("checklist_id") REFERENCES "public"."device_checklists"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "component_documents" ADD CONSTRAINT "component_documents_component_id_components_id_fk" FOREIGN KEY ("component_id") REFERENCES "public"."components"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "component_images" ADD CONSTRAINT "component_images_component_id_components_id_fk" FOREIGN KEY ("component_id") REFERENCES "public"."components"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "component_instances" ADD CONSTRAINT "component_instances_component_id_components_id_fk" FOREIGN KEY ("component_id") REFERENCES "public"."components"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "component_instances" ADD CONSTRAINT "component_instances_current_device_id_devices_id_fk" FOREIGN KEY ("current_device_id") REFERENCES "public"."devices"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "component_instances" ADD CONSTRAINT "component_instances_location_id_locations_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "components" ADD CONSTRAINT "components_default_location_id_locations_id_fk" FOREIGN KEY ("default_location_id") REFERENCES "public"."locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "computer_details" ADD CONSTRAINT "computer_details_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_checklists" ADD CONSTRAINT "device_checklists_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_documents" ADD CONSTRAINT "device_documents_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_images" ADD CONSTRAINT "device_images_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_log" ADD CONSTRAINT "device_log_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_tags" ADD CONSTRAINT "device_tags_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "devices" ADD CONSTRAINT "devices_location_id_locations_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "installation_log" ADD CONSTRAINT "installation_log_instance_id_component_instances_id_fk" FOREIGN KEY ("instance_id") REFERENCES "public"."component_instances"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "installation_log" ADD CONSTRAINT "installation_log_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "locations" ADD CONSTRAINT "locations_parent_id_locations_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "parts" ADD CONSTRAINT "parts_location_id_locations_id_fk" FOREIGN KEY ("location_id") REFERENCES "public"."locations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "template_items" ADD CONSTRAINT "template_items_template_id_checklist_templates_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."checklist_templates"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "todos" ADD CONSTRAINT "todos_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "wiki_page_tags" ADD CONSTRAINT "wiki_page_tags_page_id_wiki_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."wiki_pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "wiki_page_tags" ADD CONSTRAINT "wiki_page_tags_tag_id_wiki_tags_id_fk" FOREIGN KEY ("tag_id") REFERENCES "public"."wiki_tags"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_category_id_wiki_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."wiki_categories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "checklist_items_checklist_idx" ON "checklist_items" USING btree ("checklist_id");--> statement-breakpoint +CREATE INDEX "component_documents_component_idx" ON "component_documents" USING btree ("component_id");--> statement-breakpoint +CREATE INDEX "component_images_component_idx" ON "component_images" USING btree ("component_id");--> statement-breakpoint +CREATE INDEX "instances_component_idx" ON "component_instances" USING btree ("component_id");--> statement-breakpoint +CREATE INDEX "instances_device_idx" ON "component_instances" USING btree ("current_device_id");--> statement-breakpoint +CREATE INDEX "instances_location_idx" ON "component_instances" USING btree ("location_id");--> statement-breakpoint +CREATE INDEX "components_type_idx" ON "components" USING btree ("component_type");--> statement-breakpoint +CREATE INDEX "device_checklists_device_idx" ON "device_checklists" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "device_documents_device_idx" ON "device_documents" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "device_images_device_idx" ON "device_images" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "device_log_device_idx" ON "device_log" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "device_log_date_idx" ON "device_log" USING btree ("performed_at");--> statement-breakpoint +CREATE INDEX "device_tags_device_idx" ON "device_tags" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "device_tags_written_at_idx" ON "device_tags" USING btree ("written_at");--> statement-breakpoint +CREATE INDEX "devices_category_idx" ON "devices" USING btree ("category");--> statement-breakpoint +CREATE INDEX "devices_condition_idx" ON "devices" USING btree ("condition");--> statement-breakpoint +CREATE INDEX "devices_location_idx" ON "devices" USING btree ("location_id");--> statement-breakpoint +CREATE INDEX "install_log_instance_idx" ON "installation_log" USING btree ("instance_id");--> statement-breakpoint +CREATE INDEX "install_log_device_idx" ON "installation_log" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "install_log_date_idx" ON "installation_log" USING btree ("performed_at");--> statement-breakpoint +CREATE INDEX "parts_category_idx" ON "parts" USING btree ("category");--> statement-breakpoint +CREATE INDEX "parts_location_idx" ON "parts" USING btree ("location_id");--> statement-breakpoint +CREATE INDEX "template_items_template_idx" ON "template_items" USING btree ("template_id");--> statement-breakpoint +CREATE INDEX "todos_status_idx" ON "todos" USING btree ("status");--> statement-breakpoint +CREATE INDEX "todos_priority_idx" ON "todos" USING btree ("priority");--> statement-breakpoint +CREATE INDEX "todos_device_idx" ON "todos" USING btree ("device_id");--> statement-breakpoint +CREATE INDEX "wiki_page_tags_page_idx" ON "wiki_page_tags" USING btree ("page_id");--> statement-breakpoint +CREATE INDEX "wiki_page_tags_tag_idx" ON "wiki_page_tags" USING btree ("tag_id");--> statement-breakpoint +CREATE INDEX "wiki_pages_category_idx" ON "wiki_pages" USING btree ("category_id");--> statement-breakpoint +CREATE INDEX "wiki_pages_slug_idx" ON "wiki_pages" USING btree ("slug"); \ No newline at end of file diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..1aed8c3 --- /dev/null +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -0,0 +1,2403 @@ +{ + "id": "cebee925-8ad2-4acf-8d4d-7dfe0e514aa6", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.checklist_items": { + "name": "checklist_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checklist_id": { + "name": "checklist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_type": { + "name": "item_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'checkbox'" + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checked": { + "name": "checked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "checklist_items_checklist_idx": { + "name": "checklist_items_checklist_idx", + "columns": [ + { + "expression": "checklist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "checklist_items_checklist_id_device_checklists_id_fk": { + "name": "checklist_items_checklist_id_device_checklists_id_fk", + "tableFrom": "checklist_items", + "tableTo": "device_checklists", + "columnsFrom": [ + "checklist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checklist_templates": { + "name": "checklist_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "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": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_documents": { + "name": "component_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "component_id": { + "name": "component_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "component_documents_component_idx": { + "name": "component_documents_component_idx", + "columns": [ + { + "expression": "component_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "component_documents_component_id_components_id_fk": { + "name": "component_documents_component_id_components_id_fk", + "tableFrom": "component_documents", + "tableTo": "components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_images": { + "name": "component_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "component_id": { + "name": "component_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_path": { + "name": "thumbnail_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "component_images_component_idx": { + "name": "component_images_component_idx", + "columns": [ + { + "expression": "component_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "component_images_component_id_components_id_fk": { + "name": "component_images_component_id_components_id_fk", + "tableFrom": "component_images", + "tableTo": "components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_instances": { + "name": "component_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "component_id": { + "name": "component_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_number": { + "name": "instance_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "serial_number": { + "name": "serial_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Working'" + }, + "firmware_version": { + "name": "firmware_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_device_id": { + "name": "current_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instances_component_idx": { + "name": "instances_component_idx", + "columns": [ + { + "expression": "component_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instances_device_idx": { + "name": "instances_device_idx", + "columns": [ + { + "expression": "current_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instances_location_idx": { + "name": "instances_location_idx", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "component_instances_component_id_components_id_fk": { + "name": "component_instances_component_id_components_id_fk", + "tableFrom": "component_instances", + "tableTo": "components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "component_instances_current_device_id_devices_id_fk": { + "name": "component_instances_current_device_id_devices_id_fk", + "tableFrom": "component_instances", + "tableTo": "devices", + "columnsFrom": [ + "current_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "component_instances_location_id_locations_id_fk": { + "name": "component_instances_location_id_locations_id_fk", + "tableFrom": "component_instances", + "tableTo": "locations", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "instances_condition_check": { + "name": "instances_condition_check", + "value": "\"component_instances\".\"condition\" IN ('Working', 'Faulty', 'Unknown', 'Refurbished')" + } + }, + "isRLSEnabled": false + }, + "public.components": { + "name": "components", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "component_type": { + "name": "component_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "part_number": { + "name": "part_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "specs": { + "name": "specs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_condition": { + "name": "default_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Working'" + }, + "default_firmware_version": { + "name": "default_firmware_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_location_id": { + "name": "default_location_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "components_type_idx": { + "name": "components_type_idx", + "columns": [ + { + "expression": "component_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "components_default_location_id_locations_id_fk": { + "name": "components_default_location_id_locations_id_fk", + "tableFrom": "components", + "tableTo": "locations", + "columnsFrom": [ + "default_location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computer_details": { + "name": "computer_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "os_version": { + "name": "os_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "firmware_version": { + "name": "firmware_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_software": { + "name": "installed_software", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "computer_details_device_id_devices_id_fk": { + "name": "computer_details_device_id_devices_id_fk", + "tableFrom": "computer_details", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "computer_details_device_id_unique": { + "name": "computer_details_device_id_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_checklists": { + "name": "device_checklists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_checklists_device_idx": { + "name": "device_checklists_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_checklists_device_id_devices_id_fk": { + "name": "device_checklists_device_id_devices_id_fk", + "tableFrom": "device_checklists", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_documents": { + "name": "device_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_type": { + "name": "file_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_documents_device_idx": { + "name": "device_documents_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_documents_device_id_devices_id_fk": { + "name": "device_documents_device_id_devices_id_fk", + "tableFrom": "device_documents", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_images": { + "name": "device_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_path": { + "name": "thumbnail_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_images_device_idx": { + "name": "device_images_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_images_device_id_devices_id_fk": { + "name": "device_images_device_id_devices_id_fk", + "tableFrom": "device_images", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_log": { + "name": "device_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "condition_after": { + "name": "condition_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "performed_by": { + "name": "performed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "performed_at": { + "name": "performed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_log_device_idx": { + "name": "device_log_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_log_date_idx": { + "name": "device_log_date_idx", + "columns": [ + { + "expression": "performed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_log_device_id_devices_id_fk": { + "name": "device_log_device_id_devices_id_fk", + "tableFrom": "device_log", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "device_log_type_check": { + "name": "device_log_type_check", + "value": "\"device_log\".\"type\" IN ('repair', 'inspection', 'cleaning', 'modification', 'diagnostic', 'recap', 'other')" + } + }, + "isRLSEnabled": false + }, + "public.device_tags": { + "name": "device_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_uid": { + "name": "tag_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_url": { + "name": "payload_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "written_at": { + "name": "written_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_tags_device_idx": { + "name": "device_tags_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_tags_written_at_idx": { + "name": "device_tags_written_at_idx", + "columns": [ + { + "expression": "written_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_tags_device_id_devices_id_fk": { + "name": "device_tags_device_id_devices_id_fk", + "tableFrom": "device_tags", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serial_number": { + "name": "serial_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Waiting to be Tested'" + }, + "voltage": { + "name": "voltage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frequency": { + "name": "frequency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fault_description": { + "name": "fault_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repair_notes": { + "name": "repair_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "initial_condition": { + "name": "initial_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "general_notes": { + "name": "general_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "devices_category_idx": { + "name": "devices_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "devices_condition_idx": { + "name": "devices_condition_idx", + "columns": [ + { + "expression": "condition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "devices_location_idx": { + "name": "devices_location_idx", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "devices_location_id_locations_id_fk": { + "name": "devices_location_id_locations_id_fk", + "tableFrom": "devices", + "tableTo": "locations", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "devices_category_check": { + "name": "devices_category_check", + "value": "\"devices\".\"category\" IN ('Computer', 'Audio Equipment', 'Peripheral', 'Other')" + }, + "devices_condition_check": { + "name": "devices_condition_check", + "value": "\"devices\".\"condition\" IN ('Working', 'In Repair', 'Waiting for Repair', 'Waiting to be Tested', 'Unrepairable')" + } + }, + "isRLSEnabled": false + }, + "public.feature_requests": { + "name": "feature_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "votes": { + "name": "votes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installation_log": { + "name": "installation_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "performed_by": { + "name": "performed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "performed_at": { + "name": "performed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "install_log_instance_idx": { + "name": "install_log_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "install_log_device_idx": { + "name": "install_log_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "install_log_date_idx": { + "name": "install_log_date_idx", + "columns": [ + { + "expression": "performed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "installation_log_instance_id_component_instances_id_fk": { + "name": "installation_log_instance_id_component_instances_id_fk", + "tableFrom": "installation_log", + "tableTo": "component_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "installation_log_device_id_devices_id_fk": { + "name": "installation_log_device_id_devices_id_fk", + "tableFrom": "installation_log", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "install_log_action_check": { + "name": "install_log_action_check", + "value": "\"installation_log\".\"action\" IN ('installed', 'removed', 'swapped')" + } + }, + "isRLSEnabled": false + }, + "public.locations": { + "name": "locations", + "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 + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "locations_parent_id_locations_id_fk": { + "name": "locations_parent_id_locations_id_fk", + "tableFrom": "locations", + "tableTo": "locations", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parts": { + "name": "parts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mpn": { + "name": "mpn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voltage": { + "name": "voltage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tolerance": { + "name": "tolerance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "power": { + "name": "power", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mounting": { + "name": "mounting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package": { + "name": "package", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pcs'" + }, + "location_id": { + "name": "location_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parts_category_idx": { + "name": "parts_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parts_location_idx": { + "name": "parts_location_idx", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parts_location_id_locations_id_fk": { + "name": "parts_location_id_locations_id_fk", + "tableFrom": "parts", + "tableTo": "locations", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_items": { + "name": "template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_type": { + "name": "item_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'checkbox'" + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "template_items_template_idx": { + "name": "template_items_template_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_items_template_id_checklist_templates_id_fk": { + "name": "template_items_template_id_checklist_templates_id_fk", + "tableFrom": "template_items", + "tableTo": "checklist_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.todos": { + "name": "todos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "todos_status_idx": { + "name": "todos_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "todos_priority_idx": { + "name": "todos_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "todos_device_idx": { + "name": "todos_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "todos_device_id_devices_id_fk": { + "name": "todos_device_id_devices_id_fk", + "tableFrom": "todos", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "todos_status_check": { + "name": "todos_status_check", + "value": "\"todos\".\"status\" IN ('todo', 'in_progress', 'done')" + }, + "todos_priority_check": { + "name": "todos_priority_check", + "value": "\"todos\".\"priority\" IN (0, 1, 2, 3)" + } + }, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_categories": { + "name": "wiki_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wiki_categories_slug_unique": { + "name": "wiki_categories_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_page_tags": { + "name": "wiki_page_tags", + "schema": "", + "columns": { + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "wiki_page_tags_page_idx": { + "name": "wiki_page_tags_page_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_page_tags_tag_idx": { + "name": "wiki_page_tags_tag_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wiki_page_tags_page_id_wiki_pages_id_fk": { + "name": "wiki_page_tags_page_id_wiki_pages_id_fk", + "tableFrom": "wiki_page_tags", + "tableTo": "wiki_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "wiki_page_tags_tag_id_wiki_tags_id_fk": { + "name": "wiki_page_tags_tag_id_wiki_tags_id_fk", + "tableFrom": "wiki_page_tags", + "tableTo": "wiki_tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_pages": { + "name": "wiki_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "wiki_pages_category_idx": { + "name": "wiki_pages_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_pages_slug_idx": { + "name": "wiki_pages_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wiki_pages_category_id_wiki_categories_id_fk": { + "name": "wiki_pages_category_id_wiki_categories_id_fk", + "tableFrom": "wiki_pages", + "tableTo": "wiki_categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wiki_pages_slug_unique": { + "name": "wiki_pages_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_tags": { + "name": "wiki_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wiki_tags_name_unique": { + "name": "wiki_tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "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 new file mode 100644 index 0000000..7af1473 --- /dev/null +++ b/drizzle/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1782725202288, + "tag": "0000_clear_sister_grimm", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 3e37615..6133f39 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -4,7 +4,7 @@ interface Props { open: boolean; onToggle: () => void; - counts: { devices: number; components: number; needsRepair: number; openTodos: number }; + counts: { devices: number; components: number; parts: number; needsRepair: number; openTodos: number }; } let { open, onToggle, counts }: Props = $props(); @@ -27,6 +27,12 @@ badge: counts.components, icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z' }, + { + href: '/parts', + label: 'Parts', + badge: counts.parts || undefined, + icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4' + }, { href: '/installations', label: 'Install Log', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f500f16..1b7ef39 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -63,6 +63,23 @@ export const TODO_PRIORITIES = [ { value: 3, label: 'Low', color: 'gray' } ] as const; +export const PART_CATEGORIES = [ + 'Capacitor', + 'Resistor', + 'Transistor', + 'IC / Chip', + 'Diode', + 'LED', + 'Button / Switch', + 'Connector', + 'Fuse', + 'Inductor', + 'Crystal / Oscillator', + 'Other' +] as const; + +export type PartCategory = (typeof PART_CATEGORIES)[number]; + export const VOLTAGE_OPTIONS = [ '100-120V AC', '220-240V AC', diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 4606bc9..db08b5f 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -373,6 +373,34 @@ export const featureRequests = pgTable('feature_requests', { updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }); +// ─── Parts (consumable stock, no per-device tracking) ─────────── + +export const parts = pgTable( + 'parts', + { + id: uuid('id').defaultRandom().primaryKey(), + name: text('name'), + category: text('category').notNull(), + mpn: text('mpn'), + value: text('value'), + voltage: text('voltage'), + tolerance: text('tolerance'), + power: text('power'), + mounting: text('mounting'), + package: text('package'), + quantity: integer('quantity').notNull().default(0), + unit: text('unit').notNull().default('pcs'), + locationId: uuid('location_id').references(() => locations.id), + notes: text('notes'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() + }, + (table) => [ + index('parts_category_idx').on(table.category), + index('parts_location_idx').on(table.locationId) + ] +); + // ─── Wiki ─────────────────────────────────────────────────────────── export const wikiCategories = pgTable('wiki_categories', { diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index 0d50e90..55a2634 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -1,7 +1,7 @@ import type { LayoutServerLoad } from './$types'; import { redirect } from '@sveltejs/kit'; import { db } from '$lib/server/db/index.js'; -import { devices, components, componentInstances, todos } from '$lib/server/db/schema.js'; +import { devices, components, componentInstances, todos, parts } from '$lib/server/db/schema.js'; import { count, eq, or, and, ne } from 'drizzle-orm'; export const load: LayoutServerLoad = async ({ locals }) => { @@ -25,13 +25,16 @@ export const load: LayoutServerLoad = async ({ locals }) => { .from(todos) .where(ne(todos.status, 'done')); + const [partsCount] = await db.select({ value: count() }).from(parts); + return { user: locals.user, counts: { devices: deviceCount?.value ?? 0, components: componentCount?.value ?? 0, needsRepair: repairCount?.value ?? 0, - openTodos: openTodos?.value ?? 0 + openTodos: openTodos?.value ?? 0, + parts: partsCount?.value ?? 0 } }; }; diff --git a/src/routes/(app)/parts/+page.server.ts b/src/routes/(app)/parts/+page.server.ts new file mode 100644 index 0000000..c0121f3 --- /dev/null +++ b/src/routes/(app)/parts/+page.server.ts @@ -0,0 +1,61 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { parts, locations } from '$lib/server/db/schema.js'; +import { eq, ilike, or, and, count } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ url }) => { + const category = url.searchParams.get('category'); + const mounting = url.searchParams.get('mounting'); + const search = url.searchParams.get('q'); + const page = Math.max(1, Number(url.searchParams.get('page') ?? 1)); + const pageSize = 48; + + const conditions = []; + if (category) conditions.push(eq(parts.category, category)); + if (mounting) conditions.push(eq(parts.mounting, mounting)); + if (search) { + conditions.push( + or( + ilike(parts.name, `%${search}%`), + ilike(parts.mpn, `%${search}%`), + ilike(parts.value, `%${search}%`), + ilike(parts.package, `%${search}%`) + )! + ); + } + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const [totalResult] = await db.select({ value: count() }).from(parts).where(where); + + const partList = await db + .select({ + id: parts.id, + name: parts.name, + category: parts.category, + mpn: parts.mpn, + value: parts.value, + voltage: parts.voltage, + tolerance: parts.tolerance, + power: parts.power, + mounting: parts.mounting, + package: parts.package, + quantity: parts.quantity, + unit: parts.unit, + locationId: parts.locationId + }) + .from(parts) + .leftJoin(locations, eq(parts.locationId, locations.id)) + .where(where) + .orderBy(parts.category, parts.value) + .limit(pageSize) + .offset((page - 1) * pageSize); + + return { + parts: partList, + total: totalResult?.value ?? 0, + page, + pageSize, + filters: { category, mounting, search } + }; +}; diff --git a/src/routes/(app)/parts/+page.svelte b/src/routes/(app)/parts/+page.svelte new file mode 100644 index 0000000..ac9e9dd --- /dev/null +++ b/src/routes/(app)/parts/+page.svelte @@ -0,0 +1,150 @@ + + + + Parts - My Collection + + +
+
+

Parts

+ + Add Part + +
+ + +
+
+ +
+ + +
+ +

{data.total} part{data.total !== 1 ? 's' : ''}

+ + {#if data.parts.length === 0} +
+

No parts found.

+ Add your first part +
+ {:else} + + {/if} + + {#if totalPages > 1} +
+ {#if data.page > 1} + Previous + {/if} + Page {data.page} of {totalPages} + {#if data.page < totalPages} + Next + {/if} +
+ {/if} +
diff --git a/src/routes/(app)/parts/[id]/+page.server.ts b/src/routes/(app)/parts/[id]/+page.server.ts new file mode 100644 index 0000000..f270bbc --- /dev/null +++ b/src/routes/(app)/parts/[id]/+page.server.ts @@ -0,0 +1,59 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { parts, locations } from '$lib/server/db/schema.js'; +import { eq, sql } from 'drizzle-orm'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { z } from 'zod'; + +export const load: PageServerLoad = async ({ params }) => { + const [part] = await db + .select({ + id: parts.id, + name: parts.name, + category: parts.category, + mpn: parts.mpn, + value: parts.value, + voltage: parts.voltage, + tolerance: parts.tolerance, + power: parts.power, + mounting: parts.mounting, + package: parts.package, + quantity: parts.quantity, + unit: parts.unit, + locationId: parts.locationId, + locationName: locations.name, + notes: parts.notes, + createdAt: parts.createdAt, + updatedAt: parts.updatedAt + }) + .from(parts) + .leftJoin(locations, eq(parts.locationId, locations.id)) + .where(eq(parts.id, params.id)); + + if (!part) error(404, 'Part not found'); + + return { part }; +}; + +export const actions: Actions = { + adjust: async ({ request, params }) => { + const formData = await request.formData(); + const delta = z.coerce.number().int().safeParse(formData.get('delta')); + if (!delta.success) return fail(400, { error: 'Invalid quantity' }); + + await db + .update(parts) + .set({ + quantity: sql`GREATEST(0, ${parts.quantity} + ${delta.data})`, + updatedAt: sql`now()` + }) + .where(eq(parts.id, params.id)); + + return { success: true }; + }, + + delete: async ({ params }) => { + await db.delete(parts).where(eq(parts.id, params.id)); + redirect(303, '/parts'); + } +}; diff --git a/src/routes/(app)/parts/[id]/+page.svelte b/src/routes/(app)/parts/[id]/+page.svelte new file mode 100644 index 0000000..8182a87 --- /dev/null +++ b/src/routes/(app)/parts/[id]/+page.svelte @@ -0,0 +1,138 @@ + + + + {p.value ?? p.name ?? p.category} - Parts + + +
+ +
+
+

{p.category}

+

+ {p.value ?? p.name ?? p.mpn ?? 'Part'} +

+ {#if p.name && p.value} +

{p.name}

+ {/if} +
+
+ + Edit + +
+ +
+
+
+ + +
+

Stock

+
+
+ {p.quantity} + {p.unit} +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ {#if p.locationName} +

📍 {p.locationName}

+ {/if} +
+ + +
+

Specs

+
+ {#if p.mpn} +
+
MPN
+
{p.mpn}
+
+ {/if} + {#if p.value} +
+
Value
+
{p.value}
+
+ {/if} + {#if p.voltage} +
+
Voltage
+
{p.voltage}
+
+ {/if} + {#if p.tolerance} +
+
Tolerance
+
{p.tolerance}
+
+ {/if} + {#if p.power} +
+
Power
+
{p.power}
+
+ {/if} + {#if p.mounting || p.package} +
+
Package
+
+ {#if p.mounting} + {p.mounting} + {/if} + {#if p.package} + {p.package} + {/if} +
+
+ {/if} +
+
+ + {#if p.notes} +
+

Notes

+

{p.notes}

+
+ {/if} + + +
diff --git a/src/routes/(app)/parts/[id]/edit/+page.server.ts b/src/routes/(app)/parts/[id]/edit/+page.server.ts new file mode 100644 index 0000000..8307404 --- /dev/null +++ b/src/routes/(app)/parts/[id]/edit/+page.server.ts @@ -0,0 +1,74 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { parts, locations } from '$lib/server/db/schema.js'; +import { eq, sql } from 'drizzle-orm'; +import { error, fail, redirect } from '@sveltejs/kit'; +import { z } from 'zod'; + +const partSchema = z.object({ + name: z.string().optional(), + category: z.string().min(1, 'Category is required'), + mpn: z.string().optional(), + value: z.string().optional(), + voltage: z.string().optional(), + tolerance: z.string().optional(), + power: z.string().optional(), + mounting: z.enum(['THT', 'SMT']).optional().or(z.literal('')), + package: z.string().optional(), + quantity: z.coerce.number().int().min(0).default(0), + unit: z.string().default('pcs'), + locationId: z.string().uuid().optional().or(z.literal('')), + notes: z.string().optional() +}); + +export const load: PageServerLoad = async ({ params }) => { + const [part] = await db + .select() + .from(parts) + .where(eq(parts.id, params.id)); + + if (!part) error(404, 'Part not found'); + + const locationList = await db + .select({ id: locations.id, name: locations.name, parentId: locations.parentId }) + .from(locations) + .orderBy(locations.name); + + return { part, locations: locationList }; +}; + +export const actions: Actions = { + default: async ({ request, params }) => { + const formData = await request.formData(); + const raw = Object.fromEntries(formData); + + const result = partSchema.safeParse(raw); + if (!result.success) { + return fail(400, { error: result.error.errors[0]?.message ?? 'Invalid input', values: raw }); + } + + const data = result.data; + + await db + .update(parts) + .set({ + name: data.name || null, + category: data.category, + mpn: data.mpn || null, + value: data.value || null, + voltage: data.voltage || null, + tolerance: data.tolerance || null, + power: data.power || null, + mounting: data.mounting || null, + package: data.package || null, + quantity: data.quantity, + unit: data.unit, + locationId: data.locationId || null, + notes: data.notes || null, + updatedAt: sql`now()` + }) + .where(eq(parts.id, params.id)); + + redirect(303, `/parts/${params.id}`); + } +}; diff --git a/src/routes/(app)/parts/[id]/edit/+page.svelte b/src/routes/(app)/parts/[id]/edit/+page.svelte new file mode 100644 index 0000000..1cbe6a8 --- /dev/null +++ b/src/routes/(app)/parts/[id]/edit/+page.svelte @@ -0,0 +1,153 @@ + + + + Edit Part - My Collection + + +
+

Edit Part

+ + {#if form?.error} +
{form.error}
+ {/if} + +
+ + +
+

Identity

+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+ {#if showVoltage} +
+ + +
+ {/if} + {#if showTolerance} +
+ + +
+ {/if} + {#if showPower} +
+ + +
+ {/if} +
+
+ + +
+

Package

+
+
+

Mounting

+
+ {#each ['THT', 'SMT'] as m} + + {/each} + +
+
+
+ + +
+
+
+ + +
+

Stock

+
+
+ + +
+
+ + +
+
+
+ Storage Location + +
+
+ + +
+ + +
+ +
+ + Cancel +
+
+
diff --git a/src/routes/(app)/parts/new/+page.server.ts b/src/routes/(app)/parts/new/+page.server.ts new file mode 100644 index 0000000..111b6cf --- /dev/null +++ b/src/routes/(app)/parts/new/+page.server.ts @@ -0,0 +1,64 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { parts, locations } from '$lib/server/db/schema.js'; +import { fail, redirect } from '@sveltejs/kit'; +import { z } from 'zod'; + +const partSchema = z.object({ + name: z.string().optional(), + category: z.string().min(1, 'Category is required'), + mpn: z.string().optional(), + value: z.string().optional(), + voltage: z.string().optional(), + tolerance: z.string().optional(), + power: z.string().optional(), + mounting: z.enum(['THT', 'SMT']).optional().or(z.literal('')), + package: z.string().optional(), + quantity: z.coerce.number().int().min(0).default(0), + unit: z.string().default('pcs'), + locationId: z.string().uuid().optional().or(z.literal('')), + notes: z.string().optional() +}); + +export const load: PageServerLoad = async () => { + const locationList = await db + .select({ id: locations.id, name: locations.name, parentId: locations.parentId }) + .from(locations) + .orderBy(locations.name); + return { locations: locationList }; +}; + +export const actions: Actions = { + default: async ({ request }) => { + const formData = await request.formData(); + const raw = Object.fromEntries(formData); + + const result = partSchema.safeParse(raw); + if (!result.success) { + return fail(400, { error: result.error.errors[0]?.message ?? 'Invalid input', values: raw }); + } + + const data = result.data; + + const [part] = await db + .insert(parts) + .values({ + name: data.name || null, + category: data.category, + mpn: data.mpn || null, + value: data.value || null, + voltage: data.voltage || null, + tolerance: data.tolerance || null, + power: data.power || null, + mounting: data.mounting || null, + package: data.package || null, + quantity: data.quantity, + unit: data.unit, + locationId: data.locationId || null, + notes: data.notes || null + }) + .returning({ id: parts.id }); + + redirect(303, `/parts/${part!.id}`); + } +}; diff --git a/src/routes/(app)/parts/new/+page.svelte b/src/routes/(app)/parts/new/+page.svelte new file mode 100644 index 0000000..b2acf2b --- /dev/null +++ b/src/routes/(app)/parts/new/+page.svelte @@ -0,0 +1,162 @@ + + + + Add Part - My Collection + + +
+

Add Part

+ + {#if form?.error} +
{form.error}
+ {/if} + +
+ + +
+

Identity

+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+ {#if showVoltage} +
+ + +
+ {/if} + {#if showTolerance} +
+ + +
+ {/if} + {#if showPower} +
+ + +
+ {/if} +
+
+ + +
+

Package

+ +
+
+

Mounting

+
+ {#each ['THT', 'SMT'] as m} + + {/each} + +
+
+
+ + +
+
+
+ + +
+

Stock

+ +
+
+ + +
+
+ + +
+
+ +
+ Storage Location + +
+
+ + +
+ + +
+ +
+ + Cancel +
+
+