chore: upload parte 1 - logica
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
"use client";
|
||||
|
||||
import React from "react"
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { Component, Category, Customer, ComponentSpecs } from "@/lib/types";
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
|
||||
// Category-specific specification fields
|
||||
const CATEGORY_SPECS: Record<string, { key: keyof ComponentSpecs; label: string; type?: string; options?: string[] }[]> = {
|
||||
resistors: [
|
||||
{ key: "value", label: "Value" },
|
||||
{ key: "power", label: "Power" },
|
||||
{ key: "voltage", label: "Voltage" },
|
||||
{ key: "tolerance", label: "Tolerance" },
|
||||
],
|
||||
capacitors: [
|
||||
{ key: "value", label: "Value" },
|
||||
{ key: "power", label: "Power" },
|
||||
{ key: "voltage", label: "Voltage" },
|
||||
{ key: "tolerance", label: "Tolerance" },
|
||||
],
|
||||
inductors: [
|
||||
{ key: "value", label: "Value" },
|
||||
{ key: "tolerance", label: "Tolerance" },
|
||||
],
|
||||
leds: [
|
||||
{ key: "color", label: "Color" },
|
||||
],
|
||||
connectors: [
|
||||
{ key: "no_of_pins", label: "No. of Pins" },
|
||||
{ key: "no_of_rows", label: "No. of Rows" },
|
||||
{ key: "connector_type", label: "Type", type: "select", options: ["Female", "Male"] },
|
||||
],
|
||||
};
|
||||
|
||||
function getCategorySpecKey(categoryName: string | undefined): string | null {
|
||||
if (!categoryName) return null;
|
||||
const lower = categoryName.toLowerCase().trim();
|
||||
// Match plural or singular forms
|
||||
if (lower.includes("resistor")) return "resistors";
|
||||
if (lower.includes("capacitor")) return "capacitors";
|
||||
if (lower.includes("inductor")) return "inductors";
|
||||
if (lower.includes("led")) return "leds";
|
||||
if (lower.includes("connector")) return "connectors";
|
||||
return null;
|
||||
}
|
||||
|
||||
interface ComponentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
component: Component | null;
|
||||
categories: Category[];
|
||||
onSave: (data: Partial<Component>) => Promise<void>;
|
||||
}
|
||||
|
||||
const emptySpecs: ComponentSpecs = {};
|
||||
|
||||
export function ComponentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
component,
|
||||
categories,
|
||||
onSave,
|
||||
}: ComponentDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { data: customers = [] } = useSWR<Customer[]>("/api/customers", fetcher);
|
||||
const [form, setForm] = useState({
|
||||
part_number: "",
|
||||
name: "",
|
||||
description: "",
|
||||
category_id: "",
|
||||
manufacturer: "",
|
||||
package_type: "",
|
||||
unit_price: "",
|
||||
stock_quantity: "",
|
||||
min_stock_level: "",
|
||||
datasheet_url: "",
|
||||
supplier: "",
|
||||
supplier_part_number: "",
|
||||
shelf_location: "",
|
||||
is_internal: false,
|
||||
customer_supplied: false,
|
||||
customer_id: "",
|
||||
});
|
||||
const [specs, setSpecs] = useState<ComponentSpecs>({});
|
||||
|
||||
// Determine the selected category name for spec fields
|
||||
const selectedCategoryName = useMemo(() => {
|
||||
if (!form.category_id) return undefined;
|
||||
const cat = categories.find((c) => String(c.id) === form.category_id);
|
||||
return cat?.name;
|
||||
}, [form.category_id, categories]);
|
||||
|
||||
const specKey = getCategorySpecKey(selectedCategoryName);
|
||||
const specFields = specKey ? CATEGORY_SPECS[specKey] : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (component) {
|
||||
setForm({
|
||||
part_number: component.part_number || "",
|
||||
name: component.name || "",
|
||||
description: component.description || "",
|
||||
category_id: component.category_id ? String(component.category_id) : "",
|
||||
manufacturer: component.manufacturer || "",
|
||||
package_type: component.package_type || "",
|
||||
unit_price: String(component.unit_price || ""),
|
||||
stock_quantity: String(component.stock_quantity || ""),
|
||||
min_stock_level: String(component.min_stock_level || ""),
|
||||
datasheet_url: component.datasheet_url || "",
|
||||
supplier: component.supplier || "",
|
||||
supplier_part_number: component.supplier_part_number || "",
|
||||
shelf_location: component.shelf_location || "",
|
||||
is_internal: component.is_internal ?? false,
|
||||
customer_supplied: component.customer_supplied ?? false,
|
||||
customer_id: component.customer_id ? String(component.customer_id) : "",
|
||||
});
|
||||
const rawSpecs = component.specs;
|
||||
setSpecs(typeof rawSpecs === "string" ? JSON.parse(rawSpecs) : rawSpecs || {});
|
||||
} else {
|
||||
setForm({
|
||||
part_number: "",
|
||||
name: "",
|
||||
description: "",
|
||||
category_id: "",
|
||||
manufacturer: "",
|
||||
package_type: "",
|
||||
unit_price: "",
|
||||
stock_quantity: "",
|
||||
min_stock_level: "",
|
||||
datasheet_url: "",
|
||||
supplier: "",
|
||||
supplier_part_number: "",
|
||||
shelf_location: "",
|
||||
is_internal: false,
|
||||
customer_supplied: false,
|
||||
customer_id: "",
|
||||
});
|
||||
setSpecs({});
|
||||
}
|
||||
}, [component, open]);
|
||||
|
||||
function updateSpec(key: keyof ComponentSpecs, value: string) {
|
||||
setSpecs((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
await onSave({
|
||||
...form,
|
||||
category_id: form.category_id ? Number(form.category_id) : null,
|
||||
unit_price: Number(form.unit_price) || 0,
|
||||
stock_quantity: Number(form.stock_quantity) || 0,
|
||||
min_stock_level: Number(form.min_stock_level) || 0,
|
||||
is_internal: form.is_internal,
|
||||
customer_supplied: form.customer_supplied,
|
||||
customer_id: form.customer_supplied && form.customer_id ? Number(form.customer_id) : null,
|
||||
specs,
|
||||
} as unknown as Partial<Component>);
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{component ? "Edit Component" : "Add New Component"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4 pt-2">
|
||||
|
||||
{/* 1. Part Number and Name */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="part_number">Part Number *</Label>
|
||||
<Input
|
||||
id="part_number"
|
||||
value={form.part_number}
|
||||
onChange={(e) => setForm({ ...form, part_number: e.target.value })}
|
||||
placeholder="e.g. RC0805-10K"
|
||||
className="font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="e.g. 10K Ohm Resistor"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Description */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="Component description..."
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 3. Category, Manufacturer, Package */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select
|
||||
value={form.category_id}
|
||||
onValueChange={(val) => setForm({ ...form, category_id: val })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((cat) => (
|
||||
<SelectItem key={cat.id} value={String(cat.id)}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="manufacturer">Manufacturer</Label>
|
||||
<Input
|
||||
id="manufacturer"
|
||||
value={form.manufacturer}
|
||||
onChange={(e) => setForm({ ...form, manufacturer: e.target.value })}
|
||||
placeholder="e.g. Vishay"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="package_type">Package</Label>
|
||||
<Input
|
||||
id="package_type"
|
||||
value={form.package_type}
|
||||
onChange={(e) => setForm({ ...form, package_type: e.target.value })}
|
||||
placeholder="e.g. 0805"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3b. Manufacturer ID (all categories) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="manufacturer_id">Manufacturer ID</Label>
|
||||
<Input
|
||||
id="manufacturer_id"
|
||||
value={specs.manufacturer_id || ""}
|
||||
onChange={(e) => updateSpec("manufacturer_id", e.target.value)}
|
||||
placeholder="Manufacturer part/ID number"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 4. Category-specific specifications */}
|
||||
{specFields && specFields.length > 0 && (
|
||||
<div className="rounded-lg border p-4 flex flex-col gap-3 bg-muted/30">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{selectedCategoryName} Specifications
|
||||
</p>
|
||||
<div className={`grid gap-4 ${specFields.length === 1 ? "grid-cols-1" : specFields.length === 2 ? "grid-cols-2" : specFields.length <= 4 ? "grid-cols-2 sm:grid-cols-4" : "grid-cols-2 sm:grid-cols-3"}`}>
|
||||
{specFields.map((field) => (
|
||||
<div key={field.key} className="flex flex-col gap-2">
|
||||
<Label htmlFor={`spec_${field.key}`} className="text-xs">{field.label}</Label>
|
||||
{field.type === "select" && field.options ? (
|
||||
<Select
|
||||
value={specs[field.key] || ""}
|
||||
onValueChange={(val) => updateSpec(field.key, val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={`Select ${field.label.toLowerCase()}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>{opt}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={`spec_${field.key}`}
|
||||
value={specs[field.key] || ""}
|
||||
onChange={(e) => updateSpec(field.key, e.target.value)}
|
||||
placeholder={field.label}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 5. Unit Price, Stock Quantity, Min Stock Level */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="unit_price">Unit Price ($)</Label>
|
||||
<Input
|
||||
id="unit_price"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
value={form.unit_price}
|
||||
onChange={(e) => setForm({ ...form, unit_price: e.target.value })}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="stock_quantity">Stock Quantity</Label>
|
||||
<Input
|
||||
id="stock_quantity"
|
||||
type="number"
|
||||
min="0"
|
||||
value={form.stock_quantity}
|
||||
onChange={(e) => setForm({ ...form, stock_quantity: e.target.value })}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="min_stock_level">Min Stock Level</Label>
|
||||
<Input
|
||||
id="min_stock_level"
|
||||
type="number"
|
||||
min="0"
|
||||
value={form.min_stock_level}
|
||||
onChange={(e) => setForm({ ...form, min_stock_level: e.target.value })}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6. Supplier and Supplier Part No */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="supplier">Supplier</Label>
|
||||
<Input
|
||||
id="supplier"
|
||||
value={form.supplier}
|
||||
onChange={(e) => setForm({ ...form, supplier: e.target.value })}
|
||||
placeholder="e.g. Digi-Key, Mouser"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="supplier_part_number">Supplier Part No.</Label>
|
||||
<Input
|
||||
id="supplier_part_number"
|
||||
value={form.supplier_part_number}
|
||||
onChange={(e) => setForm({ ...form, supplier_part_number: e.target.value })}
|
||||
placeholder="e.g. 311-10KARCT-ND"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 7. Supplier URL */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="datasheet_url">Supplier URL</Label>
|
||||
<Input
|
||||
id="datasheet_url"
|
||||
type="url"
|
||||
value={form.datasheet_url}
|
||||
onChange={(e) => setForm({ ...form, datasheet_url: e.target.value })}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 8. Internal Production and Customer Supplied */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<Checkbox
|
||||
id="is_internal"
|
||||
checked={form.is_internal}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm({ ...form, is_internal: checked === true })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="is_internal" className="cursor-pointer text-sm font-medium">
|
||||
Internal Production
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<Checkbox
|
||||
id="customer_supplied"
|
||||
checked={form.customer_supplied}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm({ ...form, customer_supplied: checked === true, customer_id: checked ? form.customer_id : "" })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="customer_supplied" className="cursor-pointer text-sm font-medium">
|
||||
Customer Supplied
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{form.customer_supplied && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="customer_id">Supplied by Customer *</Label>
|
||||
<Select
|
||||
value={form.customer_id}
|
||||
onValueChange={(val) => setForm({ ...form, customer_id: val })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select customer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{customers.map((cust) => (
|
||||
<SelectItem key={cust.id} value={String(cust.id)}>
|
||||
{cust.code} - {cust.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 9. Shelf / Storage Location */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="shelf_location">Shelf / Storage Location</Label>
|
||||
<Input
|
||||
id="shelf_location"
|
||||
value={form.shelf_location}
|
||||
onChange={(e) => setForm({ ...form, shelf_location: e.target.value })}
|
||||
placeholder="e.g. A3-R2-B5, Rack 1 Shelf 3"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Saving..." : component ? "Update" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user