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 (
+