diff --git a/mcp-server/src/tools/parts.ts b/mcp-server/src/tools/parts.ts index 598fcb4a..6120980b 100644 --- a/mcp-server/src/tools/parts.ts +++ b/mcp-server/src/tools/parts.ts @@ -50,6 +50,18 @@ const tools: Tool[] = [ type: "string", description: "New current stage ID", }, + drawing_no: { + type: "string", + description: "Drawing number reference", + }, + cnc_program_name: { + type: "string", + description: "CNC program name for machine operators (generates QR code)", + }, + is_bullet_card: { + type: "boolean", + description: "QRM bullet card flag - indicates rush/priority order", + }, }, required: ["id"], }, diff --git a/package-lock.json b/package-lock.json index dbc3dad9..6ea5b85d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "lucide-react": "^0.462.0", "next-themes": "^0.3.0", "papaparse": "^5.5.3", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", @@ -8579,6 +8580,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", diff --git a/package.json b/package.json index 954de930..658e7fc2 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "lucide-react": "^0.462.0", "next-themes": "^0.3.0", "papaparse": "^5.5.3", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", diff --git a/src/components/admin/PartDetailModal.tsx b/src/components/admin/PartDetailModal.tsx index 945706d0..15021380 100644 --- a/src/components/admin/PartDetailModal.tsx +++ b/src/components/admin/PartDetailModal.tsx @@ -11,8 +11,10 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; -import { useState } from "react"; -import { Plus, Save, X, Upload, Eye, Trash2, Box, FileText, AlertTriangle, Package, ChevronRight, Wrench, Image as ImageIcon } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; +import { useState, useEffect } from "react"; +import { Plus, Save, X, Upload, Eye, Trash2, Box, FileText, AlertTriangle, Package, ChevronRight, Wrench, Image as ImageIcon, Zap, QrCode } from "lucide-react"; +import { QRCodeSVG } from "qrcode.react"; import { useToast } from "@/hooks/use-toast"; import { STEPViewer } from "@/components/STEPViewer"; import { PDFViewer } from "@/components/PDFViewer"; @@ -64,6 +66,12 @@ export default function PartDetailModal({ partId, onClose, onUpdate }: PartDetai const [currentFileType, setCurrentFileType] = useState<'step' | 'pdf' | null>(null); const [currentFileTitle, setCurrentFileTitle] = useState(""); + // New part fields state + const [drawingNo, setDrawingNo] = useState(""); + const [cncProgramName, setCncProgramName] = useState(""); + const [isBulletCard, setIsBulletCard] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + // Upload hook with progress tracking and quota validation const { progress: uploadProgress, @@ -103,6 +111,55 @@ export default function PartDetailModal({ partId, onClose, onUpdate }: PartDetai }, }); + // Initialize new fields from part data + useEffect(() => { + if (part) { + setDrawingNo(part.drawing_no || ""); + setCncProgramName(part.cnc_program_name || ""); + setIsBulletCard(part.is_bullet_card || false); + setHasChanges(false); + } + }, [part]); + + // Mutation to update part fields + const updatePartFieldsMutation = useMutation({ + mutationFn: async () => { + const { error } = await supabase + .from("parts") + .update({ + drawing_no: drawingNo || null, + cnc_program_name: cncProgramName || null, + is_bullet_card: isBulletCard, + updated_at: new Date().toISOString(), + }) + .eq("id", partId); + + if (error) throw error; + }, + onSuccess: async () => { + toast({ + title: t("common.success"), + description: t("parts.fieldsUpdated"), + }); + setHasChanges(false); + await queryClient.invalidateQueries({ queryKey: ["part-detail", partId] }); + onUpdate(); + }, + onError: (error: any) => { + toast({ + title: t("common.error"), + description: error.message, + variant: "destructive", + }); + }, + }); + + // Track changes + const handleFieldChange = (setter: (value: any) => void, value: any) => { + setter(value); + setHasChanges(true); + }; + // Fetch available resources for linking const { data: availableResources } = useQuery({ queryKey: ["available-resources", profile?.tenant_id], @@ -500,6 +557,95 @@ export default function PartDetailModal({ partId, onClose, onUpdate }: PartDetai + {/* Manufacturing Fields - Drawing No, CNC Program, Bullet Card */} +
+
+ + {hasChanges && ( + + )} +
+ +
+ {/* Drawing Number */} +
+ + handleFieldChange(setDrawingNo, e.target.value)} + placeholder={t("parts.drawingNoPlaceholder")} + className="mt-1" + /> +
+ + {/* CNC Program Name */} +
+ + handleFieldChange(setCncProgramName, e.target.value)} + placeholder={t("parts.cncProgramPlaceholder")} + className="mt-1" + /> +
+ + {/* Bullet Card Toggle */} +
+
+
+ +
+ +

