fix: route trace new lot modal instead of separate page

This commit is contained in:
2026-06-02 05:04:49 +00:00
parent d7fe1ea3fd
commit fb25c5ee22
4 changed files with 477 additions and 41 deletions
+3 -35
View File
@@ -1,38 +1,6 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { getAdminUser } from "@/lib/admin-permissions";
import { isFeatureEnabled } from "@/lib/feature-flags";
import LotCreateForm from "@/components/route-trace/LotCreateForm";
import RouteTraceNav from "@/components/route-trace/RouteTraceNav";
export const metadata = {
title: "Create New Lot | Route Trace",
description: "Create a new harvest lot for traceability tracking. Record crop type, field location, worker, quantity, and harvest date.",
keywords: ["create lot", "new lot", "harvest entry", "lot creation", "traceability", "harvest tracking"],
};
export default async function NewLotPage() {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
const isDevMode = !!devSession;
const effectiveBrandId = adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
const enabled = isDevMode ? true : await isFeatureEnabled(effectiveBrandId, "route_trace");
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
return (
<div className="min-h-screen px-6 py-10" style={{ backgroundColor: "var(--admin-bg)" }}>
<div className="mx-auto max-w-2xl">
<div className="mb-6">
<a href="/admin/route-trace/lots" className="text-sm text-stone-500 hover:text-stone-700"> Back to Lots</a>
</div>
<RouteTraceNav activeTab="lots" />
<LotCreateForm brandId={effectiveBrandId} />
</div>
</div>
);
export default function NewLotPage() {
// New lot creation is now handled via modal in the Lots tab
redirect("/admin/route-trace/lots");
}
@@ -0,0 +1,455 @@
"use client";
import { useState, useTransition, useEffect } from "react";
import { useRouter } from "next/navigation";
import { createHarvestLot } from "@/actions/route-trace/lots";
type Props = {
isOpen: boolean;
onClose: () => void;
brandId: string;
};
export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [notesOpen, setNotesOpen] = useState(false);
const [crop_type, setCropType] = useState("");
const [variety, setVariety] = useState("");
const [harvest_date, setHarvestDate] = useState(new Date().toISOString().split("T")[0]);
const [field_location, setFieldLocation] = useState("");
const [field_block, setFieldBlock] = useState("");
const [worker_name, setWorkerName] = useState("");
const [packer_name, setPackerName] = useState("");
const [quantity_lbs, setQuantityLbs] = useState("");
const [yield_estimate_lbs, setYieldEstimateLbs] = useState("");
const [yield_unit, setYieldUnit] = useState("lbs");
const [customYieldUnit, setCustomYieldUnit] = useState("");
const [bin_id, setBinId] = useState("");
const [container_id, setContainerId] = useState("");
const [pallets, setPallets] = useState("");
const [notes, setNotes] = useState("");
// Reset form when modal opens
useEffect(() => {
if (isOpen) {
setCropType("");
setVariety("");
setHarvestDate(new Date().toISOString().split("T")[0]);
setFieldLocation("");
setFieldBlock("");
setWorkerName("");
setPackerName("");
setQuantityLbs("");
setYieldEstimateLbs("");
setYieldUnit("lbs");
setCustomYieldUnit("");
setBinId("");
setContainerId("");
setPallets("");
setNotes("");
setError(null);
setNotesOpen(false);
}
}, [isOpen]);
if (!isOpen) return null;
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
startTransition(async () => {
const result = await createHarvestLot(brandId, {
crop_type,
variety: variety || undefined,
harvest_date,
field_location: field_location || undefined,
worker_name: worker_name || undefined,
packer_name: packer_name || undefined,
quantity_lbs: quantity_lbs ? Number(quantity_lbs) : undefined,
notes: notes || undefined,
bin_id: bin_id || undefined,
container_id: container_id || undefined,
field_block: field_block || undefined,
yield_estimate_lbs: yield_estimate_lbs ? Number(yield_estimate_lbs) : undefined,
yield_unit: yield_unit === "custom" ? customYieldUnit.trim() || undefined : yield_unit,
pallets: pallets ? Number(pallets) : undefined,
});
if (result.success && result.lot) {
onClose();
router.push(`/admin/route-trace/lots/${result.lot.id}`);
} else {
setError(result.error ?? "Failed to create lot");
}
});
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div
className="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-2xl border shadow-2xl"
style={{
backgroundColor: "white",
borderColor: "var(--admin-border)"
}}
>
<div className="sticky top-0 border-b px-6 py-5 flex items-center justify-between" style={{ backgroundColor: "white", borderColor: "var(--admin-border)" }}>
<div className="flex items-center gap-3">
<div
className="flex h-9 w-9 items-center justify-center rounded-xl"
style={{ backgroundColor: "var(--admin-accent-light)" }}
>
<svg className="h-5 w-5" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path d="M12 22V12M12 12C12 12 7 10 7 5c0-2.5 2.5-5 5-5s5 2.5 5 5c0 5-5 7-5 7z" />
</svg>
</div>
<div>
<h2 className="text-lg font-bold" style={{ color: "var(--admin-text-primary)" }}>New Harvest Lot</h2>
<p className="text-sm" style={{ color: "var(--admin-text-muted)" }}>Quick entry scan or fill in the fields below</p>
</div>
</div>
<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)" }}
>
<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>
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-5">
{error && (
<div
className="rounded-xl p-4 text-sm border"
style={{
backgroundColor: "rgba(239, 68, 68, 0.1)",
borderColor: "rgba(239, 68, 68, 0.3)",
color: "rgb(239, 68, 68)"
}}
>
{error}
</div>
)}
{/* Required fields */}
<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
type="text"
value={crop_type}
onChange={(e) => setCropType(e.target.value)}
placeholder="e.g. Sweet Corn"
required
className="w-full rounded-xl border px-4 py-4 text-lg font-medium outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: "var(--admin-text-primary)" }}>Variety</label>
<input
type="text"
value={variety}
onChange={(e) => setVariety(e.target.value)}
placeholder="e.g. Golden Bantam"
className="w-full rounded-xl border px-4 py-4 text-lg font-medium outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: "var(--admin-text-primary)" }}>Harvest Date *</label>
<input
type="date"
value={harvest_date}
onChange={(e) => setHarvestDate(e.target.value)}
required
className="w-full rounded-xl border px-4 py-4 text-lg font-medium outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
{/* Field & Location */}
<div className="border-t pt-5" style={{ borderColor: "var(--admin-border)" }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-3" style={{ color: "var(--admin-text-muted)" }}>Field & Location</p>
<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
type="text"
value={field_location}
onChange={(e) => setFieldLocation(e.target.value)}
placeholder="e.g. North Field"
required
className="w-full rounded-xl border px-4 py-3.5 text-base outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
<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
type="text"
value={field_block}
onChange={(e) => setFieldBlock(e.target.value)}
placeholder="e.g. Block A"
className="w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Worker / Harvest Lead</label>
<input
type="text"
value={worker_name}
onChange={(e) => setWorkerName(e.target.value)}
placeholder="e.g. Maria S."
className="w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
</div>
</div>
</div>
{/* Quantity + Yield */}
<div className="border-t pt-5" style={{ borderColor: "var(--admin-border)" }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-3" style={{ color: "var(--admin-text-muted)" }}>Weight & Yield</p>
<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
type="number"
value={quantity_lbs}
onChange={(e) => setQuantityLbs(e.target.value)}
placeholder="e.g. 4800"
min="0"
step="0.01"
className="w-full rounded-xl border px-3 py-3.5 text-base outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
<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
type="number"
value={yield_estimate_lbs}
onChange={(e) => setYieldEstimateLbs(e.target.value)}
placeholder="e.g. 5000"
min="0"
step="0.01"
className="flex-1 px-3 py-3.5 text-base outline-none bg-transparent"
style={{ color: "var(--admin-text-primary)" }}
/>
<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"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg)",
color: "var(--admin-text-muted)"
}}
>
<option value="lbs">lbs</option>
<option value="bushel">bu</option>
<option value="box">bx</option>
<option value="sack">sk</option>
<option value="crate">cr</option>
<option value="bin">bn</option>
<option value="pallet">plt</option>
<option value="custom">custom</option>
</select>
</div>
{yield_unit === "custom" && (
<input
type="text"
placeholder="Unit name…"
value={customYieldUnit}
onChange={(e) => setCustomYieldUnit(e.target.value)}
className="mt-1.5 w-full rounded-xl border px-3 py-2 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
)}
</div>
</div>
</div>
{/* Bin + Container */}
<div className="border-t pt-5" style={{ borderColor: "var(--admin-border)" }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-3" style={{ color: "var(--admin-text-muted)" }}>Bin, Container & Pallets</p>
<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
type="text"
value={bin_id}
onChange={(e) => setBinId(e.target.value)}
placeholder="e.g. BIN-001"
className="w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Container ID</label>
<input
type="text"
value={container_id}
onChange={(e) => setContainerId(e.target.value)}
placeholder="e.g. CONT-A42"
className="w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
</div>
<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
type="number"
value={pallets}
onChange={(e) => setPallets(e.target.value)}
placeholder="e.g. 4"
min="0"
className="w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Packer Name</label>
<input
type="text"
value={packer_name}
onChange={(e) => setPackerName(e.target.value)}
placeholder="e.g. Jose R."
className="w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
</div>
</div>
</div>
{/* Notes — collapsible */}
<div className="border-t pt-5" style={{ borderColor: "var(--admin-border)" }}>
<button
type="button"
onClick={() => setNotesOpen(!notesOpen)}
className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider transition-colors"
style={{ color: "var(--admin-text-muted)" }}
>
<span>Notes</span>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d={notesOpen ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6"} />
</svg>
</button>
{notesOpen && (
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Spray records, weather conditions, or other harvest details..."
rows={3}
className="mt-3 w-full rounded-xl border px-3 py-3 text-sm outline-none transition-colors resize-none"
style={{
borderColor: "var(--admin-border)",
backgroundColor: "var(--admin-bg-subtle)",
color: "var(--admin-text-primary)"
}}
/>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-3 border-t pt-5" style={{ borderColor: "var(--admin-border)" }}>
<button
type="button"
onClick={onClose}
className="rounded-xl border px-6 py-3 text-sm font-semibold transition-colors"
style={{
borderColor: "var(--admin-border)",
color: "var(--admin-text-muted)"
}}
>
Cancel
</button>
<button
type="submit"
disabled={isPending || !crop_type || !harvest_date || !field_location}
className="rounded-xl px-8 py-3 text-base font-bold text-white transition-colors disabled:opacity-50 flex items-center gap-2"
style={{ backgroundColor: "var(--admin-accent)" }}
>
{isPending ? "Creating..." : (
<>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path d="M12 22V12M12 12C12 12 7 10 7 5c0-2.5 2.5-5 5-5s5 2.5 5 5c0 5-5 7-5 7z" />
</svg>
Create Lot
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}
+5 -3
View File
@@ -89,9 +89,11 @@ function getAgeStatus(harvestDate: string): { label: string; className: string;
export default function LotListTable({
initialLots,
brandId,
onCreateNew,
}: {
initialLots: HaulingLot[];
brandId: string;
onCreateNew?: () => void;
}) {
const [filter, setFilter] = useState("");
const [query, setQuery] = useState("");
@@ -180,13 +182,13 @@ export default function LotListTable({
<p className="text-sm text-stone-500">{filtered.length} lot{filtered.length !== 1 ? "s" : ""}</p>
</div>
</div>
<Link
href="/admin/route-trace/lots/new"
<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"
>
{Icons.plus("h-4 w-4")}
<span>New Lot</span>
</Link>
</button>
</div>
</div>
<div className="p-6 space-y-4">
+14 -3
View File
@@ -5,6 +5,7 @@ import Link from "next/link";
import RouteTraceDashboard from "./RouteTraceDashboard";
import AdminLookupPage from "./AdminLookupPage";
import LotListTable from "./LotListTable";
import LotCreateModal from "./LotCreateModal";
import RouteTraceSettings from "./RouteTraceSettings";
import {
RouteTraceStats,
@@ -94,9 +95,17 @@ export default function RouteTracePage({
lots,
}: Props) {
const [activeTab, setActiveTab] = useState<Tab>("dashboard");
const [showCreateModal, setShowCreateModal] = useState(false);
return (
<div className="min-h-screen bg-[var(--admin-bg)]">
{/* Modal */}
<LotCreateModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
brandId={brandId}
/>
{/* Tab navigation */}
<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">
@@ -140,9 +149,11 @@ export default function RouteTracePage({
{activeTab === "lots" && (
<div>
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
<LotListTable initialLots={lots} brandId={brandId} />
</div>
<LotListTable
initialLots={lots}
brandId={brandId}
onCreateNew={() => setShowCreateModal(true)}
/>
</div>
)}