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 && (
+
+ )}
+
+ );
+ })}
)}
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 => (
+
+ ))}
+
+ )}
+
+ );
+}
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",