+ {t("parts.bulletCardDesc")} +

+
+
+ handleFieldChange(setIsBulletCard, checked)} + /> +
+
+ + {/* QR Code Preview */} + {cncProgramName && ( +
+ +
+ +
+

{cncProgramName}

+

+ {t("parts.qrCodeDesc")} +

+
+
+
+ )} +
+
+ {/* Routing Visualization */}
diff --git a/src/components/terminal/CncProgramQrCode.tsx b/src/components/terminal/CncProgramQrCode.tsx new file mode 100644 index 00000000..2aba76e9 --- /dev/null +++ b/src/components/terminal/CncProgramQrCode.tsx @@ -0,0 +1,28 @@ +import { QRCodeSVG } from 'qrcode.react'; +import { cn } from '@/lib/utils'; + +interface CncProgramQrCodeProps { + programName: string; + className?: string; + size?: number; +} + +export function CncProgramQrCode({ + programName, + className, + size = 64, +}: CncProgramQrCodeProps) { + if (!programName) return null; + + return ( + + ); +} diff --git a/src/components/terminal/DetailPanel.tsx b/src/components/terminal/DetailPanel.tsx index 423bb305..f036778f 100644 --- a/src/components/terminal/DetailPanel.tsx +++ b/src/components/terminal/DetailPanel.tsx @@ -2,10 +2,9 @@ import React, { useState, useMemo, useEffect } from 'react'; import { TerminalJob } from '@/types/terminal'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Separator } from '@/components/ui/separator'; -import { Play, Pause, Square, FileText, Box, AlertTriangle, CheckCircle2, Clock, Circle, Maximize2, X, ChevronDown, ChevronRight, PackageCheck } from 'lucide-react'; -import { STEPViewer } from '@/components/STEPViewer'; // Reusing existing viewer +import { Play, Pause, Square, FileText, Box, AlertTriangle, CheckCircle2, Clock, Circle, Maximize2, X, ChevronDown, ChevronRight, PackageCheck, Zap } from 'lucide-react'; +import { CncProgramQrCode } from './CncProgramQrCode'; +import { STEPViewer } from '@/components/STEPViewer'; import { PDFViewer } from '@/components/PDFViewer'; import { OperationWithDetails } from '@/lib/database'; import { cn } from '@/lib/utils'; @@ -18,6 +17,7 @@ import { useAuth } from '@/contexts/AuthContext'; import { createPortal } from 'react-dom'; import { supabase } from '@/integrations/supabase/client'; import { IconDisplay } from '@/components/ui/icon-picker'; +import { useTranslation } from 'react-i18next'; interface Substep { id: string; @@ -41,6 +41,7 @@ interface DetailPanelProps { } export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl, operations = [], onDataRefresh }: DetailPanelProps) { + const { t } = useTranslation(); const [isIssueModalOpen, setIsIssueModalOpen] = useState(false); const [isQuantityModalOpen, setIsQuantityModalOpen] = useState(false); const [fullscreenViewer, setFullscreenViewer] = useState<'3d' | 'pdf' | null>(null); @@ -122,10 +123,21 @@ export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl {/* Header Card - Compact */}
+ {/* Bullet Card Alert Banner */} + {job.isBulletCard && ( +
+ + {t('terminal.bulletCard')} +
+ )} +

{job.jobCode}

{job.description}

