Skip to content

Commit 0a49675

Browse files
Merge pull request #245 from SheetMetalConnect/claude/fix-operator-view-modal-01Vh99wras7adhQyTozrbPFE
Fix operator view updates and onboarding modal persistence
2 parents 5916a6f + 769b998 commit 0a49675

File tree

6 files changed

+108
-58
lines changed

6 files changed

+108
-58
lines changed

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ function AppRoutes() {
184184
element={
185185
<ProtectedRoute>
186186
<Layout>
187-
<OperatorTerminal />
187+
<OperatorView />
188188
</Layout>
189189
</ProtectedRoute>
190190
}

src/components/operator/CurrentlyTimingWidget.tsx

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { useOperator } from "@/contexts/OperatorContext";
44
import { supabase } from "@/integrations/supabase/client";
55
import { Card } from "@/components/ui/card";
66
import { Button } from "@/components/ui/button";
7-
import { Clock, Square } from "lucide-react";
7+
import { Clock, Square, ChevronDown, ChevronUp } from "lucide-react";
88
import { stopTimeTracking } from "@/lib/database";
99
import { toast } from "sonner";
1010
import { formatDistanceToNow } from "date-fns";
1111
import { useTranslation } from "react-i18next";
12+
import { cn } from "@/lib/utils";
1213

1314
interface ActiveEntry {
1415
id: string;
@@ -32,6 +33,7 @@ export default function CurrentlyTimingWidget() {
3233
const operatorId = activeOperator?.id || profile?.id;
3334
const [activeEntries, setActiveEntries] = useState<ActiveEntry[]>([]);
3435
const [, setTick] = useState(0);
36+
const [isCollapsed, setIsCollapsed] = useState(false);
3537

3638
const loadActiveEntries = useCallback(async () => {
3739
if (!operatorId) return;
@@ -108,37 +110,71 @@ export default function CurrentlyTimingWidget() {
108110
if (activeEntries.length === 0) return null;
109111

110112
return (
111-
<Card className="p-4 bg-active-work/5 border-active-work/30">
112-
<div className="flex items-center gap-2 mb-3">
113-
<Clock className="h-5 w-5 text-active-work" />
114-
<h3 className="font-semibold">{t("operations.currentlyTiming")}</h3>
115-
</div>
116-
<div className="space-y-2">
117-
{activeEntries.map((entry) => (
118-
<div
119-
key={entry.id}
120-
className="flex items-center justify-between p-3 bg-background rounded-lg border"
121-
>
122-
<div className="flex-1 min-w-0">
123-
<div className="font-medium truncate">{entry.operation.operation_name}</div>
124-
<div className="text-sm text-muted-foreground">
125-
{t("operations.job")} {entry.operation.part.job.job_number}{entry.operation.part.part_number}
126-
</div>
127-
<div className="text-xs text-muted-foreground mt-1">
128-
{t("operations.started")} {formatDistanceToNow(new Date(entry.start_time), { addSuffix: true })}
113+
<Card className="bg-active-work/5 border-active-work/30 overflow-hidden">
114+
{/* Header - always visible, clickable to toggle */}
115+
<button
116+
onClick={() => setIsCollapsed(!isCollapsed)}
117+
className="w-full flex items-center justify-between p-3 hover:bg-active-work/10 transition-colors"
118+
>
119+
<div className="flex items-center gap-2">
120+
<Clock className="h-4 w-4 text-active-work animate-pulse" />
121+
<span className="font-semibold text-sm">{t("operations.currentlyTiming")}</span>
122+
<span className="text-xs text-active-work bg-active-work/20 px-1.5 py-0.5 rounded-full">
123+
{activeEntries.length}
124+
</span>
125+
</div>
126+
<div className="flex items-center gap-2">
127+
{/* Show first entry summary when collapsed */}
128+
{isCollapsed && activeEntries[0] && (
129+
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
130+
{activeEntries[0].operation.operation_name}
131+
</span>
132+
)}
133+
{isCollapsed ? (
134+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
135+
) : (
136+
<ChevronUp className="h-4 w-4 text-muted-foreground" />
137+
)}
138+
</div>
139+
</button>
140+
141+
{/* Content - collapsible */}
142+
<div
143+
className={cn(
144+
"transition-all duration-200 ease-in-out overflow-hidden",
145+
isCollapsed ? "max-h-0 opacity-0" : "max-h-[500px] opacity-100"
146+
)}
147+
>
148+
<div className="px-3 pb-3 space-y-2">
149+
{activeEntries.map((entry) => (
150+
<div
151+
key={entry.id}
152+
className="flex items-center justify-between p-3 bg-background rounded-lg border"
153+
>
154+
<div className="flex-1 min-w-0">
155+
<div className="font-medium truncate text-sm">{entry.operation.operation_name}</div>
156+
<div className="text-xs text-muted-foreground">
157+
{t("operations.job")} {entry.operation.part.job.job_number}{entry.operation.part.part_number}
158+
</div>
159+
<div className="text-[10px] text-muted-foreground mt-1">
160+
{t("operations.started")} {formatDistanceToNow(new Date(entry.start_time), { addSuffix: true })}
161+
</div>
129162
</div>
163+
<Button
164+
variant="outline"
165+
size="sm"
166+
onClick={(e) => {
167+
e.stopPropagation();
168+
handleStop(entry.operation_id);
169+
}}
170+
className="gap-1.5 ml-3 h-8 text-xs"
171+
>
172+
<Square className="h-3 w-3" />
173+
{t("operations.stop")}
174+
</Button>
130175
</div>
131-
<Button
132-
variant="outline"
133-
size="sm"
134-
onClick={() => handleStop(entry.operation_id)}
135-
className="gap-2 ml-4"
136-
>
137-
<Square className="h-4 w-4" />
138-
{t("operations.stop")}
139-
</Button>
140-
</div>
141-
))}
176+
))}
177+
</div>
142178
</div>
143179
</Card>
144180
);

src/components/operator/OperatorLayout.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,26 +65,34 @@ export const OperatorLayout = ({ children }: OperatorLayoutProps) => {
6565
{/* Top Header - Glass Morphism - Compact */}
6666
<header className="sticky top-0 z-50 w-full glass-card border-b border-border-subtle">
6767
<div className="flex items-center justify-between h-12 px-3 sm:px-4">
68-
{/* Logo/Brand */}
69-
<div className="flex items-center gap-2">
70-
<Factory className="h-6 w-6 text-primary" strokeWidth={1.5} />
71-
<span className="hidden sm:block text-sm font-bold hero-title">
72-
{t('app.name')}
68+
{/* Left: Tenant/Company Name */}
69+
<div className="flex items-center gap-2 min-w-0">
70+
<Building2 className="h-5 w-5 text-primary shrink-0" />
71+
<span className="text-sm font-bold truncate max-w-[120px] sm:max-w-[200px]">
72+
{tenant?.company_name || tenant?.name || t('app.name')}
7373
</span>
7474
</div>
7575

76+
{/* Center: Active Operator - Always visible and prominent */}
77+
<div className="flex items-center gap-2">
78+
{activeOperator ? (
79+
<div className="flex items-center gap-2 px-2 sm:px-3 py-1 rounded-full bg-green-500/10 border border-green-500/30">
80+
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse shrink-0" />
81+
<UserCheck className="h-3.5 w-3.5 text-green-500 shrink-0 hidden sm:block" />
82+
<span className="text-xs font-bold text-green-500 truncate max-w-[80px] sm:max-w-[120px]">
83+
{activeOperator.full_name}
84+
</span>
85+
<span className="text-[10px] text-green-500/70 font-mono hidden sm:block">
86+
{activeOperator.employee_id}
87+
</span>
88+
</div>
89+
) : (
90+
<OperatorSwitcher variant="button" className="h-8" />
91+
)}
92+
</div>
93+
7694
{/* Right Side Actions */}
7795
<div className="flex items-center gap-1.5">
78-
{/* Active Operator Badge - Desktop */}
79-
<div className="hidden sm:block">
80-
<ActiveOperatorBadge />
81-
</div>
82-
83-
{/* Operator Switcher - Mobile */}
84-
<div className="sm:hidden">
85-
<OperatorSwitcher variant="compact" />
86-
</div>
87-
8896
{/* Search Button */}
8997
<SearchTriggerButton onClick={() => setSearchOpen(true)} compact />
9098

src/contexts/OperatorContext.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,21 @@ export function OperatorProvider({ children }: { children: React.ReactNode }) {
4040

4141
// Load active operator from localStorage on mount
4242
useEffect(() => {
43+
// Wait for tenant to be loaded before validating stored operator
44+
// If tenant is not yet loaded (undefined), don't do anything yet
45+
if (tenant?.id === undefined) {
46+
return;
47+
}
48+
4349
const stored = localStorage.getItem(STORAGE_KEY);
4450
if (stored) {
4551
try {
4652
const parsed = JSON.parse(stored);
4753
// Validate that the stored operator belongs to current tenant
48-
if (parsed.tenant_id === tenant?.id) {
54+
if (parsed.tenant_id === tenant.id) {
4955
setActiveOperator(parsed);
5056
} else {
57+
// Only clear if we have a valid tenant and it doesn't match
5158
localStorage.removeItem(STORAGE_KEY);
5259
}
5360
} catch {

src/pages/operator/OperatorView.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,16 @@ export default function OperatorView() {
145145
const { t } = useTranslation();
146146
const navigate = useNavigate();
147147
const { profile, tenant } = useAuth();
148-
const { activeOperator, verifyAndSwitchOperator, clearActiveOperator } = useOperator();
148+
const { activeOperator, isLoading: operatorLoading, verifyAndSwitchOperator, clearActiveOperator } = useOperator();
149149
const operatorId = activeOperator?.id || profile?.id;
150150

151151
// Responsive breakpoints
152152
const isMobile = useMediaQuery("(max-width: 639px)");
153153
const isTablet = useMediaQuery("(min-width: 640px) and (max-width: 1023px)");
154154
const isDesktop = useMediaQuery("(min-width: 1024px)");
155155

156-
// Operator login state
157-
const [showOperatorLogin, setShowOperatorLogin] = useState(!activeOperator);
156+
// Operator login state - initialize as false, will be set after operatorLoading completes
157+
const [showOperatorLogin, setShowOperatorLogin] = useState(false);
158158
const [employeeId, setEmployeeId] = useState("");
159159
const [pin, setPin] = useState("");
160160
const [loginLoading, setLoginLoading] = useState(false);
@@ -192,10 +192,12 @@ export default function OperatorView() {
192192
const [isDragging, setIsDragging] = useState<boolean>(false);
193193
const containerRef = useRef<HTMLDivElement>(null);
194194

195-
// Update operator login visibility when activeOperator changes
195+
// Update operator login visibility when activeOperator changes (only after loading completes)
196196
useEffect(() => {
197-
setShowOperatorLogin(!activeOperator);
198-
}, [activeOperator]);
197+
if (!operatorLoading) {
198+
setShowOperatorLogin(!activeOperator);
199+
}
200+
}, [activeOperator, operatorLoading]);
199201

200202
// Load jobs
201203
useEffect(() => {
@@ -853,7 +855,8 @@ export default function OperatorView() {
853855
);
854856
}
855857

856-
if (loading && !jobs.length) {
858+
// Show loading spinner while operator context is loading or jobs are loading
859+
if (operatorLoading || (loading && !jobs.length)) {
857860
return (
858861
<>
859862
<AnimatedBackground />

src/pages/operator/WorkQueue.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useOperator } from "@/contexts/OperatorContext";
44
import { supabase } from "@/integrations/supabase/client";
55
import { fetchOperationsWithDetails, OperationWithDetails } from "@/lib/database";
66
import OperationCard from "@/components/operator/OperationCard";
7-
import CurrentlyTimingWidget from "@/components/operator/CurrentlyTimingWidget";
87
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
98
import { Input } from "@/components/ui/input";
109
import { Card } from "@/components/ui/card";
@@ -272,9 +271,6 @@ export default function WorkQueue() {
272271
return (
273272
<>
274273
<div className="space-y-4">
275-
{/* Currently Timing Widget */}
276-
<CurrentlyTimingWidget />
277-
278274
{/* Stats Card */}
279275
<Card className="glass-card p-6">
280276
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">

0 commit comments

Comments
 (0)