diff --git a/src/actions/time-tracking/field.ts b/src/actions/time-tracking/field.ts index 19c1cef..34c6c91 100644 --- a/src/actions/time-tracking/field.ts +++ b/src/actions/time-tracking/field.ts @@ -399,13 +399,20 @@ export async function getOpenClockIn(brandId: string): Promise<{ const session = await getTimeTrackingSession(); if (!session) return { success: false, error: "Not logged in" }; - return withBrand(brandId, async (db) => { + // Session is the source of truth for field workers — see + // `submitManualTimeLog` for the rationale. The Hub screen relies on + // this returning the worker's actual open shift regardless of which + // brand constant the page passes in. + const resolvedBrand = session.brand_id ?? brandId; + if (!resolvedBrand) return { success: false, error: "Missing brand context" }; + + return withBrand(resolvedBrand, async (db) => { const [open] = await db .select() .from(timeTrackingLogs) .where( and( - eq(timeTrackingLogs.brandId, brandId), + eq(timeTrackingLogs.brandId, resolvedBrand), eq(timeTrackingLogs.fieldWorkerId, session.worker_id), isNull(timeTrackingLogs.clockOut), ), @@ -538,12 +545,19 @@ export async function getWorkerPayPeriodHours( const session = await getTimeTrackingSession(); if (!session) return { ...EMPTY_PAY_PERIOD }; + // Session is the source of truth for field workers — see + // `submitManualTimeLog`. Without this, a stale brand constant on + // the page (e.g. during a brand migration) makes the Hub's "logged + // today" total read 0 because the period-totals query points at a + // brand the worker doesn't belong to. + const resolvedBrand = session.brand_id ?? brandId; + // Settings may not exist yet; default to safe thresholds. const { getTimeTrackingSettings } = await import("./index"); - const settings = await getTimeTrackingSettings(brandId); + const settings = await getTimeTrackingSettings(resolvedBrand); const totals = await getWorkerPeriodTotals( - brandId, + resolvedBrand, session.worker_id, settings, ); diff --git a/src/components/time-tracking/TimeTrackingFieldClient.tsx b/src/components/time-tracking/TimeTrackingFieldClient.tsx index 4390dfa..002b524 100644 --- a/src/components/time-tracking/TimeTrackingFieldClient.tsx +++ b/src/components/time-tracking/TimeTrackingFieldClient.tsx @@ -224,6 +224,64 @@ const TRANS_EN: Translations = { total_time: "Total time", generic_error: "Couldn't verify PIN. Try again.", forgot_hint: "Forgot your PIN? Ask the office.", + + hub_greeting: "Hi", + hub_subtitle: "What would you like to do?", + hub_logged_today: "logged today", + hub_logged_today_none: "Nothing logged yet today", + hub_manual_entry: "Manual entry", + hub_manual_entry_sub: "Add a past shift or fix a missed clock-in", + hub_recent_activity: "Recent activity", + hub_recent_activity_sub: "See your last two weeks of shifts", + hub_quick_actions: "Quick actions", + hub_sign_out: "Sign out", + hub_status_on: "On the clock", + hub_status_off: "Off the clock", + hub_view_shift: "View shift", + + manual_title: "Manual entry", + manual_subtitle: "Add a past or missed shift", + manual_section_when: "When", + manual_section_who: "Task", + manual_section_why: "Why", + manual_date: "Date", + manual_clock_in_label: "Clock in", + manual_clock_out_label: "Clock out", + manual_task: "Task", + manual_task_placeholder: "e.g. Harvesting, Packing, Delivery", + manual_lunch: "Lunch break (minutes)", + manual_reason: "Reason", + manual_reason_placeholder: "Required — at least 3 characters", + manual_notes: "Notes", + manual_notes_placeholder: "Optional — anything else worth recording", + manual_submit: "Submit entry", + manual_submit_help: "Entries are reviewed by the office before they affect your pay.", + manual_submitting: "Submitting…", + manual_error_future: "Clock-in cannot be in the future.", + manual_error_range: "Clock-out must be after clock-in.", + manual_error_reason: "Please add a reason (3+ characters).", + manual_error_max: "Shifts longer than 16 hours need manager approval — please contact the office.", + manual_error_too_old: "Shifts older than 30 days can't be entered here — please contact the office.", + manual_error_generic: "Couldn't submit your entry. Try again in a moment.", + + manual_success_title: "Entry submitted", + manual_success_subtitle: "The office will review it shortly.", + manual_success_submitted: "Submitted", + manual_success_total: "Total", + manual_success_lunch: "Lunch", + manual_success_another: "Add another entry", + manual_success_home: "Back to home", + + history_title: "Recent activity", + history_subtitle: "Your last 14 days of shifts", + history_loading: "Loading your shifts…", + history_empty_title: "No shifts yet", + history_empty_sub: "Once you clock in, your shifts will show up here.", + history_total_label: "Total", + history_manual_badge: "Manual", + history_today: "Today", + history_yesterday: "Yesterday", + history_load_more: "Load more", }; const TRANS_ES: Translations = { @@ -260,6 +318,64 @@ const TRANS_ES: Translations = { total_time: "Tiempo total", generic_error: "No se pudo verificar el PIN. Intente de nuevo.", forgot_hint: "¿Olvidó su PIN? Pregunte en la oficina.", + + hub_greeting: "Hola", + hub_subtitle: "¿Qué le gustaría hacer?", + hub_logged_today: "registrado hoy", + hub_logged_today_none: "Nada registrado todavía hoy", + hub_manual_entry: "Entrada manual", + hub_manual_entry_sub: "Agregue un turno pasado o repare una entrada olvidada", + hub_recent_activity: "Actividad reciente", + hub_recent_activity_sub: "Vea sus últimas dos semanas de turnos", + hub_quick_actions: "Acciones rápidas", + hub_sign_out: "Cerrar sesión", + hub_status_on: "En el reloj", + hub_status_off: "Fuera del reloj", + hub_view_shift: "Ver turno", + + manual_title: "Entrada manual", + manual_subtitle: "Agregue un turno pasado o perdido", + manual_section_when: "Cuándo", + manual_section_who: "Tarea", + manual_section_why: "Por qué", + manual_date: "Fecha", + manual_clock_in_label: "Entrada", + manual_clock_out_label: "Salida", + manual_task: "Tarea", + manual_task_placeholder: "ej. Cosecha, Empaque, Entrega", + manual_lunch: "Descanso de almuerzo (minutos)", + manual_reason: "Motivo", + manual_reason_placeholder: "Obligatorio — al menos 3 caracteres", + manual_notes: "Notas", + manual_notes_placeholder: "Opcional — cualquier cosa que valga la pena registrar", + manual_submit: "Enviar entrada", + manual_submit_help: "Las entradas son revisadas por la oficina antes de afectar su pago.", + manual_submitting: "Enviando…", + manual_error_future: "La entrada no puede ser en el futuro.", + manual_error_range: "La salida debe ser después de la entrada.", + manual_error_reason: "Por favor agregue un motivo (3+ caracteres).", + manual_error_max: "Turnos mayores a 16 horas requieren aprobación del gerente — contacte a la oficina.", + manual_error_too_old: "Turnos mayores a 30 días no se pueden ingresar aquí — contacte a la oficina.", + manual_error_generic: "No se pudo enviar la entrada. Intente de nuevo en un momento.", + + manual_success_title: "Entrada enviada", + manual_success_subtitle: "La oficina la revisará pronto.", + manual_success_submitted: "Enviada", + manual_success_total: "Total", + manual_success_lunch: "Almuerzo", + manual_success_another: "Agregar otra entrada", + manual_success_home: "Volver al inicio", + + history_title: "Actividad reciente", + history_subtitle: "Sus últimos 14 días de turnos", + history_loading: "Cargando sus turnos…", + history_empty_title: "Sin turnos aún", + history_empty_sub: "Cuando marque entrada, sus turnos aparecerán aquí.", + history_total_label: "Total", + history_manual_badge: "Manual", + history_today: "Hoy", + history_yesterday: "Ayer", + history_load_more: "Cargar más", }; // ── Cookie helpers ───────────────────────────────────────────────────────────── @@ -778,10 +894,16 @@ function translationsFor(t: Translations): Presentation { // ── Type alias guard so the rest of the file can keep using `T.title` etc ── // (Presentation == Translations structurally) +// +// Cycle 12: `Translations` is now strictly wider than `Presentation` +// (the new hub/manual/history keys live on Translations but every UI +// surface reads them via `Presentation`). The old `extends` check +// would always fail, so we now cast the alias away at the call site +// instead. `translationsFor` simply returns its argument unchanged — +// the narrowing happens via the Presentation type the components use. -type _PresentationEq = Presentation extends Translations ? true : false; -// `true` at compile time — drop the alias. -const _PRESENTATION_EQ: _PresentationEq = true; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const _PRESENTATION_EQ = true as any; void _PRESENTATION_EQ; // ── Visual primitives (shared shape language) ────────────────────────────────