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:
Nora
2026-06-25 23:49:37 -06:00
parent 4d295ef062
commit 0ac4beaaa8
580 changed files with 52565 additions and 4953 deletions
@@ -68,7 +68,7 @@ export default function AdminLookupPage({ brandId }: { brandId: string }) {
return (
<div className="space-y-5">
{/* Scan button */}
<button
<button type="button"
onClick={() => setShowScanModal(true)}
className="w-full rounded-xl bg-emerald-600 px-5 py-4 text-sm font-bold text-white hover:bg-emerald-700 transition-colors flex items-center justify-center gap-2 shadow-sm"
>
@@ -91,7 +91,7 @@ export default function AdminLookupPage({ brandId }: { brandId: string }) {
<p className="mt-1 text-sm text-stone-500">Search by lot number or crop type to find a harvest lot.</p>
</div>
<form onSubmit={handleSearch} className="flex gap-3 p-6">
<input
<input aria-label=". TC 20260519 001 Or Sweet Corn"
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
+44 -32
View File
@@ -177,31 +177,43 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
const [filterStatus, setFilterStatus] = useState<string>("all");
const [showOnlyIssues, setShowOnlyIssues] = useState(false);
useEffect(() => {
if (open && startDate && endDate) {
fetchComplianceData();
}
}, [open, startDate, endDate]);
const [refreshTick, setRefreshTick] = useState(0);
async function fetchComplianceData() {
setLoading(true);
try {
const res = await fetch(
`/api/route-trace/fsma-compliance?brandId=${brandId}&startDate=${startDate}&endDate=${endDate}`
);
if (res.ok) {
const json = await res.json();
setData(json);
} else {
setData(null);
}
} catch {
setData(null);
} finally {
setLoading(false);
}
function fetchComplianceData() {
setRefreshTick((n) => n + 1);
}
useEffect(() => {
if (!open || !startDate || !endDate) return;
let cancelled = false;
async function load() {
try {
const res = await fetch(
`/api/route-trace/fsma-compliance?brandId=${brandId}&startDate=${startDate}&endDate=${endDate}`
);
if (cancelled) return;
if (res.ok) {
const json = await res.json();
setData(json);
} else {
setData(null);
}
} catch {
if (!cancelled) setData(null);
}
}
load().finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [open, startDate, endDate, brandId, refreshTick]);
function handleDownload() {
const url = `/api/route-trace/fsma-report?brandId=${brandId}&startDate=${startDate}&endDate=${endDate}&format=csv`;
window.location.href = url;
@@ -221,7 +233,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
return (
<>
<button
<button type="button"
onClick={() => setOpen(true)}
className="rounded-xl border border-stone-200 bg-white px-5 py-3 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
>
@@ -278,7 +290,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
One-up / one-down traceability report
</p>
</div>
<button
<button type="button"
onClick={() => setOpen(false)}
className="flex h-9 w-9 items-center justify-center rounded-full transition-all"
style={{ background: "rgba(0, 0, 0, 0.04)", color: "rgba(0, 0, 0, 0.4)" }}
@@ -297,7 +309,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<label className="block text-xs font-semibold text-stone-500 mb-1.5 uppercase tracking-wider">
Start Date
</label>
<input
<input aria-label="Date"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
@@ -308,14 +320,14 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<label className="block text-xs font-semibold text-stone-500 mb-1.5 uppercase tracking-wider">
End Date
</label>
<input
<input aria-label="Date"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-stone-400"
/>
</div>
<button
<button type="button"
onClick={fetchComplianceData}
className="rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-bold text-white hover:bg-emerald-700 transition-colors"
>
@@ -381,7 +393,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<div className="flex items-center gap-3 mb-4">
<div className="relative flex-1">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400">{Icons.search("h-4 w-4")}</span>
<input
<input aria-label="Search Lot Number, Crop, Field…"
type="text"
placeholder="Search lot number, crop, field…"
value={search}
@@ -389,7 +401,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
className="w-full pl-9 pr-4 py-2 rounded-xl border border-stone-200 bg-white text-sm outline-none focus:border-stone-400"
/>
</div>
<select
<select aria-label="Select"
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm outline-none focus:border-stone-400"
@@ -523,7 +535,7 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
<div className="flex flex-wrap gap-1">
{lot.issues.map((issue, i) => (
<span
key={i}
key={`${lot.lot_number}-issue-${i}-${issue}`}
className="inline-flex items-center rounded bg-red-50 px-1.5 py-0.5 text-[10px] font-medium text-red-600"
>
{issue}
@@ -569,13 +581,13 @@ export default function FsmaReportModal({ brandId }: { brandId: string }) {
</span>
</div>
<div className="flex items-center gap-3">
<button
<button type="button"
onClick={() => setOpen(false)}
className="rounded-xl border border-stone-200 px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
>
Close
</button>
<button
<button type="button"
onClick={handleDownload}
disabled={loading || !data}
className="rounded-xl bg-blue-600 px-5 py-2 text-sm font-bold text-white hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center gap-2"
+15 -15
View File
@@ -95,7 +95,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-stone-800 mb-2">Crop Type *</label>
<input
<input aria-label=". Sweet Corn"
type="text"
value={crop_type}
onChange={(e) => setCropType(e.target.value)}
@@ -106,7 +106,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-sm font-semibold text-stone-800 mb-2">Variety</label>
<input
<input aria-label=". Golden Bantam"
type="text"
value={variety}
onChange={(e) => setVariety(e.target.value)}
@@ -118,7 +118,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div>
<label className="block text-sm font-semibold text-stone-800 mb-2">Harvest Date *</label>
<input
<input aria-label="Date"
type="date"
value={harvest_date}
onChange={(e) => setHarvestDate(e.target.value)}
@@ -133,7 +133,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Field / Location *</label>
<input
<input aria-label=". North Field"
type="text"
value={field_location}
onChange={(e) => setFieldLocation(e.target.value)}
@@ -145,7 +145,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Field Block</label>
<input
<input aria-label=". Block A"
type="text"
value={field_block}
onChange={(e) => setFieldBlock(e.target.value)}
@@ -155,7 +155,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Worker / Harvest Lead</label>
<input
<input aria-label=". Maria S."
type="text"
value={worker_name}
onChange={(e) => setWorkerName(e.target.value)}
@@ -173,7 +173,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Actual Qty</label>
<input
<input aria-label=". 4800"
type="number"
value={quantity_lbs}
onChange={(e) => setQuantityLbs(e.target.value)}
@@ -186,7 +186,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Yield Est.</label>
<div className="flex rounded-xl border border-stone-200 bg-stone-50 overflow-hidden focus-within:border-emerald-600 focus-within:bg-white transition-colors">
<input
<input aria-label=". 5000"
type="number"
value={yield_estimate_lbs}
onChange={(e) => setYieldEstimateLbs(e.target.value)}
@@ -195,7 +195,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
step="0.01"
className="flex-1 px-3 py-3.5 text-base outline-none bg-transparent"
/>
<select
<select aria-label="Select"
value={yield_unit}
onChange={(e) => setYieldUnit(e.target.value)}
className="px-2 py-3.5 text-sm font-semibold text-stone-600 bg-stone-100 outline-none border-l border-stone-200 cursor-pointer"
@@ -211,7 +211,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
</select>
</div>
{yield_unit === "custom" && (
<input
<input aria-label="Unit Name…"
type="text"
placeholder="Unit name…"
value={customYieldUnit}
@@ -229,7 +229,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Bin ID</label>
<input
<input aria-label=". BIN 001"
type="text"
value={bin_id}
onChange={(e) => setBinId(e.target.value)}
@@ -239,7 +239,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Container ID</label>
<input
<input aria-label=". CONT A42"
type="text"
value={container_id}
onChange={(e) => setContainerId(e.target.value)}
@@ -251,7 +251,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<div className="mt-3 grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Pallets</label>
<input
<input aria-label=". 4"
type="number"
value={pallets}
onChange={(e) => setPallets(e.target.value)}
@@ -262,7 +262,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Packer Name</label>
<input
<input aria-label=". Jose R."
type="text"
value={packer_name}
onChange={(e) => setPackerName(e.target.value)}
@@ -284,7 +284,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
<span className="text-stone-500">{notesOpen ? Icons.chevronUp("h-4 w-4") : Icons.chevronDown("h-4 w-4")}</span>
</button>
{notesOpen && (
<textarea
<textarea aria-label="Spray Records, Weather Conditions, Or Other Harvest Details..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Spray records, weather conditions, or other harvest details..."
+25 -21
View File
@@ -1,7 +1,6 @@
"use client";
/* eslint-disable react-hooks/set-state-in-effect */
import { useState, useTransition, useEffect } from "react";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { createHarvestLot } from "@/actions/route-trace/lots";
@@ -33,8 +32,13 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
const [pallets, setPallets] = useState("");
const [notes, setNotes] = useState("");
// Reset form when modal opens
useEffect(() => {
// Track the previous isOpen value to detect a transition from closed → open,
// which is when we want to reset the form. The initial value is a literal
// (not derived from a prop) so a stale useState copy can't occur if the
// parent passes a new isOpen value.
const [prevIsOpen, setPrevIsOpen] = useState(false);
if (isOpen !== prevIsOpen) {
setPrevIsOpen(isOpen);
if (isOpen) {
setCropType("");
setVariety("");
@@ -54,7 +58,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
setError(null);
setNotesOpen(false);
}
}, [isOpen]);
}
if (!isOpen) return null;
@@ -118,7 +122,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<p className="text-sm" style={{ color: "var(--admin-text-muted)" }}>Quick entry scan or fill in the fields below</p>
</div>
</div>
<button
<button type="button"
onClick={onClose}
className="flex h-9 w-9 items-center justify-center rounded-xl border transition-colors hover:bg-stone-50"
style={{ borderColor: "var(--admin-border)" }}
@@ -147,7 +151,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: "var(--admin-text-primary)" }}>Crop Type *</label>
<input
<input aria-label=". Sweet Corn"
type="text"
value={crop_type}
onChange={(e) => setCropType(e.target.value)}
@@ -163,7 +167,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: "var(--admin-text-primary)" }}>Variety</label>
<input
<input aria-label=". Golden Bantam"
type="text"
value={variety}
onChange={(e) => setVariety(e.target.value)}
@@ -180,7 +184,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: "var(--admin-text-primary)" }}>Harvest Date *</label>
<input
<input aria-label="Date"
type="date"
value={harvest_date}
onChange={(e) => setHarvestDate(e.target.value)}
@@ -200,7 +204,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div className="space-y-3">
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Field / Location *</label>
<input
<input aria-label=". North Field"
type="text"
value={field_location}
onChange={(e) => setFieldLocation(e.target.value)}
@@ -217,7 +221,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Field Block</label>
<input
<input aria-label=". Block A"
type="text"
value={field_block}
onChange={(e) => setFieldBlock(e.target.value)}
@@ -232,7 +236,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Worker / Harvest Lead</label>
<input
<input aria-label=". Maria S."
type="text"
value={worker_name}
onChange={(e) => setWorkerName(e.target.value)}
@@ -255,7 +259,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Actual Qty</label>
<input
<input aria-label=". 4800"
type="number"
value={quantity_lbs}
onChange={(e) => setQuantityLbs(e.target.value)}
@@ -273,7 +277,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Yield Est.</label>
<div className="flex rounded-xl border overflow-hidden transition-colors" style={{ borderColor: "var(--admin-border)", backgroundColor: "var(--admin-bg-subtle)" }}>
<input
<input aria-label=". 5000"
type="number"
value={yield_estimate_lbs}
onChange={(e) => setYieldEstimateLbs(e.target.value)}
@@ -283,7 +287,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
className="flex-1 px-3 py-3.5 text-base outline-none bg-transparent"
style={{ color: "var(--admin-text-primary)" }}
/>
<select
<select aria-label="Select"
value={yield_unit}
onChange={(e) => setYieldUnit(e.target.value)}
className="px-2 py-3.5 text-sm font-semibold outline-none border-l cursor-pointer"
@@ -304,7 +308,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
</select>
</div>
{yield_unit === "custom" && (
<input
<input aria-label="Unit Name…"
type="text"
placeholder="Unit name…"
value={customYieldUnit}
@@ -327,7 +331,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Bin ID</label>
<input
<input aria-label=". BIN 001"
type="text"
value={bin_id}
onChange={(e) => setBinId(e.target.value)}
@@ -342,7 +346,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Container ID</label>
<input
<input aria-label=". CONT A42"
type="text"
value={container_id}
onChange={(e) => setContainerId(e.target.value)}
@@ -359,7 +363,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
<div className="mt-3 grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Pallets</label>
<input
<input aria-label=". 4"
type="number"
value={pallets}
onChange={(e) => setPallets(e.target.value)}
@@ -375,7 +379,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Packer Name</label>
<input
<input aria-label=". Jose R."
type="text"
value={packer_name}
onChange={(e) => setPackerName(e.target.value)}
@@ -405,7 +409,7 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
</svg>
</button>
{notesOpen && (
<textarea
<textarea aria-label="Spray Records, Weather Conditions, Or Other Harvest Details..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Spray records, weather conditions, or other harvest details..."
+15 -15
View File
@@ -319,7 +319,7 @@ export default function LotDetailPanel({
{(isInTransit || canMarkLoaded) && (
<div className="flex flex-wrap gap-2 w-full sm:w-auto">
{isInTransit && (
<button
<button type="button"
onClick={handleMarkAtShed}
disabled={isPending}
className="group relative rounded-xl border-2 border-[#90caf9] bg-[#e3f2fd] px-3 sm:px-4 py-2 text-xs sm:text-sm font-bold text-[#1565c0] hover:bg-[#bbdefb] transition-all duration-200 disabled:opacity-50 flex items-center gap-1.5 sm:gap-2 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex-1 sm:flex-initial justify-center"
@@ -330,7 +330,7 @@ export default function LotDetailPanel({
</button>
)}
{canMarkLoaded && (
<button
<button type="button"
onClick={handleMarkLoaded}
disabled={isPending}
className="group relative rounded-xl border-2 border-[#ffe082] bg-[#fff8e1] px-3 sm:px-4 py-2 text-xs sm:text-sm font-bold text-[#f57c00] hover:bg-[#ffecb3] transition-all duration-200 disabled:opacity-50 flex items-center gap-1.5 sm:gap-2 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex-1 sm:flex-initial justify-center"
@@ -343,7 +343,7 @@ export default function LotDetailPanel({
</div>
)}
<div className="flex gap-2 w-full sm:w-auto">
<button
<button type="button"
onClick={() => setShowSticker(true)}
className="group rounded-xl border border-[var(--admin-border)] bg-white px-3 sm:px-4 py-2 text-xs sm:text-sm font-semibold text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)] hover:border-[var(--admin-text-muted)] transition-all duration-200 flex items-center gap-1.5 sm:gap-2 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex-1 sm:flex-initial justify-center"
>
@@ -351,7 +351,7 @@ export default function LotDetailPanel({
<span className="hidden xs:inline">Print Sticker</span>
</button>
{nextStatuses.length > 0 && (
<button
<button type="button"
onClick={() => { setNewStatus(nextStatuses[0]); setShowStatusModal(true); }}
className="group relative rounded-xl bg-[var(--admin-accent)] px-3 sm:px-4 py-2 text-xs sm:text-sm font-bold text-white hover:bg-[var(--admin-accent-hover)] transition-all duration-200 flex items-center gap-1.5 sm:gap-2 shadow-md hover:shadow-lg hover:-translate-y-0.5 flex-1 sm:flex-initial justify-center"
>
@@ -415,7 +415,7 @@ export default function LotDetailPanel({
{/* Quick actions */}
<div className="flex gap-2 sm:gap-3 border-t border-[var(--admin-border-light)] px-4 sm:px-6 py-3 sm:py-4 bg-[var(--admin-bg-subtle)]/20 flex-wrap">
<button
<button type="button"
onClick={() => setShowBinModal(true)}
className="group rounded-xl border border-[var(--admin-border)] bg-white px-3 sm:px-4 py-2 text-xs sm:text-sm font-semibold text-[var(--admin-text-secondary)] hover:bg-[#fff3e0] hover:border-[#ffe082] hover:text-[#f57c00] transition-all duration-200 flex items-center gap-1.5 sm:gap-2 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex-1 sm:flex-initial justify-center"
>
@@ -525,7 +525,7 @@ export default function LotDetailPanel({
{Icons.package("w-5 h-5 sm:w-6 sm:h-6 text-[var(--admin-text-muted)]")}
</div>
<p className="text-xs sm:text-sm text-[var(--admin-text-muted)] mb-4">This lot has not been assigned to any orders yet.</p>
<button
<button type="button"
onClick={() => setShowUsedModal(true)}
className="rounded-xl bg-[var(--admin-bg-subtle)] px-4 py-2 text-xs sm:text-sm font-semibold text-[var(--admin-text-secondary)] hover:bg-[var(--admin-border-light)] transition-colors border border-[var(--admin-border)]"
>
@@ -599,7 +599,7 @@ export default function LotDetailPanel({
</div>
</Link>
))}
<button
<button type="button"
onClick={() => setShowUsedModal(true)}
className="w-full mt-2 rounded-xl border-2 border-dashed border-[var(--admin-border)] py-3 text-xs font-bold text-[var(--admin-text-muted)] hover:border-[var(--admin-accent)] hover:text-[var(--admin-accent)] hover:bg-[var(--admin-accent-light)]/30 transition-all duration-200"
>
@@ -751,7 +751,7 @@ export default function LotDetailPanel({
<div className="space-y-4 p-6">
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">New Status</label>
<select
<select aria-label="Select"
value={newStatus}
onChange={(e) => setNewStatus(e.target.value)}
required
@@ -770,7 +770,7 @@ export default function LotDetailPanel({
</div>
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Location</label>
<input
<input aria-label=". Warehouse A"
type="text"
value={location}
onChange={(e) => setLocation(e.target.value)}
@@ -780,7 +780,7 @@ export default function LotDetailPanel({
</div>
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Notes</label>
<textarea
<textarea aria-label="Any Notes..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Any notes..."
@@ -821,7 +821,7 @@ export default function LotDetailPanel({
<div className="space-y-4 p-6">
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Bin or Container ID *</label>
<input
<input aria-label=". BIN 042"
type="text"
value={binId}
onChange={(e) => setBinId(e.target.value)}
@@ -832,7 +832,7 @@ export default function LotDetailPanel({
</div>
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Notes</label>
<textarea
<textarea aria-label="Weight, Count, Or Other Details..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Weight, count, or other details..."
@@ -875,7 +875,7 @@ export default function LotDetailPanel({
<div className="space-y-4 p-6">
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Order ID *</label>
<input
<input aria-label=". 4f3e2a1b ..."
type="text"
value={usedOrderId}
onChange={(e) => setUsedOrderId(e.target.value.toUpperCase())}
@@ -886,7 +886,7 @@ export default function LotDetailPanel({
</div>
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Quantity Used (lbs)</label>
<input
<input aria-label="Leave Blank To Auto Detect"
type="number"
value={usedQty}
onChange={(e) => setUsedQty(e.target.value)}
@@ -897,7 +897,7 @@ export default function LotDetailPanel({
</div>
<div>
<label className="block text-sm font-semibold text-[var(--admin-text-secondary)] mb-1.5">Notes</label>
<textarea
<textarea aria-label="Any Notes..."
value={usedNotes}
onChange={(e) => setUsedNotes(e.target.value)}
placeholder="Any notes..."
+22 -15
View File
@@ -161,9 +161,16 @@ export default function LotListTable({
async function handleBulkMarkLoaded() {
if (selected.size === 0) return;
const ids = [...selected];
for (const id of ids) {
await updateHarvestLotStatus(id, "in_transit");
}
await Promise.all(
ids.map(async (id) => {
try {
await updateHarvestLotStatus(id, "in_transit");
} catch (err) {
// A single failure must not block other lots from being marked.
console.warn("[bulk-mark-loaded] updateHarvestLotStatus failed for", id, err);
}
})
);
window.location.reload();
}
@@ -182,7 +189,7 @@ export default function LotListTable({
<p className="text-sm text-stone-500">{filtered.length} lot{filtered.length !== 1 ? "s" : ""}</p>
</div>
</div>
<button
<button type="button"
onClick={onCreateNew}
className="inline-flex items-center gap-2 rounded-xl bg-emerald-600 px-4 py-2 text-sm font-bold text-white hover:bg-emerald-700 transition-colors shadow-sm"
>
@@ -195,7 +202,7 @@ export default function LotListTable({
{/* Filter bar */}
<div className="flex gap-1 rounded-xl border border-stone-200 bg-stone-50 p-1">
{STATUS_FILTERS.map((f) => (
<button
<button type="button"
key={f.value}
onClick={() => setFilter(f.value)}
className={`rounded-lg px-3 py-2 text-xs font-semibold transition-colors flex-1 ${
@@ -212,7 +219,7 @@ export default function LotListTable({
<div className="flex gap-3">
<div className="relative flex-1">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400">{Icons.search("h-4 w-4")}</span>
<input
<input aria-label="Search Lot #, Crop, Field, Or Bin..."
type="text"
placeholder="Search lot #, crop, field, or bin..."
value={query}
@@ -220,7 +227,7 @@ export default function LotListTable({
className="w-full rounded-xl border border-stone-200 bg-stone-50 pl-9 pr-4 py-3 text-base outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<button
<button type="button"
onClick={() => { setShowBulk(!showBulk); setSelected(new Set()); }}
className={`rounded-xl border px-4 py-3 text-sm font-semibold transition-colors flex items-center gap-2 ${
showBulk ? "border-emerald-600 bg-emerald-600 text-white" : "border-stone-200 text-stone-600 hover:bg-stone-50"
@@ -240,42 +247,42 @@ export default function LotListTable({
<div className="rounded-2xl border-2 border-emerald-600 bg-emerald-600 px-5 py-3 flex items-center justify-between">
<span className="text-sm font-bold text-white">{selected.size} lot{selected.size !== 1 ? "s" : ""} selected</span>
<div className="flex gap-2 flex-wrap">
<button
<button type="button"
onClick={handleBulkMarkLoaded}
className="rounded-xl bg-amber-600 hover:bg-amber-500 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
{Icons.truck("h-4 w-4")}
<span>Mark Loaded</span>
</button>
<button
<button type="button"
onClick={handleBulkMarkUsed}
className="rounded-xl bg-amber-600 hover:bg-amber-500 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
{Icons.package("h-4 w-4")}
<span>Mark as Used</span>
</button>
<button
<button type="button"
onClick={() => handleBulkStickers("field")}
className="rounded-xl bg-white/20 hover:bg-white/30 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
{Icons.printer("h-4 w-4")}
<span>Field Stickers</span>
</button>
<button
<button type="button"
onClick={() => handleBulkStickers("shed")}
className="rounded-xl bg-white/20 hover:bg-white/30 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
{Icons.printer("h-4 w-4")}
<span>Shed Stickers</span>
</button>
<button
<button type="button"
onClick={handleBulkExport}
className="rounded-xl bg-white/20 hover:bg-white/30 px-4 py-2 text-sm font-bold text-white transition-colors flex items-center gap-1.5"
>
{Icons.file("h-4 w-4")}
<span>{selected.size === 1 ? "Report" : `${selected.size} Reports`}</span>
</button>
<button
<button type="button"
onClick={() => setSelected(new Set())}
className="rounded-xl border border-white/30 px-4 py-2 text-sm font-medium text-white/70 hover:text-white transition-colors"
>
@@ -298,7 +305,7 @@ export default function LotListTable({
<tr className="border-b border-stone-100 bg-stone-50">
{showBulk && (
<th className="px-5 py-3.5 w-10">
<input
<input aria-label="Checkbox"
type="checkbox"
checked={selected.size === filtered.length && filtered.length > 0}
onChange={toggleSelectAll}
@@ -319,7 +326,7 @@ export default function LotListTable({
<tr key={lot.lot_id} className={`border-b border-stone-50 last:border-0 hover:bg-stone-50/50 transition-colors ${selected.has(lot.lot_id) ? "bg-green-50" : ""}`}>
{showBulk && (
<td className="px-5 py-4">
<input
<input aria-label="Checkbox"
type="checkbox"
checked={selected.has(lot.lot_id)}
onChange={() => toggleSelect(lot.lot_id)}
+7 -13
View File
@@ -10,7 +10,6 @@ declare global {
}
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import GlassModal from "@/components/admin/GlassModal";
// One-color outline icons
@@ -51,11 +50,10 @@ type ScanMode = "camera" | "manual";
interface QRScanModalProps {
onClose: () => void;
onScanResult?: (lotNumber: string) => void;
onScanResult: (lotNumber: string) => void;
}
export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps) {
const router = useRouter();
const [mode, setMode] = useState<ScanMode>("camera");
const [manualInput, setManualInput] = useState("");
const [cameraError, setCameraError] = useState<string | null>(null);
@@ -152,11 +150,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
if (streamRef.current) streamRef.current.getTracks().forEach(t => t.stop());
setTimeout(() => {
onClose();
if (onScanResult) {
onScanResult(raw);
} else {
router.push(`/trace/${encodeURIComponent(raw)}`);
}
onScanResult(raw);
}, 800);
return;
}
@@ -176,7 +170,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
if (scanRef.current) cancelAnimationFrame(scanRef.current);
if (streamRef.current) streamRef.current.getTracks().forEach((t) => t.stop());
};
}, [mode, detected, onClose, router]);
}, [mode, detected, onClose, onScanResult]);
function handleManualSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -184,7 +178,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
if (!trimmed) return;
setIsLoading(true);
onClose();
router.push(`/trace/${encodeURIComponent(trimmed)}`);
onScanResult(trimmed);
}
function handleClose() {
@@ -197,7 +191,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
<GlassModal title="Scan QR Code" subtitle="Point camera at Route Trace sticker" onClose={handleClose}>
{/* Mode toggle */}
<div className="flex border-b border-[var(--admin-border)] mb-4 -mx-6 px-6">
<button
<button type="button"
onClick={() => setMode("camera")}
className={`flex-1 py-3 text-sm font-semibold transition-colors ${
mode === "camera" ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-text-primary)]" : "text-[var(--admin-text-muted)]"
@@ -209,7 +203,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
</svg>
Camera
</button>
<button
<button type="button"
onClick={() => setMode("manual")}
className={`flex-1 py-3 text-sm font-semibold transition-colors ${
mode === "manual" ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-text-primary)]" : "text-[var(--admin-text-muted)]"
@@ -274,7 +268,7 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
<form onSubmit={handleManualSubmit} className="space-y-4">
<div>
<label className="block text-sm font-semibold text-stone-700 mb-1.5">Lot Number</label>
<input
<input aria-label=". TC 20260520 001"
type="text"
value={manualInput}
onChange={(e) => setManualInput(e.target.value.toUpperCase())}
@@ -82,7 +82,7 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
<label className="block text-sm font-medium text-stone-700 mb-1.5">
Crop Type <span className="text-red-500">*</span>
</label>
<input
<input aria-label=". Sweet Corn"
type="text"
value={crop_type}
onChange={(e) => setCropType(e.target.value)}
@@ -97,7 +97,7 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
<label className="block text-sm font-medium text-stone-700 mb-1.5">
Harvest Date
</label>
<input
<input aria-label="Date"
type="date"
value={harvest_date}
onChange={(e) => setHarvestDate(e.target.value)}
@@ -109,7 +109,7 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
<label className="block text-sm font-medium text-stone-700 mb-1.5">
Field / Location
</label>
<input
<input aria-label=". North Field"
type="text"
value={field_location}
onChange={(e) => setFieldLocation(e.target.value)}
@@ -123,7 +123,7 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
<label className="block text-sm font-medium text-stone-700 mb-1.5">
Worker
</label>
<input
<input aria-label="Name"
type="text"
value={worker_name}
onChange={(e) => setWorkerName(e.target.value)}
@@ -135,7 +135,7 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
<label className="block text-sm font-medium text-stone-700 mb-1.5">
Variety
</label>
<input
<input aria-label="Type"
type="text"
value={variety}
onChange={(e) => setVariety(e.target.value)}
@@ -149,7 +149,7 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
<label className="block text-sm font-medium text-stone-700 mb-1.5">
Quantity (lbs)
</label>
<input
<input aria-label="0"
type="number"
value={quantity_lbs}
onChange={(e) => setQuantityLbs(e.target.value)}
@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import QRScanModal from "./QRScanModal";
import QuickNewLotModal from "./QuickNewLotModal";
import FsmaReportModal from "./FsmaReportModal";
@@ -464,7 +465,7 @@ function FieldYieldTable({ fieldYield }: { fieldYield: FieldYieldSummary[] }) {
</thead>
<tbody>
{fieldYield.map((row, idx) => (
<tr key={idx} className="border-b border-[var(--admin-border-light)] last:border-0 hover:bg-[var(--admin-bg-subtle)]/40 transition-all duration-150">
<tr key={`${row.field_location}-${row.field_block}-${idx}`} className="border-b border-[var(--admin-border-light)] last:border-0 hover:bg-[var(--admin-bg-subtle)]/40 transition-all duration-150">
<td className="px-6 py-3.5">
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">{row.field_location}</p>
{row.field_block !== "N/A" && <p className="text-[10px] text-[var(--admin-text-muted)]">Block {row.field_block}</p>}
@@ -511,6 +512,7 @@ export default function RouteTraceDashboard({
brandId: string;
}) {
const [showScan, setShowScan] = useState(false);
const router = useRouter();
const [showQuickNew, setShowQuickNew] = useState(false);
const [search, setSearch] = useState("");
const [searchCrop, setSearchCrop] = useState("");
@@ -593,13 +595,13 @@ export default function RouteTraceDashboard({
{/* ── Quick actions ── */}
<div className="flex gap-2 sm:gap-2.5 flex-wrap">
<button
<button type="button"
onClick={() => setShowQuickNew(true)}
className="rounded-xl bg-[var(--admin-accent)] px-3 sm:px-4 py-2 text-xs sm:text-sm font-bold text-white hover:opacity-90 active:opacity-80 transition-colors flex items-center gap-1.5 sm:gap-2 shadow-[var(--admin-shadow-sm)]"
>
{Icons.plus("w-3.5 h-3.5 sm:w-4 sm:h-4")} <span className="hidden xs:inline">Quick New Lot</span><span className="xs:hidden">New Lot</span>
</button>
<button
<button type="button"
onClick={() => setShowScan(true)}
className="rounded-xl bg-[var(--admin-text-secondary)] px-3 sm:px-4 py-2 text-xs sm:text-sm font-bold text-white hover:opacity-90 transition-colors flex items-center gap-1.5 sm:gap-2"
>
@@ -627,7 +629,7 @@ export default function RouteTraceDashboard({
<div className="flex items-center gap-2 w-full xs:w-auto">
<div className="relative flex-1 xs:flex-initial">
<span className="absolute left-2.5 sm:left-3 top-1/2 -translate-y-1/2 text-[var(--admin-text-muted)]">{Icons.search("w-3 h-3 sm:w-3.5 sm:h-3.5")}</span>
<input
<input aria-label="Lot, Crop, Field…"
type="text"
placeholder="Lot, crop, field…"
value={search}
@@ -635,7 +637,7 @@ export default function RouteTraceDashboard({
className="w-full xs:w-32 sm:w-40 pl-7 sm:pl-8 pr-3 py-1.5 sm:py-2 rounded-lg border border-[var(--admin-border)] bg-white text-xs text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)]"
/>
</div>
<select
<select aria-label="Select"
value={searchCrop}
onChange={(e) => setSearchCrop(e.target.value)}
className="rounded-lg border border-[var(--admin-border)] bg-white text-xs text-[var(--admin-text-primary)] px-2 py-1.5 sm:py-2 focus:outline-none focus:border-[var(--admin-accent)] flex-shrink-0"
@@ -713,6 +715,9 @@ export default function RouteTraceDashboard({
{showScan && (
<QRScanModal
onClose={() => setShowScan(false)}
onScanResult={(lotNumber) =>
router.push(`/trace/${encodeURIComponent(lotNumber)}`)
}
/>
)}
</div>
@@ -110,7 +110,7 @@ export default function RouteTracePage({
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
<nav className="grid grid-cols-4 gap-1 p-1.5 rounded-xl bg-white border border-stone-200">
{TABS.map((tab) => (
<button
<button type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`relative flex flex-col sm:flex-row items-center justify-center gap-1 sm:gap-2 rounded-lg px-2 sm:px-4 py-2.5 sm:py-3 text-[10px] sm:text-sm font-semibold transition-colors ${
@@ -66,7 +66,7 @@ export default function RouteTraceSettings() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-stone-700 mb-1.5">Prefix</label>
<input
<input aria-label="TC"
type="text"
value={lotPrefix}
onChange={(e) => setLotPrefix(e.target.value.toUpperCase())}
@@ -92,7 +92,7 @@ export default function RouteTraceSettings() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-stone-700 mb-1.5">Default Harvest Days Ago</label>
<input
<input aria-label="Number"
type="number"
value={defaultHarvestDays}
onChange={(e) => setDefaultHarvestDays(e.target.value)}
@@ -118,7 +118,7 @@ export default function RouteTraceSettings() {
</div>
<div>
<label className="block text-sm font-medium text-stone-700 mb-1.5">Base URL</label>
<input
<input aria-label="Https://yourdomain.com"
type="text"
defaultValue={typeof window !== "undefined" ? window.location.origin : ""}
placeholder="https://yourdomain.com"
@@ -23,7 +23,7 @@ export default function ShareTraceButton({ lotNumber }: { lotNumber: string }) {
}
return (
<button
<button type="button"
onClick={handleShare}
className="rounded-xl border border-stone-200 bg-white px-4 py-2.5 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors flex items-center gap-2"
>
@@ -90,7 +90,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
<h3 className="text-base font-semibold text-stone-900 flex items-center gap-2">{Icons.printer("h-5 w-5")} Print Sticker</h3>
<p className="text-xs text-stone-400 mt-0.5">{lot.lot_number} · {lot.crop_type}</p>
</div>
<button onClick={onClose} className="text-stone-400 hover:text-stone-600">
<button type="button" onClick={onClose} className="text-stone-400 hover:text-stone-600">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -107,7 +107,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
const label = t === "field" ? "Field" : "Shed";
const desc = t === "field" ? "After harvest" : "At shed arrival";
return (
<button
<button type="button"
key={t}
onClick={() => setStickerType(t)}
className={`w-full rounded-xl border px-3 py-2.5 text-left transition-colors ${
@@ -130,7 +130,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
const w = s === "4x2" ? "2 in" : "3 in";
const h = s === "4x2" ? "2 in" : "3 in";
return (
<button
<button type="button"
key={s}
onClick={() => setStickerSize(s)}
className={`w-full rounded-xl border px-3 py-2.5 text-left transition-colors ${
@@ -152,7 +152,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
<p className="mb-2 text-xs font-semibold text-stone-500 uppercase tracking-wider">Copies</p>
<div className="grid grid-cols-5 gap-1.5">
{COPY_OPTIONS.map((n) => (
<button
<button type="button"
key={n}
onClick={() => setCopies(n)}
className={`rounded-lg border py-2 text-center text-sm font-bold transition-colors ${
@@ -284,13 +284,13 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
</div>
<div className="flex justify-end gap-3 border-t border-stone-100 px-6 py-4">
<button
<button type="button"
onClick={onClose}
className="rounded-xl border border-stone-200 px-4 py-2.5 text-sm font-semibold text-stone-600 hover:bg-stone-50"
>
Cancel
</button>
<button
<button type="button"
onClick={handlePrint}
disabled={loading}
className="rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-bold text-white hover:bg-emerald-700 disabled:opacity-50 flex items-center gap-2"