diff --git a/src/app/tuxedo/time-clock/page.tsx b/src/app/tuxedo/time-clock/page.tsx index 950beb6..2a82bad 100644 --- a/src/app/tuxedo/time-clock/page.tsx +++ b/src/app/tuxedo/time-clock/page.tsx @@ -1,12 +1,13 @@ import TimeTrackingFieldClient from "@/components/time-tracking/TimeTrackingFieldClient"; import { getBrandSettingsPublic } from "@/actions/brand-settings"; - -const BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; -const BRAND_NAME = "Tuxedo Corn"; -const BRAND_ACCENT = "green"; +import { + TUXEDO_BRAND_ID, + TUXEDO_BRAND_NAME, + TUXEDO_BRAND_ACCENT, +} from "@/lib/water-log/brand"; export const metadata = { - title: "Worker Clock — Tuxedo Corn", + title: `Worker Clock — ${TUXEDO_BRAND_NAME}`, }; export default async function TuxedoTimeClockPage() { @@ -15,9 +16,9 @@ export default async function TuxedoTimeClockPage() { return ( ); diff --git a/src/components/water/mobile/MobileWaterApp.tsx b/src/components/water/mobile/MobileWaterApp.tsx index 14ad66f..77e4a64 100644 --- a/src/components/water/mobile/MobileWaterApp.tsx +++ b/src/components/water/mobile/MobileWaterApp.tsx @@ -5,13 +5,14 @@ * Tuxedo Water Log experience. * * Flow: - * pin ─verify─▶ headgates ─pick─▶ log ─submit─▶ success - * ▲ │ │ - * │ └───────────────(logout)─────────────────┤ - * └─(logout)─────(anywhere)──────────────────────────────┘ - * │ - * success ── Log Another ─▶ log (cleared) ─┘ - * success ── Back to Headgates ─▶ headgates + * pin ─verify─▶ tabbed shell + * │ + * ├──[Headgates]── headgates ─pick─▶ log ─submit─▶ success + * │ ▲ │ + * │ │ │ + * │ └───────── (logout) ────────────┤ + * │ + * └──[Time]──── TimeTrackingFieldClient (clock-in / clock-out) * * Hydration: * - On mount we peek at `document.cookie` for the `wl_session` @@ -21,11 +22,19 @@ * who already signed in once in their shift shouldn't have to * re-enter their PIN every time they reopen the page. * + * Cycle 4 — the existing PIN is unified with the time-tracking PIN: + * after `verifyWaterPin` succeeds we also best-effort call + * `verifyTimeTrackingPin` with the same digits. If the worker exists + * in `time_tracking_workers`, the time-tracking session cookie is set + * too, so they never re-enter their PIN on the Time tab. + * * Server actions used: * - `verifyWaterPin` → signs the irrigator in, sets cookie * - `getWaterHeadgates` → loads the list of active gates * - `submitWaterEntry` → posts a new reading * - `logoutWater` → clears the cookie + session row + * - `verifyTimeTrackingPin` → unifies PIN with Time tab (Cycle 4) + * - `logoutTimeTracking` → clears the time-tracking cookie on logout */ import { useCallback, useEffect, useState } from "react"; import { @@ -34,13 +43,28 @@ import { getWaterHeadgates, logoutWater, } from "@/actions/water-log/field"; -import type { FieldHeadgate, Screen, SubmittedLog } from "./types"; +import { + verifyTimeTrackingPin, + logoutTimeTracking, +} from "@/actions/time-tracking/field"; +import TimeTrackingFieldClient from "@/components/time-tracking/TimeTrackingFieldClient"; +import type { + FieldHeadgate, + Screen, + SubmittedLog, + ActiveTab, +} from "./types"; import { MobileWaterPinScreen } from "./PinScreen"; import { MobileWaterHeadgateList } from "./HeadgateList"; import { MobileWaterLogForm } from "./LogForm"; import { MobileWaterSuccessScreen } from "./SuccessScreen"; import { WL_SESSION_COOKIE } from "@/lib/water-log/session"; -import { TUXEDO_BRAND_ID } from "@/lib/water-log/brand"; +import { + TUXEDO_BRAND_ID, + TUXEDO_BRAND_NAME, + TUXEDO_BRAND_ACCENT, + TUXEDO_BRAND_LOGO_URL, +} from "@/lib/water-log/brand"; import { useLang } from "@/lib/water-log/lang-context"; // Server-side auth errors that should bounce the user to the PIN @@ -56,6 +80,7 @@ const SESSION_ERRORS = [ export function MobileWaterApp() { const [screen, setScreen] = useState("pin"); + const [activeTab, setActiveTab] = useState("headgates"); // `useLang()` reads from the in WaterFieldClient, // so it's always up-to-date when the user toggles via the toggle // in the headgate list header. @@ -125,6 +150,11 @@ export function MobileWaterApp() { setIrrigatorName(result.name ?? null); setScreen("headgates"); void loadHeadgates(); + // Cycle 4 — unify the PIN with Time Tracking. Best-effort: a + // worker who isn't also set up as a time-tracking worker just + // sees the regular PIN screen on the Time tab. Never blocks + // the water flow, never surfaces a 500 to the user. + void verifyTimeTrackingPin(TUXEDO_BRAND_ID, pin).catch(() => {}); }, [loadHeadgates], ); @@ -184,19 +214,47 @@ export function MobileWaterApp() { // ── Logout (from anywhere) ──────────────────────────────────── const handleLogout = useCallback(async () => { + // Cycle 4 — clear BOTH cookies. The water flow only knew about + // `wl_session`; the Time tab introduced `time_tracking_session`. try { await logoutWater(); } catch { // Even if the server logout fails, clear local state so the // user isn't stuck. } + try { + await logoutTimeTracking(); + } catch { + // Same — defensive cleanup. + } setIrrigatorName(null); setHeadgates([]); setSelectedHeadgate(null); setSubmittedLog(null); + setActiveTab("headgates"); setScreen("pin"); }, []); + // ── Tab change (Cycle 4) ────────────────────────────────────── + const handleTabChange = useCallback((next: ActiveTab) => { + setActiveTab(next); + if (next === "time") { + // Switching to Time drops the in-flight water flow (selected + // headgate + submitted log) so it doesn't linger when the + // worker comes back to Headgates. The headgate list itself + // is cached and will reload on demand. + setSubmittedLog(null); + setSelectedHeadgate(null); + setScreen("time"); + } else { + // Switching back to Headgates — restore the flow at the + // headgate list (we always have that state cached; the + // loadHeadgates effect picks up fresh data on mount). + if (headgates.length === 0) void loadHeadgates(); + setScreen("headgates"); + } + }, [headgates.length, loadHeadgates]); + // ── Switch on the current screen ────────────────────────────── // Compute the screen element first, then wrap it once in // . Wrapping per-case would remount the provider on @@ -258,11 +316,113 @@ export function MobileWaterApp() { /> ); break; + + case "time": + // Cycle 4 — Tuxedo-only Time tab. Same brand id / name / + // accent as the standalone /tuxedo/time-clock route so the + // embedded surface matches the standalone one pixel-for-pixel. + screenElement = ( + + ); + break; } - // `initialLang` is computed by the parent in - // WaterFieldClient — we just return the screen element here. - return screenElement; + // ── Post-PIN shell ──────────────────────────────────────────── + // The pre-PIN screen is bare (no nav). After PIN we wrap the + // screen element in a sticky tab bar so the worker can flip + // between Headgates and Time without re-entering credentials. + const isPostPin = screen !== "pin"; + + return ( +
+ {isPostPin && ( + + )} +
{screenElement}
+
+ ); +} + +// ── TabBar (Cycle 4) ─────────────────────────────────────────── +// Sticky two-tab top nav for the Tuxedo worker experience. +// Stays visible across Headgates → Log → Success AND on the Time +// tab. Extracted as a presentational component (no server actions, +// no effects) so it's pure and easy to iterate on visually. +function TabBar({ + activeTab, + onChange, + onLogout, + irrigatorName, +}: { + activeTab: ActiveTab; + onChange: (next: ActiveTab) => void; + onLogout: () => void; + irrigatorName?: string; +}) { + const t = useLang().t; + return ( +
+
+ + +
+
+ {irrigatorName && ( + + {irrigatorName} + + )} + +
+
+ ); } export default MobileWaterApp; \ No newline at end of file diff --git a/src/components/water/mobile/types.ts b/src/components/water/mobile/types.ts index b105341..d43d6af 100644 --- a/src/components/water/mobile/types.ts +++ b/src/components/water/mobile/types.ts @@ -49,8 +49,16 @@ export function isIntegerUnit(unit: string): boolean { return unit === "holes"; } -/** Top-level screen identifiers — drives the state machine. */ -export type Screen = "pin" | "headgates" | "log" | "success"; +/** Top-level screen identifiers — drives the state machine. + * + * Cycle 4 adds "time" for the Tuxedo-only Time tab, which renders + * `TimeTrackingFieldClient` instead of the existing water-entry + * screens. The "pin" → "headgates"/"log"/"success" flow is unchanged. + */ +export type Screen = "pin" | "headgates" | "log" | "success" | "time"; + +/** Tuxedo-only Time-tab branding (Cycle 4). */ +export type ActiveTab = "headgates" | "time"; /** A submitted log entry (only kept locally for the success screen). */ export type SubmittedLog = { diff --git a/src/lib/water-log/brand.ts b/src/lib/water-log/brand.ts index 23584ff..00d39aa 100644 --- a/src/lib/water-log/brand.ts +++ b/src/lib/water-log/brand.ts @@ -11,4 +11,13 @@ * `TUXEDO_BRAND_ID` with `effectiveBrandId` from `getAdminUser()` — * which is the standard pattern used throughout the rest of the app. */ -export const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; \ No newline at end of file +export const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de"; + +/** Display surfaces (Cycle 4) — single source of truth for the + * Tuxedo worker experience so the standalone `/tuxedo/time-clock` + * page and the embedded Time tab inside `/water` stay in sync. + * Bump these together when the brand refreshes its accent/logo. */ +export const TUXEDO_BRAND_NAME = "Tuxedo Corn"; +export const TUXEDO_BRAND_ACCENT = "green"; +/** `null` until the Tuxedo brand gets a logo asset uploaded. */ +export const TUXEDO_BRAND_LOGO_URL: string | null = null; \ No newline at end of file diff --git a/src/lib/water-log/i18n.ts b/src/lib/water-log/i18n.ts index c361118..a0ca81b 100644 --- a/src/lib/water-log/i18n.ts +++ b/src/lib/water-log/i18n.ts @@ -138,6 +138,13 @@ export type Labels = { minutesAgo: (n: number) => string; hoursAgo: (n: number) => string; }; + /** Tuxedo worker post-PIN shell (Cycle 4) — names for the two + * top tabs and the inline logout affordance. */ + tab: { + headgates: string; + time: string; + logout: string; + }; }; export const LABELS: Record = { @@ -231,6 +238,11 @@ export const LABELS: Record = { minutesAgo: (n) => `${n}m ago`, hoursAgo: (n) => `${n}h ago`, }, + tab: { + headgates: "Headgates", + time: "Time", + logout: "Log out", + }, }, es: { common: { @@ -323,6 +335,11 @@ export const LABELS: Record = { minutesAgo: (n) => `hace ${n}m`, hoursAgo: (n) => `hace ${n}h`, }, + tab: { + headgates: "Compuertas", + time: "Tiempo", + logout: "Salir", + }, }, };