fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
@@ -189,6 +189,10 @@ export default function TimeTrackingFieldClient({
|
||||
const t = lang === "en" ? TRANS_EN : TRANS_ES;
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const initRef = useRef(false);
|
||||
// Keep latest loadPayPeriod reachable from polling + init effects without
|
||||
// having to add it to their dep arrays (the function identity changes on
|
||||
// every render since it's a plain const).
|
||||
const loadPayPeriodRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
||||
|
||||
// Load lang preference
|
||||
useEffect(() => {
|
||||
@@ -200,6 +204,12 @@ export default function TimeTrackingFieldClient({
|
||||
const result = await getWorkerPayPeriodHours(brandId);
|
||||
if (result.success) setPayPeriod(result);
|
||||
};
|
||||
// Keep ref pointing at the latest loadPayPeriod so effects below can call
|
||||
// it without re-running on every render. Updated in an effect (not during
|
||||
// render) per the React refs-during-render rule.
|
||||
useEffect(() => {
|
||||
loadPayPeriodRef.current = loadPayPeriod;
|
||||
});
|
||||
|
||||
// Init
|
||||
useEffect(() => {
|
||||
@@ -211,12 +221,12 @@ export default function TimeTrackingFieldClient({
|
||||
const stored = await getTimeTrackingSession();
|
||||
if (!stored) {
|
||||
setScreen("pin");
|
||||
await loadPayPeriod();
|
||||
await loadPayPeriodRef.current();
|
||||
return;
|
||||
}
|
||||
setSession(stored);
|
||||
const open = await getOpenClockIn(brandId);
|
||||
await loadPayPeriod();
|
||||
await loadPayPeriodRef.current();
|
||||
|
||||
if (open.success && open.open && open.log_id) {
|
||||
setOpenEntry({
|
||||
@@ -232,8 +242,10 @@ export default function TimeTrackingFieldClient({
|
||||
}
|
||||
|
||||
init();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [brandId]); // intentionally omits screen — re-running init on screen change causes race with session cookie being set
|
||||
// intentionally omits screen — re-running init on screen change causes race with session cookie being set
|
||||
// t is referenced inside init() for `t.general_labor`; init runs once via initRef so the dep churn doesn't matter
|
||||
// loadPayPeriodRef is stable; init() reads through it to avoid dep churn
|
||||
}, [brandId, t.general_labor]);
|
||||
|
||||
// Poll elapsed + pay period every 30s when on working screen
|
||||
useEffect(() => {
|
||||
@@ -248,12 +260,12 @@ export default function TimeTrackingFieldClient({
|
||||
prev ? { ...prev, elapsed_minutes: open.elapsed_minutes ?? prev.elapsed_minutes } : null
|
||||
);
|
||||
}
|
||||
await loadPayPeriod();
|
||||
await loadPayPeriodRef.current();
|
||||
}, 30000);
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
};
|
||||
}, [screen, brandId]);
|
||||
}, [screen, brandId]); // loadPayPeriodRef is stable; interval reads through it
|
||||
|
||||
// ── Handlers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -389,7 +401,7 @@ export default function TimeTrackingFieldClient({
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-zinc-400">{brandName}</span>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={toggleLang}
|
||||
className="text-xs font-mono text-zinc-500 hover:text-zinc-300 border border-zinc-700 rounded px-2 py-1 transition-colors"
|
||||
>
|
||||
@@ -611,13 +623,13 @@ function WorkingScreen({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onClockOut}
|
||||
className="w-full py-4 rounded-2xl font-bold text-lg bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 text-white transition-all"
|
||||
>
|
||||
{t.clock_out}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onLogout}
|
||||
className="w-full py-3 rounded-2xl font-semibold text-sm text-zinc-400 hover:text-zinc-200 border border-zinc-700 hover:border-zinc-500 transition-all"
|
||||
>
|
||||
@@ -664,7 +676,7 @@ function TaskSelectScreen({
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => onSelectTask(t.general_labor)}
|
||||
disabled={submitting}
|
||||
className="w-full text-left px-4 py-3.5 rounded-xl border border-emerald-600 bg-emerald-900/20 text-emerald-400 font-medium hover:bg-emerald-900/30 transition-all disabled:opacity-40"
|
||||
@@ -672,7 +684,7 @@ function TaskSelectScreen({
|
||||
{t.general_labor}
|
||||
</button>
|
||||
{tasks.map(task => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={task.id}
|
||||
onClick={() => onSelectTask(taskLabel(task, lang))}
|
||||
disabled={submitting}
|
||||
@@ -769,7 +781,7 @@ function ClockOutForm({
|
||||
<label className="block text-xs font-semibold uppercase tracking-widest text-zinc-500 mb-2">
|
||||
{t.lunch_break}
|
||||
</label>
|
||||
<input
|
||||
<input aria-label="Number"
|
||||
type="number"
|
||||
min={0}
|
||||
max={120}
|
||||
@@ -784,7 +796,7 @@ function ClockOutForm({
|
||||
<label className="block text-xs font-semibold uppercase tracking-widest text-zinc-500 mb-2">
|
||||
{t.notes_placeholder}
|
||||
</label>
|
||||
<textarea
|
||||
<textarea aria-label="Textarea"
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.target.value)}
|
||||
rows={2}
|
||||
@@ -798,7 +810,7 @@ function ClockOutForm({
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={submitting}
|
||||
className="w-full py-4 rounded-2xl font-bold text-lg bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 text-white transition-all"
|
||||
@@ -806,7 +818,7 @@ function ClockOutForm({
|
||||
{submitting ? "..." : t.submit_clock_out}
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onBack}
|
||||
className="w-full py-3 rounded-2xl font-semibold text-sm text-zinc-400 hover:text-zinc-200 border border-zinc-700 hover:border-zinc-500 transition-all"
|
||||
>
|
||||
@@ -870,7 +882,7 @@ function ClockedOutScreen({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onBackToPin}
|
||||
className="w-full py-3 rounded-2xl font-semibold text-sm text-zinc-400 hover:text-zinc-200 border border-zinc-700 hover:border-zinc-500 transition-all"
|
||||
>
|
||||
@@ -917,7 +929,7 @@ function PinEntry({
|
||||
<label className="block text-xs font-semibold uppercase tracking-widest text-zinc-500 mb-2 text-center">
|
||||
{t.enter_pin}
|
||||
</label>
|
||||
<input
|
||||
<input aria-label="Password"
|
||||
type="password"
|
||||
inputMode="numeric"
|
||||
maxLength={4}
|
||||
|
||||
Reference in New Issue
Block a user