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:
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user