diff --git a/src/components/admin/OperationDetailModal.tsx b/src/components/admin/OperationDetailModal.tsx index b7c52f54..30a58c58 100644 --- a/src/components/admin/OperationDetailModal.tsx +++ b/src/components/admin/OperationDetailModal.tsx @@ -28,11 +28,13 @@ import { Play, AlertCircle, ChevronRight, + MapPin, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { STEPViewer } from "@/components/STEPViewer"; import { PDFViewer } from "@/components/PDFViewer"; import { useAuth } from "@/contexts/AuthContext"; +import { useTranslation } from "react-i18next"; import { format } from "date-fns"; interface OperationDetailModalProps { @@ -46,6 +48,7 @@ export default function OperationDetailModal({ onClose, onUpdate, }: OperationDetailModalProps) { + const { t } = useTranslation(); const { toast } = useToast(); const { profile } = useAuth(); const queryClient = useQueryClient(); @@ -94,7 +97,7 @@ export default function OperationDetailModal({ }, }); - // Fetch resources for this operation + // Fetch resources for this operation with full details const { data: resources } = useQuery({ queryKey: ["operation-resources", operationId], queryFn: async () => { @@ -107,7 +110,10 @@ export default function OperationDetailModal({ resource:resources ( id, name, - type + type, + identifier, + location, + status ) `) .eq("operation_id", operationId); @@ -348,15 +354,69 @@ export default function OperationDetailModal({ {/* Resources */} {resources && resources.length > 0 && (
- -
- {resources.map((r: any) => ( - - - {r.resource?.name} - {r.quantity > 1 && ` × ${r.quantity}`} - - ))} + +
+ {resources.map((r: any) => { + const statusColors: Record = { + available: "text-emerald-600 bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-800", + in_use: "text-amber-600 bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800", + maintenance: "text-red-600 bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-800", + retired: "text-muted-foreground bg-muted/50 border-muted", + }; + const getStatusLabel = (status: string) => { + const labels: Record = { + available: t("terminal.resources.status.available"), + in_use: t("terminal.resources.status.inUse"), + maintenance: t("terminal.resources.status.maintenance"), + retired: t("terminal.resources.status.retired"), + }; + return labels[status] || status; + }; + return ( +
+
+
+
+ + {r.resource?.name} + {r.quantity > 1 && ( + ×{r.quantity} + )} +
+
+

{t("operations.type")}: {r.resource?.type?.replace("_", " ")}

+ {r.resource?.identifier &&

ID: {r.resource.identifier}

} + {r.resource?.location && ( +

+ + {t("terminal.resources.location")}: {r.resource.location} +

+ )} +
+
+ {r.resource?.status && ( + + {getStatusLabel(r.resource.status)} + + )} +
+ {r.notes && ( +
+
+ + {r.notes} +
+
+ )} +
+ ); + })}
)} diff --git a/src/components/terminal/AssemblyDependencies.tsx b/src/components/terminal/AssemblyDependencies.tsx new file mode 100644 index 00000000..11ced580 --- /dev/null +++ b/src/components/terminal/AssemblyDependencies.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useState } from 'react'; +import { AlertTriangle, CheckCircle2, Layers, Circle, Clock } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '@/contexts/AuthContext'; +import { cn } from '@/lib/utils'; + +interface ChildPart { + id: string; + part_number: string; + status: 'not_started' | 'in_progress' | 'completed'; +} + +interface AssemblyDependenciesProps { + partId: string; + className?: string; +} + +/** + * Compact assembly dependencies display for terminal DetailPanel + * Shows status of child parts that need to be complete before assembly can start + */ +export function AssemblyDependencies({ partId, className }: AssemblyDependenciesProps) { + const { t } = useTranslation(); + const { profile } = useAuth(); + const [childParts, setChildParts] = useState([]); + const [loading, setLoading] = useState(true); + const [hasChildren, setHasChildren] = useState(false); + + useEffect(() => { + const fetchChildParts = async () => { + if (!partId || !profile?.tenant_id) return; + + try { + const { data, error } = await supabase + .from('parts') + .select('id, part_number, status') + .eq('parent_part_id', partId) + .eq('tenant_id', profile.tenant_id) + .order('part_number'); + + if (!error && data && data.length > 0) { + setChildParts(data as ChildPart[]); + setHasChildren(true); + } else { + setHasChildren(false); + } + } catch (error) { + console.error('Error fetching child parts:', error); + } finally { + setLoading(false); + } + }; + + fetchChildParts(); + }, [partId, profile?.tenant_id]); + + if (loading || !hasChildren) { + return null; + } + + const completedCount = childParts.filter(p => p.status === 'completed').length; + const incompleteCount = childParts.length - completedCount; + const allComplete = incompleteCount === 0; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'completed': + return ; + case 'in_progress': + return ; + default: + return ; + } + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case 'completed': + return t('terminal.assembly.partStatus.completed'); + case 'in_progress': + return t('terminal.assembly.partStatus.inProgress'); + default: + return t('terminal.assembly.partStatus.notStarted'); + } + }; + + return ( +
+
+ {allComplete ? ( + + ) : ( + + )} + {t('terminal.assembly.title')} +
+ + {!allComplete && ( +
+
+ + {t('terminal.assembly.warning')} +
+
+ {t('terminal.assembly.componentsPending', { count: incompleteCount })} +
+
+ )} + + {allComplete && ( +
+
+ + {t('terminal.assembly.allComplete')} +
+
+ )} + +
+ {childParts.map((part) => ( +
+
+ {getStatusIcon(part.status)} + + {part.part_number} + +
+ + {getStatusLabel(part.status)} + +
+ ))} +
+
+ ); +} diff --git a/src/components/terminal/DetailPanel.tsx b/src/components/terminal/DetailPanel.tsx index f036778f..a01f9c79 100644 --- a/src/components/terminal/DetailPanel.tsx +++ b/src/components/terminal/DetailPanel.tsx @@ -6,6 +6,8 @@ import { Play, Pause, Square, FileText, Box, AlertTriangle, CheckCircle2, Clock, import { CncProgramQrCode } from './CncProgramQrCode'; import { STEPViewer } from '@/components/STEPViewer'; import { PDFViewer } from '@/components/PDFViewer'; +import { OperationResources } from './OperationResources'; +import { AssemblyDependencies } from './AssemblyDependencies'; import { OperationWithDetails } from '@/lib/database'; import { cn } from '@/lib/utils'; import IssueForm from '@/components/operator/IssueForm'; @@ -234,6 +236,15 @@ export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl
)} + {/* Resources & Assembly Dependencies Section */} +
+ {/* Required Resources for this Operation */} + + + {/* Assembly Dependencies - show if this part has child parts */} + +
+ {/* Main Content Tabs - Compact */}
diff --git a/src/components/terminal/OperationResources.tsx b/src/components/terminal/OperationResources.tsx new file mode 100644 index 00000000..41cf82c9 --- /dev/null +++ b/src/components/terminal/OperationResources.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from 'react'; +import { Wrench, MapPin, AlertCircle } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useTranslation } from 'react-i18next'; +import { cn } from '@/lib/utils'; + +interface Resource { + id: string; + name: string; + type: string; + identifier?: string | null; + location?: string | null; + status?: string | null; +} + +interface OperationResource { + id: string; + quantity?: number | null; + notes?: string | null; + resource: Resource; +} + +interface OperationResourcesProps { + operationId: string; + className?: string; +} + +/** + * Compact resource display for terminal DetailPanel + * Shows required resources for an operation in a minimal, touch-friendly format + */ +export function OperationResources({ operationId, className }: OperationResourcesProps) { + const { t } = useTranslation(); + const [resources, setResources] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchResources = async () => { + if (!operationId) return; + + try { + const { data, error } = await supabase + .from('operation_resources') + .select(` + *, + resource:resources(*) + `) + .eq('operation_id', operationId); + + if (!error && data) { + setResources(data as unknown as OperationResource[]); + } + } catch (error) { + console.error('Error fetching resources:', error); + } finally { + setLoading(false); + } + }; + + fetchResources(); + }, [operationId]); + + if (loading) { + return ( +
+ {t('terminal.resources.loading')} +
+ ); + } + + if (resources.length === 0) { + return null; + } + + const getStatusColor = (status?: string | null) => { + switch (status) { + case 'available': + return 'text-emerald-600 dark:text-emerald-400'; + case 'in_use': + return 'text-amber-600 dark:text-amber-400'; + case 'maintenance': + return 'text-red-600 dark:text-red-400'; + default: + return 'text-muted-foreground'; + } + }; + + const getStatusLabel = (status?: string | null) => { + switch (status) { + case 'available': + return t('terminal.resources.status.available'); + case 'in_use': + return t('terminal.resources.status.inUse'); + case 'maintenance': + return t('terminal.resources.status.maintenance'); + case 'retired': + return t('terminal.resources.status.retired'); + default: + return status || ''; + } + }; + + return ( +
+
+ + {t('terminal.resources.title')} + + ({resources.length}) + +
+ +
+ {resources.map((opResource) => ( +
+
+ +
+
+ {opResource.resource.name} + {opResource.quantity && opResource.quantity > 1 && ( + + ×{opResource.quantity} + + )} +
+ {opResource.resource.location && ( +
+ + {opResource.resource.location} +
+ )} +
+
+ + {opResource.resource.status && ( + + {getStatusLabel(opResource.resource.status)} + + )} +
+ ))} +
+ + {resources.some(r => r.notes) && ( +
+ {resources.filter(r => r.notes).map(r => ( +
+ + + {r.notes} + +
+ ))} +
+ )} +
+ ); +} diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 1189f00d..e05bc58d 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1346,7 +1346,34 @@ "you": "SIE", "other": "Andere", "youAreClockedOn": "Sie sind derzeit bei diesem Arbeitsgang eingestempelt", - "bulletCard": "Eilauftrag - Bullet Card" + "bulletCard": "Eilauftrag - Bullet Card", + "resources": { + "title": "Erforderliche Betriebsmittel", + "noResources": "Keine Betriebsmittel erforderlich", + "loading": "Betriebsmittel werden geladen...", + "resource": "Betriebsmittel", + "resources": "Betriebsmittel", + "location": "Standort", + "status": { + "available": "Verfügbar", + "inUse": "In Verwendung", + "maintenance": "Wartung", + "retired": "Ausgemustert" + } + }, + "assembly": { + "title": "Baugruppen-Abhängigkeiten", + "warning": "Komponenten nicht bereit", + "warningDesc": "Einige Komponenten sind noch nicht fertig", + "allComplete": "Alle Komponenten bereit", + "componentsPending": "{{count}} Komponente(n) ausstehend", + "checkComponents": "Prüfen Sie Komponenten vor Beginn der Montage", + "partStatus": { + "notStarted": "Nicht gestartet", + "inProgress": "In Bearbeitung", + "completed": "Abgeschlossen" + } + } }, "activityMonitor": { "title": "Aktivitätsmonitor", diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 28b341bb..c1bcd586 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -1477,7 +1477,34 @@ "you": "YOU", "other": "Other", "youAreClockedOn": "You are currently clocked on this operation", - "bulletCard": "Rush Order - Bullet Card" + "bulletCard": "Rush Order - Bullet Card", + "resources": { + "title": "Required Resources", + "noResources": "No resources required", + "loading": "Loading resources...", + "resource": "resource", + "resources": "resources", + "location": "Location", + "status": { + "available": "Available", + "inUse": "In Use", + "maintenance": "Maintenance", + "retired": "Retired" + } + }, + "assembly": { + "title": "Assembly Dependencies", + "warning": "Components Not Ready", + "warningDesc": "Some components are not yet complete", + "allComplete": "All Components Ready", + "componentsPending": "{{count}} component(s) pending", + "checkComponents": "Check components before starting assembly", + "partStatus": { + "notStarted": "Not Started", + "inProgress": "In Progress", + "completed": "Completed" + } + } }, "activityMonitor": { "title": "Activity Monitor", diff --git a/src/i18n/locales/nl/translation.json b/src/i18n/locales/nl/translation.json index 727771d9..3385ee50 100644 --- a/src/i18n/locales/nl/translation.json +++ b/src/i18n/locales/nl/translation.json @@ -1616,7 +1616,34 @@ "you": "JIJ", "other": "Ander", "youAreClockedOn": "Je bent momenteel ingeklokt op deze bewerking", - "bulletCard": "Spoedorder - Bullet Card" + "bulletCard": "Spoedorder - Bullet Card", + "resources": { + "title": "Benodigde Gereedschappen", + "noResources": "Geen gereedschappen nodig", + "loading": "Gereedschappen laden...", + "resource": "gereedschap", + "resources": "gereedschappen", + "location": "Locatie", + "status": { + "available": "Beschikbaar", + "inUse": "In gebruik", + "maintenance": "Onderhoud", + "retired": "Buiten gebruik" + } + }, + "assembly": { + "title": "Samenstelling Afhankelijkheden", + "warning": "Componenten Niet Gereed", + "warningDesc": "Sommige componenten zijn nog niet voltooid", + "allComplete": "Alle Componenten Gereed", + "componentsPending": "{{count}} component(en) wachtend", + "checkComponents": "Controleer componenten voor montage", + "partStatus": { + "notStarted": "Niet gestart", + "inProgress": "In bewerking", + "completed": "Voltooid" + } + } }, "activityMonitor": { "title": "Activiteitenmonitor",