+ {job.drawingNo && ( +

{t('parts.drawingNo')}: {job.drawingNo}

+ )}
{job.quantity} pcs
@@ -206,6 +218,22 @@ export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl )}
+ {/* CNC Program QR Code - Compact inline display */} + {job.cncProgramName && ( +
+
+ +
+
+
{t('parts.cncProgramName')}
+

{job.cncProgramName}

+
+
+ )} + {/* Main Content Tabs - Compact */}
diff --git a/src/components/terminal/JobRow.tsx b/src/components/terminal/JobRow.tsx index 82a5ef05..31d55adf 100644 --- a/src/components/terminal/JobRow.tsx +++ b/src/components/terminal/JobRow.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { TerminalJob } from '@/types/terminal'; import { Badge } from '@/components/ui/badge'; -import { FileText, Box, AlertTriangle, Clock, User } from 'lucide-react'; +import { FileText, Box, AlertTriangle, Clock, User, Zap } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useTranslation } from 'react-i18next'; @@ -34,11 +34,15 @@ export function JobRow({ job, isSelected, onClick, variant }: JobRowProps) { isSelected && "bg-accent/50 ring-1 ring-primary", variant === 'process' && "bg-status-active/5", job.isCurrentUserClocked && "bg-primary/10 ring-1 ring-primary/50", + job.isBulletCard && "bg-destructive/5 border-l-2 border-l-destructive", )} > {/* Job Number with clocking indicator */}
+ {job.isBulletCard && ( + + )} {job.isCurrentUserClocked && ( { @@ -122,6 +125,37 @@ export class PartValidator extends BaseValidator { if (stepError) errors.push(stepError); } + // drawing_no + if (entity.drawing_no !== undefined) { + const drawingNoError = this.validateString(entity, "drawing_no", index, { + required: false, + maxLength: 255, + }); + if (drawingNoError) errors.push(drawingNoError); + } + + // cnc_program_name + if (entity.cnc_program_name !== undefined) { + const cncProgramError = this.validateString(entity, "cnc_program_name", index, { + required: false, + maxLength: 255, + }); + if (cncProgramError) errors.push(cncProgramError); + } + + // is_bullet_card (boolean validation) + if (entity.is_bullet_card !== undefined) { + if (typeof entity.is_bullet_card !== "boolean") { + errors.push({ + field: "is_bullet_card", + message: "is_bullet_card must be a boolean", + constraint: "TYPE", + entityType: "part", + entityIndex: index, + }); + } + } + return errors; } } diff --git a/supabase/functions/api-parts/index.ts b/supabase/functions/api-parts/index.ts index 39ec41ac..92f36425 100644 --- a/supabase/functions/api-parts/index.ts +++ b/supabase/functions/api-parts/index.ts @@ -151,6 +151,9 @@ async function handleGetParts( material_lot, material_supplier, material_cert_number, + drawing_no, + cnc_program_name, + is_bullet_card, job:jobs ( id, job_number, @@ -357,6 +360,9 @@ async function handleCreatePart( file_paths: body.file_paths, notes: body.notes || body.description, metadata: body.metadata, + drawing_no: body.drawing_no, + cnc_program_name: body.cnc_program_name, + is_bullet_card: body.is_bullet_card ?? false, status: "not_started", }) .select() @@ -404,6 +410,9 @@ async function handleUpdatePart( "material_cert_number", "current_cell_id", "material_id", + "drawing_no", + "cnc_program_name", + "is_bullet_card", ]; const updates: any = {}; diff --git a/supabase/migrations/20251203100000_add_part_fields_qr_code.sql b/supabase/migrations/20251203100000_add_part_fields_qr_code.sql new file mode 100644 index 00000000..e9918dab --- /dev/null +++ b/supabase/migrations/20251203100000_add_part_fields_qr_code.sql @@ -0,0 +1,18 @@ +-- Add new fields to parts table for drawing number, CNC program reference, and priority (bullet card) +-- These fields support QR code generation for CNC operators and QRM priority tracking + +ALTER TABLE public.parts +ADD COLUMN IF NOT EXISTS drawing_no TEXT, +ADD COLUMN IF NOT EXISTS cnc_program_name TEXT, +ADD COLUMN IF NOT EXISTS is_bullet_card BOOLEAN DEFAULT false; + +-- Add comments for documentation +COMMENT ON COLUMN public.parts.drawing_no IS 'Drawing number reference for the part'; +COMMENT ON COLUMN public.parts.cnc_program_name IS 'CNC program name for machine operators - generates QR code when set'; +COMMENT ON COLUMN public.parts.is_bullet_card IS 'QRM bullet card - indicates rush/priority order'; + +-- Create index for bullet card filtering (commonly used for priority filtering) +CREATE INDEX IF NOT EXISTS idx_parts_is_bullet_card ON public.parts(tenant_id, is_bullet_card) WHERE is_bullet_card = true; + +-- Create index for CNC program lookup +CREATE INDEX IF NOT EXISTS idx_parts_cnc_program_name ON public.parts(tenant_id, cnc_program_name) WHERE cnc_program_name IS NOT NULL;