fix: route trace new lot modal instead of separate page
This commit is contained in:
@@ -1,38 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
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 = {
|
export default function NewLotPage() {
|
||||||
title: "Create New Lot | Route Trace",
|
// New lot creation is now handled via modal in the Lots tab
|
||||||
description: "Create a new harvest lot for traceability tracking. Record crop type, field location, worker, quantity, and harvest date.",
|
redirect("/admin/route-trace/lots");
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -89,9 +89,11 @@ function getAgeStatus(harvestDate: string): { label: string; className: string;
|
|||||||
export default function LotListTable({
|
export default function LotListTable({
|
||||||
initialLots,
|
initialLots,
|
||||||
brandId,
|
brandId,
|
||||||
|
onCreateNew,
|
||||||
}: {
|
}: {
|
||||||
initialLots: HaulingLot[];
|
initialLots: HaulingLot[];
|
||||||
brandId: string;
|
brandId: string;
|
||||||
|
onCreateNew?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [filter, setFilter] = useState("");
|
const [filter, setFilter] = useState("");
|
||||||
const [query, setQuery] = 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>
|
<p className="text-sm text-stone-500">{filtered.length} lot{filtered.length !== 1 ? "s" : ""}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<button
|
||||||
href="/admin/route-trace/lots/new"
|
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"
|
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")}
|
{Icons.plus("h-4 w-4")}
|
||||||
<span>New Lot</span>
|
<span>New Lot</span>
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|||||||
import RouteTraceDashboard from "./RouteTraceDashboard";
|
import RouteTraceDashboard from "./RouteTraceDashboard";
|
||||||
import AdminLookupPage from "./AdminLookupPage";
|
import AdminLookupPage from "./AdminLookupPage";
|
||||||
import LotListTable from "./LotListTable";
|
import LotListTable from "./LotListTable";
|
||||||
|
import LotCreateModal from "./LotCreateModal";
|
||||||
import RouteTraceSettings from "./RouteTraceSettings";
|
import RouteTraceSettings from "./RouteTraceSettings";
|
||||||
import {
|
import {
|
||||||
RouteTraceStats,
|
RouteTraceStats,
|
||||||
@@ -94,9 +95,17 @@ export default function RouteTracePage({
|
|||||||
lots,
|
lots,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("dashboard");
|
const [activeTab, setActiveTab] = useState<Tab>("dashboard");
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||||
|
{/* Modal */}
|
||||||
|
<LotCreateModal
|
||||||
|
isOpen={showCreateModal}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
brandId={brandId}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Tab navigation */}
|
{/* Tab navigation */}
|
||||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
<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">
|
<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" && (
|
{activeTab === "lots" && (
|
||||||
<div>
|
<div>
|
||||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
<LotListTable
|
||||||
<LotListTable initialLots={lots} brandId={brandId} />
|
initialLots={lots}
|
||||||
</div>
|
brandId={brandId}
|
||||||
|
onCreateNew={() => setShowCreateModal(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user