Add Parts section: consumable stock inventory (caps, resistors, etc.)
Deploy to LXC / deploy (push) Failing after 13m44s
Deploy to LXC / deploy (push) Failing after 13m44s
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1782725202288,
|
||||
"tag": "0000_clear_sister_grimm",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { PART_CATEGORIES } from '$lib/constants.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let search = $state(data.filters.search ?? '');
|
||||
|
||||
function applyFilter(key: string, value: string | null) {
|
||||
const url = new URL($page.url);
|
||||
if (value) url.searchParams.set(key, value);
|
||||
else url.searchParams.delete(key);
|
||||
url.searchParams.delete('page');
|
||||
goto(url.toString());
|
||||
}
|
||||
|
||||
function handleSearch(e: Event) {
|
||||
e.preventDefault();
|
||||
applyFilter('q', search || null);
|
||||
}
|
||||
|
||||
const totalPages = $derived(Math.ceil(data.total / data.pageSize));
|
||||
|
||||
function mountingColor(mounting: string | null) {
|
||||
if (mounting === 'SMT') return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300';
|
||||
if (mounting === 'THT') return 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300';
|
||||
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400';
|
||||
}
|
||||
|
||||
function stockColor(qty: number) {
|
||||
if (qty <= 0) return 'text-red-600 dark:text-red-400';
|
||||
if (qty <= 5) return 'text-amber-600 dark:text-amber-400';
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Parts - My Collection</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Parts</h1>
|
||||
<a href="/parts/new" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
||||
Add Part
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-6 flex flex-wrap items-center gap-3">
|
||||
<form onsubmit={handleSearch} class="flex-1">
|
||||
<input type="text" bind:value={search} placeholder="Search by value, MPN, package..."
|
||||
class="w-full max-w-sm rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</form>
|
||||
<select onchange={(e) => applyFilter('category', (e.target as HTMLSelectElement).value || null)}
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">All Categories</option>
|
||||
{#each PART_CATEGORIES as c}
|
||||
<option value={c} selected={data.filters.category === c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select onchange={(e) => applyFilter('mounting', (e.target as HTMLSelectElement).value || null)}
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">THT + SMT</option>
|
||||
<option value="THT" selected={data.filters.mounting === 'THT'}>THT</option>
|
||||
<option value="SMT" selected={data.filters.mounting === 'SMT'}>SMT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">{data.total} part{data.total !== 1 ? 's' : ''}</p>
|
||||
|
||||
{#if data.parts.length === 0}
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center dark:border-gray-700 dark:bg-gray-800">
|
||||
<p class="text-gray-500 dark:text-gray-400">No parts found.</p>
|
||||
<a href="/parts/new" class="mt-2 inline-block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">Add your first part</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Category</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Value / MPN</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Package</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Specs</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Stock</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{#each data.parts as part}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-4 py-3">
|
||||
<a href="/parts/{part.id}" class="block">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{part.category}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="/parts/{part.id}" class="block">
|
||||
{#if part.value}
|
||||
<span class="font-medium text-gray-900 dark:text-white">{part.value}</span>
|
||||
{/if}
|
||||
{#if part.name}
|
||||
<span class="block text-xs text-gray-500 dark:text-gray-400">{part.name}</span>
|
||||
{/if}
|
||||
{#if part.mpn}
|
||||
<span class="block text-xs font-mono text-gray-400 dark:text-gray-500">{part.mpn}</span>
|
||||
{/if}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="/parts/{part.id}" class="flex items-center gap-2">
|
||||
{#if part.mounting}
|
||||
<span class="rounded px-1.5 py-0.5 text-xs font-medium {mountingColor(part.mounting)}">{part.mounting}</span>
|
||||
{/if}
|
||||
{#if part.package}
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{part.package}</span>
|
||||
{/if}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
<a href="/parts/{part.id}" class="block">
|
||||
{[part.voltage, part.tolerance, part.power].filter(Boolean).join(' · ')}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="/parts/{part.id}" class="block">
|
||||
<span class="text-sm font-medium {stockColor(part.quantity)}">{part.quantity}</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500"> {part.unit}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if totalPages > 1}
|
||||
<div class="mt-6 flex items-center justify-center gap-2">
|
||||
{#if data.page > 1}
|
||||
<a href="?page={data.page - 1}" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">Previous</a>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">Page {data.page} of {totalPages}</span>
|
||||
{#if data.page < totalPages}
|
||||
<a href="?page={data.page + 1}" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">Next</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { data } = $props();
|
||||
const p = $derived(data.part);
|
||||
|
||||
function mountingColor(mounting: string | null) {
|
||||
if (mounting === 'SMT') return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300';
|
||||
if (mounting === 'THT') return 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300';
|
||||
return '';
|
||||
}
|
||||
|
||||
function stockColor(qty: number) {
|
||||
if (qty <= 0) return 'text-red-600 dark:text-red-400';
|
||||
if (qty <= 5) return 'text-amber-600 dark:text-amber-400';
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{p.value ?? p.name ?? p.category} - Parts</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="mb-1 text-sm text-gray-500 dark:text-gray-400">{p.category}</p>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{p.value ?? p.name ?? p.mpn ?? 'Part'}
|
||||
</h1>
|
||||
{#if p.name && p.value}
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">{p.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<a href="/parts/{p.id}/edit" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
Edit
|
||||
</a>
|
||||
<form method="POST" action="?/delete" use:enhance>
|
||||
<button type="submit" onclick={(e) => { if (!confirm('Delete this part?')) e.preventDefault(); }}
|
||||
class="rounded-md border border-red-200 px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock card -->
|
||||
<div class="mb-5 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Stock</h2>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="text-4xl font-bold {stockColor(p.quantity)}">
|
||||
{p.quantity}
|
||||
<span class="text-lg font-normal text-gray-400 dark:text-gray-500">{p.unit}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<form method="POST" action="?/adjust" use:enhance>
|
||||
<input type="hidden" name="delta" value="-1" />
|
||||
<button type="submit" class="flex h-9 w-9 items-center justify-center rounded-md border border-gray-300 text-lg font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">−</button>
|
||||
</form>
|
||||
<form method="POST" action="?/adjust" use:enhance>
|
||||
<input type="hidden" name="delta" value="1" />
|
||||
<button type="submit" class="flex h-9 w-9 items-center justify-center rounded-md border border-gray-300 text-lg font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">+</button>
|
||||
</form>
|
||||
<form method="POST" action="?/adjust" use:enhance class="flex items-center gap-1">
|
||||
<input type="number" name="delta" placeholder="±qty"
|
||||
class="w-20 rounded-md border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
<button type="submit" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{#if p.locationName}
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">📍 {p.locationName}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Specs -->
|
||||
<div class="mb-5 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Specs</h2>
|
||||
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm sm:grid-cols-3">
|
||||
{#if p.mpn}
|
||||
<div class="col-span-2 sm:col-span-3">
|
||||
<dt class="text-gray-500 dark:text-gray-400">MPN</dt>
|
||||
<dd class="font-mono font-medium text-gray-900 dark:text-white">{p.mpn}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if p.value}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Value</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{p.value}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if p.voltage}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Voltage</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{p.voltage}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if p.tolerance}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Tolerance</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{p.tolerance}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if p.power}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Power</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white">{p.power}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if p.mounting || p.package}
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Package</dt>
|
||||
<dd class="flex items-center gap-2">
|
||||
{#if p.mounting}
|
||||
<span class="rounded px-1.5 py-0.5 text-xs font-medium {mountingColor(p.mounting)}">{p.mounting}</span>
|
||||
{/if}
|
||||
{#if p.package}
|
||||
<span class="font-medium text-gray-900 dark:text-white">{p.package}</span>
|
||||
{/if}
|
||||
</dd>
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{#if p.notes}
|
||||
<div class="mb-5 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Notes</h2>
|
||||
<p class="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300">{p.notes}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2">
|
||||
<a href="/parts" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">← Back to Parts</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { PART_CATEGORIES } from '$lib/constants.js';
|
||||
import LocationPicker from '$lib/components/ui/LocationPicker.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
let category = $state(String(form?.values?.category ?? data.part.category ?? ''));
|
||||
|
||||
const showCapacitance = $derived(category === 'Capacitor');
|
||||
const showResistance = $derived(category === 'Resistor');
|
||||
const showVoltage = $derived(['Capacitor', 'Diode', 'LED', 'Transistor', 'IC / Chip', 'Fuse'].includes(category));
|
||||
const showPower = $derived(['Resistor', 'Transistor', 'Diode', 'Inductor'].includes(category));
|
||||
const showTolerance = $derived(['Resistor', 'Capacitor', 'Inductor', 'Crystal / Oscillator'].includes(category));
|
||||
|
||||
const val = (field: string) => (form?.values as any)?.[field] ?? (data.part as any)[field] ?? '';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Edit Part - My Collection</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Edit Part</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-300">{form.error}</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" use:enhance class="space-y-5">
|
||||
|
||||
<!-- Identity -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Identity</h2>
|
||||
|
||||
<div class="mb-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="category" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Category *</label>
|
||||
<select id="category" name="category" required bind:value={category}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
{#each PART_CATEGORIES as c}
|
||||
<option value={c} selected={category === c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mpn" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">MPN</label>
|
||||
<input type="text" id="mpn" name="mpn" value={val('mpn')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="name" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Name <span class="text-gray-400 font-normal">(optional)</span></label>
|
||||
<input type="text" id="name" name="name" value={val('name')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="value" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{#if showCapacitance}Capacitance{:else if showResistance}Resistance{:else}Value{/if}
|
||||
</label>
|
||||
<input type="text" id="value" name="value" value={val('value')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
{#if showVoltage}
|
||||
<div>
|
||||
<label for="voltage" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Voltage Rating</label>
|
||||
<input type="text" id="voltage" name="voltage" value={val('voltage')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTolerance}
|
||||
<div>
|
||||
<label for="tolerance" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Tolerance</label>
|
||||
<input type="text" id="tolerance" name="tolerance" value={val('tolerance')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showPower}
|
||||
<div>
|
||||
<label for="power" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Power Rating</label>
|
||||
<input type="text" id="power" name="power" value={val('power')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Package -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Package</h2>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Mounting</p>
|
||||
<div class="flex gap-3">
|
||||
{#each ['THT', 'SMT'] as m}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input type="radio" name="mounting" value={m} checked={val('mounting') === m}
|
||||
class="text-blue-600 focus:ring-blue-500" />
|
||||
{m}
|
||||
</label>
|
||||
{/each}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<input type="radio" name="mounting" value="" checked={!val('mounting')}
|
||||
class="text-blue-600 focus:ring-blue-500" />
|
||||
Unknown
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="package" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Package / Size</label>
|
||||
<input type="text" id="package" name="package" value={val('package')}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Stock</h2>
|
||||
<div class="mb-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="quantity" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Quantity</label>
|
||||
<input type="number" id="quantity" name="quantity" value={data.part.quantity} min="0"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="unit" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Unit</label>
|
||||
<input type="text" id="unit" name="unit" value={data.part.unit}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Storage Location</span>
|
||||
<LocationPicker locations={data.locations} name="locationId" value={data.part.locationId ?? null} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<label for="notes" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Notes</label>
|
||||
<textarea id="notes" name="notes" rows="3"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white">{val('notes')}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">Save Changes</button>
|
||||
<a href="/parts/{data.part.id}" class="rounded-md px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { PART_CATEGORIES } from '$lib/constants.js';
|
||||
import LocationPicker from '$lib/components/ui/LocationPicker.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
let category = $state(String(form?.values?.category ?? ''));
|
||||
|
||||
const showCapacitance = $derived(category === 'Capacitor');
|
||||
const showResistance = $derived(category === 'Resistor');
|
||||
const showVoltage = $derived(['Capacitor', 'Diode', 'LED', 'Transistor', 'IC / Chip', 'Fuse'].includes(category));
|
||||
const showPower = $derived(['Resistor', 'Transistor', 'Diode', 'Inductor'].includes(category));
|
||||
const showTolerance = $derived(['Resistor', 'Capacitor', 'Inductor', 'Crystal / Oscillator'].includes(category));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Add Part - My Collection</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Add Part</h1>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-300">{form.error}</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" use:enhance class="space-y-5">
|
||||
|
||||
<!-- Identity -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Identity</h2>
|
||||
|
||||
<div class="mb-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="category" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Category *</label>
|
||||
<select id="category" name="category" required bind:value={category}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">Select category…</option>
|
||||
{#each PART_CATEGORIES as c}
|
||||
<option value={c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mpn" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">MPN</label>
|
||||
<input type="text" id="mpn" name="mpn" value={form?.values?.mpn ?? ''}
|
||||
placeholder="Manufacturer part number"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="name" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Name <span class="text-gray-400 font-normal">(optional)</span></label>
|
||||
<input type="text" id="name" name="name" value={form?.values?.name ?? ''}
|
||||
placeholder="e.g. Nichicon electrolytic, BC547 NPN"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="value" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{#if showCapacitance}Capacitance{:else if showResistance}Resistance{:else}Value{/if}
|
||||
</label>
|
||||
<input type="text" id="value" name="value" value={form?.values?.value ?? ''}
|
||||
placeholder={showCapacitance ? 'e.g. 100µF' : showResistance ? 'e.g. 10kΩ' : 'e.g. 1N4148'}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
{#if showVoltage}
|
||||
<div>
|
||||
<label for="voltage" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Voltage Rating</label>
|
||||
<input type="text" id="voltage" name="voltage" value={form?.values?.voltage ?? ''}
|
||||
placeholder="e.g. 16V, 50V"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTolerance}
|
||||
<div>
|
||||
<label for="tolerance" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Tolerance</label>
|
||||
<input type="text" id="tolerance" name="tolerance" value={form?.values?.tolerance ?? ''}
|
||||
placeholder="e.g. 5%, 10%, 20%"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showPower}
|
||||
<div>
|
||||
<label for="power" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Power Rating</label>
|
||||
<input type="text" id="power" name="power" value={form?.values?.power ?? ''}
|
||||
placeholder="e.g. 0.25W, 1W"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Package -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Package</h2>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p class="mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Mounting</p>
|
||||
<div class="flex gap-3">
|
||||
{#each ['THT', 'SMT'] as m}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input type="radio" name="mounting" value={m} checked={form?.values?.mounting === m}
|
||||
class="text-blue-600 focus:ring-blue-500" />
|
||||
{m}
|
||||
</label>
|
||||
{/each}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<input type="radio" name="mounting" value="" checked={!form?.values?.mounting}
|
||||
class="text-blue-600 focus:ring-blue-500" />
|
||||
Unknown
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="package" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Package / Size</label>
|
||||
<input type="text" id="package" name="package" value={form?.values?.package ?? ''}
|
||||
placeholder="e.g. 0805, DIP-8, TO-92, Radial 5mm"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Stock</h2>
|
||||
|
||||
<div class="mb-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="quantity" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Quantity</label>
|
||||
<input type="number" id="quantity" name="quantity" value={form?.values?.quantity ?? 0} min="0"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="unit" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Unit</label>
|
||||
<input type="text" id="unit" name="unit" value={form?.values?.unit ?? 'pcs'}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Storage Location</span>
|
||||
<LocationPicker locations={data.locations} name="locationId" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<label for="notes" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Notes</label>
|
||||
<textarea id="notes" name="notes" rows="3"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white">{form?.values?.notes ?? ''}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">Add Part</button>
|
||||
<a href="/parts" class="rounded-md px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user