Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 71 additions & 11 deletions src/components/admin/OperationDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -46,6 +48,7 @@ export default function OperationDetailModal({
onClose,
onUpdate,
}: OperationDetailModalProps) {
const { t } = useTranslation();
const { toast } = useToast();
const { profile } = useAuth();
const queryClient = useQueryClient();
Expand Down Expand Up @@ -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 () => {
Expand All @@ -107,7 +110,10 @@ export default function OperationDetailModal({
resource:resources (
id,
name,
type
type,
identifier,
location,
status
)
`)
.eq("operation_id", operationId);
Expand Down Expand Up @@ -348,15 +354,69 @@ export default function OperationDetailModal({
{/* Resources */}
{resources && resources.length > 0 && (
<div>
<Label className="text-xs text-muted-foreground mb-2 block">Required Resources</Label>
<div className="flex flex-wrap gap-2">
{resources.map((r: any) => (
<Badge key={r.id} variant="outline" className="gap-1">
<Wrench className="h-3 w-3" />
{r.resource?.name}
{r.quantity > 1 && ` × ${r.quantity}`}
</Badge>
))}
<Label className="text-xs text-muted-foreground mb-2 block flex items-center gap-1.5">
<Wrench className="h-4 w-4" />
{t("operations.requiredResources")} ({resources.length})
</Label>
<div className="space-y-2">
{resources.map((r: any) => {
const statusColors: Record<string, string> = {
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<string, string> = {
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 (
<div key={r.id} className="border rounded-md p-2.5 bg-muted/20">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<Wrench className="h-3.5 w-3.5 text-orange-500 shrink-0" />
<span className="font-medium text-sm">{r.resource?.name}</span>
{r.quantity > 1 && (
<Badge variant="secondary" className="text-[10px] px-1 py-0">×{r.quantity}</Badge>
)}
</div>
<div className="text-xs text-muted-foreground mt-1 space-y-0.5">
<p className="capitalize">{t("operations.type")}: {r.resource?.type?.replace("_", " ")}</p>
{r.resource?.identifier && <p>ID: {r.resource.identifier}</p>}
{r.resource?.location && (
<p className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{t("terminal.resources.location")}: {r.resource.location}
</p>
)}
</div>
</div>
{r.resource?.status && (
<Badge
variant="outline"
className={`text-[10px] shrink-0 ${statusColors[r.resource.status] || ""}`}
>
{getStatusLabel(r.resource.status)}
</Badge>
)}
</div>
{r.notes && (
<div className="mt-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-xs">
<div className="flex items-start gap-1.5">
<AlertCircle className="h-3 w-3 text-amber-600 mt-0.5 shrink-0" />
<span className="text-amber-700 dark:text-amber-300">{r.notes}</span>
</div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
Expand Down
158 changes: 158 additions & 0 deletions src/components/terminal/AssemblyDependencies.tsx
Original file line number Diff line number Diff line change
@@ -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<ChildPart[]>([]);
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 <CheckCircle2 className="w-3 h-3 text-emerald-500" />;
case 'in_progress':
return <Clock className="w-3 h-3 text-primary" />;
default:
return <Circle className="w-3 h-3 text-muted-foreground/50" />;
}
};

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 (
<div className={cn("space-y-1.5", className)}>
<div className={cn(
"flex items-center gap-1.5 text-[10px] uppercase tracking-wider font-medium",
allComplete ? "text-emerald-600 dark:text-emerald-400" : "text-amber-600 dark:text-amber-400"
)}>
{allComplete ? (
<CheckCircle2 className="w-3 h-3" />
) : (
<AlertTriangle className="w-3 h-3" />
)}
{t('terminal.assembly.title')}
</div>

{!allComplete && (
<div className="px-2 py-1.5 bg-amber-500/10 border border-amber-500/30 rounded text-xs">
<div className="flex items-center gap-1.5 text-amber-700 dark:text-amber-300 font-medium">
<AlertTriangle className="w-3.5 h-3.5" />
{t('terminal.assembly.warning')}
</div>
<div className="text-[10px] text-amber-600 dark:text-amber-400 mt-0.5 ml-5">
{t('terminal.assembly.componentsPending', { count: incompleteCount })}
</div>
</div>
)}

{allComplete && (
<div className="px-2 py-1.5 bg-emerald-500/10 border border-emerald-500/30 rounded text-xs">
<div className="flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300 font-medium">
<CheckCircle2 className="w-3.5 h-3.5" />
{t('terminal.assembly.allComplete')}
</div>
</div>
)}

<div className="space-y-0.5 mt-1">
{childParts.map((part) => (
<div
key={part.id}
className={cn(
"flex items-center justify-between gap-2 px-2 py-1 rounded text-[11px]",
part.status === 'completed'
? "bg-muted/20 text-muted-foreground"
: "bg-muted/40"
)}
>
<div className="flex items-center gap-1.5 min-w-0 flex-1">
{getStatusIcon(part.status)}
<span className={cn(
"truncate",
part.status === 'completed' && "line-through"
)}>
{part.part_number}
</span>
</div>
<span className={cn(
"text-[9px] shrink-0",
part.status === 'completed'
? "text-emerald-600 dark:text-emerald-400"
: part.status === 'in_progress'
? "text-primary"
: "text-muted-foreground"
)}>
{getStatusLabel(part.status)}
</span>
</div>
))}
</div>
</div>
);
}
11 changes: 11 additions & 0 deletions src/components/terminal/DetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -234,6 +236,15 @@ export function DetailPanel({ job, onStart, onPause, onComplete, stepUrl, pdfUrl
</div>
)}

{/* Resources & Assembly Dependencies Section */}
<div className="px-3 py-2 border-b border-border bg-muted/10 space-y-2">
{/* Required Resources for this Operation */}
<OperationResources operationId={job.operationId} />

{/* Assembly Dependencies - show if this part has child parts */}
<AssemblyDependencies partId={job.partId} />
</div>

{/* Main Content Tabs - Compact */}
<div className="flex-1 min-h-0">
<Tabs defaultValue={job.hasModel ? "3d" : job.hasPdf ? "pdf" : "ops"} className="h-full flex flex-col">
Expand Down
Loading
Loading