Consolidate admin pages into tabbed layouts with warm stone theme

- Add SettingsClient with tabs: General, Users, Integrations
- Add RouteTracePage with tabs: Dashboard, Lots, Lookup, Settings
- Fix dark styling in AdminMeClient.tsx (red/green alerts)
- Fix dark styling in AIClient.tsx (emerald/amber/green/red/blue)
- Fix ImportCenterClient.tsx (already correct, no changes needed)
- Fix TypeScript errors in LotListTable, RouteTraceDashboard
- Convert lot.id to lot.lot_id for HaulingLot compatibility
- Update route-trace components to warm stone theme
This commit is contained in:
2026-06-01 20:45:58 +00:00
parent 3274470737
commit 809e0061ca
20 changed files with 834 additions and 560 deletions
@@ -49,12 +49,12 @@ export default function AdminLookupPage({ brandId }: { brandId: string }) {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="e.g. TC-20260519-001 or Sweet Corn"
className="flex-1 rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-base outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="flex-1 rounded-xl border border-stone-200 bg-stone-50 px-4 py-3 text-base outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
<button
type="submit"
disabled={loading || !query.trim()}
className="rounded-xl bg-stone-900 px-5 py-3 text-sm font-bold text-white hover:bg-stone-800 disabled:opacity-50"
className="rounded-xl bg-emerald-600 px-5 py-3 text-sm font-bold text-white hover:bg-emerald-700 disabled:opacity-50"
>
{loading ? "Searching..." : "🔍 Search"}
</button>
@@ -78,7 +78,7 @@ export default function AdminLookupPage({ brandId }: { brandId: string }) {
const input = prompt("Enter lot number from QR scan:");
if (input) handleScanResult(input.trim());
}}
className="rounded-lg bg-stone-900 px-4 py-2 text-sm font-medium text-white hover:bg-stone-800"
className="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-700"
>
Enter Manually
</button>
@@ -51,7 +51,7 @@ export default function FsmaReportModal({ brandId }: Props) {
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
/>
</div>
<div>
@@ -60,7 +60,7 @@ export default function FsmaReportModal({ brandId }: Props) {
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
/>
</div>
</div>
@@ -78,7 +78,7 @@ export default function FsmaReportModal({ brandId }: Props) {
<button
onClick={handleDownload}
disabled={loading}
className="rounded-xl bg-stone-900 px-5 py-2 text-sm font-bold text-white hover:bg-stone-800 disabled:opacity-50 flex items-center gap-2"
className="rounded-xl bg-emerald-600 px-5 py-2 text-sm font-bold text-white hover:bg-emerald-700 disabled:opacity-50 flex items-center gap-2"
>
{loading ? "Preparing..." : "📥 Download CSV"}
</button>
+14 -14
View File
@@ -82,7 +82,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
onChange={(e) => setCropType(e.target.value)}
placeholder="e.g. Sweet Corn"
required
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-lg font-medium outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-lg font-medium outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<div>
@@ -92,7 +92,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={variety}
onChange={(e) => setVariety(e.target.value)}
placeholder="e.g. Golden Bantam"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-lg font-medium outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-lg font-medium outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
</div>
@@ -104,7 +104,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={harvest_date}
onChange={(e) => setHarvestDate(e.target.value)}
required
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-lg font-medium outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-4 text-lg font-medium outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
@@ -120,7 +120,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
onChange={(e) => setFieldLocation(e.target.value)}
placeholder="e.g. North Field"
required
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-3.5 text-base outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-3.5 text-base outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<div className="grid grid-cols-2 gap-3">
@@ -131,7 +131,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={field_block}
onChange={(e) => setFieldBlock(e.target.value)}
placeholder="e.g. Block A"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<div>
@@ -141,7 +141,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={worker_name}
onChange={(e) => setWorkerName(e.target.value)}
placeholder="e.g. Maria S."
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
</div>
@@ -161,12 +161,12 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
placeholder="e.g. 4800"
min="0"
step="0.01"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3.5 text-base outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3.5 text-base outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<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-stone-900 focus-within:bg-white transition-colors">
<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
type="number"
value={yield_estimate_lbs}
@@ -197,7 +197,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
placeholder="Unit name…"
value={customYieldUnit}
onChange={(e) => setCustomYieldUnit(e.target.value)}
className="mt-1.5 w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-2 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="mt-1.5 w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-2 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
)}
</div>
@@ -215,7 +215,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={bin_id}
onChange={(e) => setBinId(e.target.value)}
placeholder="e.g. BIN-001"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<div>
@@ -225,7 +225,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={container_id}
onChange={(e) => setContainerId(e.target.value)}
placeholder="e.g. CONT-A42"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
</div>
@@ -238,7 +238,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
onChange={(e) => setPallets(e.target.value)}
placeholder="e.g. 4"
min="0"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
<div>
@@ -248,7 +248,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
value={packer_name}
onChange={(e) => setPackerName(e.target.value)}
placeholder="e.g. Jose R."
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors"
/>
</div>
</div>
@@ -270,7 +270,7 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
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 border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 focus:bg-white transition-colors resize-none"
className="mt-3 w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 focus:bg-white transition-colors resize-none"
/>
)}
</div>
+11 -11
View File
@@ -145,7 +145,7 @@ export default function LotDetailPanel({
{nextStatuses.length > 0 && (
<button
onClick={() => { setNewStatus(nextStatuses[0]); setShowStatusModal(true); }}
className="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-blue-700 transition-colors"
className="rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700 transition-colors"
>
Update Status
</button>
@@ -396,7 +396,7 @@ export default function LotDetailPanel({
value={newStatus}
onChange={(e) => setNewStatus(e.target.value)}
required
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
>
<option value="">Select status...</option>
{STATUS_FLOW.filter((s) => s !== lot.status).map((s) => {
@@ -416,7 +416,7 @@ export default function LotDetailPanel({
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Warehouse A"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
/>
</div>
<div>
@@ -426,7 +426,7 @@ export default function LotDetailPanel({
onChange={(e) => setNotes(e.target.value)}
placeholder="Any notes..."
rows={2}
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 resize-none"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 resize-none"
/>
</div>
</div>
@@ -434,7 +434,7 @@ export default function LotDetailPanel({
<button type="button" onClick={() => setShowStatusModal(false)} className="rounded-xl border border-stone-200 px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50">
Cancel
</button>
<button type="submit" disabled={isPending || !newStatus} className="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-50">
<button type="submit" disabled={isPending || !newStatus} className="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50">
{isPending ? "Saving..." : "Save"}
</button>
</div>
@@ -461,7 +461,7 @@ export default function LotDetailPanel({
onChange={(e) => setBinId(e.target.value)}
placeholder="e.g. BIN-042"
required
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
/>
</div>
<div>
@@ -471,7 +471,7 @@ export default function LotDetailPanel({
onChange={(e) => setNotes(e.target.value)}
placeholder="Weight, count, or other details..."
rows={2}
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 resize-none"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 resize-none"
/>
</div>
</div>
@@ -508,7 +508,7 @@ export default function LotDetailPanel({
onChange={(e) => setUsedOrderId(e.target.value.toUpperCase())}
placeholder="e.g. 4f3e2a1b-..."
required
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm font-mono outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm font-mono outline-none focus:border-emerald-600"
/>
</div>
<div>
@@ -519,7 +519,7 @@ export default function LotDetailPanel({
onChange={(e) => setUsedQty(e.target.value)}
placeholder="Leave blank to auto-detect from order items"
min="0"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
/>
</div>
<div>
@@ -529,7 +529,7 @@ export default function LotDetailPanel({
onChange={(e) => setUsedNotes(e.target.value)}
placeholder="Any notes..."
rows={2}
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-stone-900 resize-none"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600 resize-none"
/>
</div>
</div>
@@ -537,7 +537,7 @@ export default function LotDetailPanel({
<button type="button" onClick={() => setShowUsedModal(false)} className="rounded-xl border border-stone-200 px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50">
Cancel
</button>
<button type="submit" disabled={isPending || !usedOrderId.trim()} className="rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-800 disabled:opacity-50">
<button type="submit" disabled={isPending || !usedOrderId.trim()} className="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50">
{isPending ? "Saving..." : "Mark as Used"}
</button>
</div>
+12 -13
View File
@@ -3,7 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import StatusBadge from "./StatusBadge";
import { HarvestLot, updateHarvestLotStatus } from "@/actions/route-trace/lots";
import { HaulingLot, updateHarvestLotStatus } from "@/actions/route-trace/lots";
const STATUS_FILTERS = [
{ value: "", label: "All" },
@@ -29,7 +29,7 @@ export default function LotListTable({
initialLots,
brandId,
}: {
initialLots: HarvestLot[];
initialLots: HaulingLot[];
brandId: string;
}) {
const [filter, setFilter] = useState("");
@@ -65,7 +65,7 @@ export default function LotListTable({
if (selected.size === filtered.length) {
setSelected(new Set());
} else {
setSelected(new Set(filtered.map((l) => l.id)));
setSelected(new Set(filtered.map((l) => l.lot_id)));
}
}
@@ -115,7 +115,7 @@ export default function LotListTable({
onClick={() => setFilter(f.value)}
className={`rounded-lg px-3 py-2 text-xs font-semibold transition-colors ${
filter === f.value
? "bg-stone-900 text-white"
? "bg-emerald-600 text-white"
: "text-stone-600 hover:bg-stone-50"
}`}
>
@@ -129,12 +129,12 @@ export default function LotListTable({
placeholder="Search lot #, crop, field, or bin..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-56 rounded-xl border border-stone-200 bg-white px-4 py-2.5 text-sm outline-none focus:border-stone-900"
className="w-56 rounded-xl border border-stone-200 bg-white px-4 py-2.5 text-sm outline-none focus:border-emerald-600"
/>
<button
onClick={() => { setShowBulk(!showBulk); setSelected(new Set()); }}
className={`rounded-xl border px-4 py-2.5 text-sm font-semibold transition-colors ${
showBulk ? "border-stone-900 bg-stone-900 text-white" : "border-stone-200 text-stone-600 hover:bg-stone-50"
showBulk ? "border-emerald-600 bg-emerald-600 text-white" : "border-stone-200 text-stone-600 hover:bg-stone-50"
}`}
>
Bulk {showBulk ? "On" : "Off"}
@@ -144,7 +144,7 @@ export default function LotListTable({
{/* Bulk action bar */}
{selected.size > 0 && (
<div className="rounded-2xl border-2 border-stone-900 bg-stone-900 px-5 py-3 flex items-center justify-between">
<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
@@ -217,25 +217,24 @@ export default function LotListTable({
</thead>
<tbody>
{filtered.map((lot) => (
<tr key={lot.id} className={`border-b border-stone-50 last:border-0 hover:bg-stone-50/50 transition-colors ${selected.has(lot.id) ? "bg-green-50" : ""}`}>
<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
type="checkbox"
checked={selected.has(lot.id)}
onChange={() => toggleSelect(lot.id)}
checked={selected.has(lot.lot_id)}
onChange={() => toggleSelect(lot.lot_id)}
className="h-4 w-4 rounded border-stone-300"
/>
</td>
)}
<td className="px-5 py-4">
<Link href={`/admin/route-trace/lots/${lot.id}`} className="font-mono text-sm font-black text-stone-900 hover:text-blue-600">
<Link href={`/admin/route-trace/lots/${lot.lot_id}`} className="font-mono text-sm font-black text-stone-900 hover:text-blue-600">
{lot.lot_number}
</Link>
</td>
<td className="px-5 py-4">
<div className="text-sm font-semibold text-stone-700">{lot.crop_type}</div>
{lot.variety && <div className="text-xs text-stone-400">{lot.variety}</div>}
</td>
<td className="px-5 py-4">
{(() => {
@@ -275,7 +274,7 @@ export default function LotListTable({
<StatusBadge status={lot.status} />
</td>
<td className="px-5 py-4 text-right">
<Link href={`/admin/route-trace/lots/${lot.id}`} className="text-sm font-semibold text-blue-600 hover:text-blue-800">
<Link href={`/admin/route-trace/lots/${lot.lot_id}`} className="text-sm font-semibold text-blue-600 hover:text-blue-800">
View
</Link>
</td>
+4 -4
View File
@@ -168,7 +168,7 @@ export default function QRScanModal({ onClose }: { onClose: () => void }) {
<button
onClick={() => setMode("camera")}
className={`flex-1 py-3 text-sm font-semibold transition-colors ${
mode === "camera" ? "text-stone-900 border-b-2 border-stone-900" : "text-stone-400"
mode === "camera" ? "text-emerald-700 border-b-2 border-emerald-600" : "text-stone-400"
}`}
>
📷 Camera
@@ -176,7 +176,7 @@ export default function QRScanModal({ onClose }: { onClose: () => void }) {
<button
onClick={() => setMode("manual")}
className={`flex-1 py-3 text-sm font-semibold transition-colors ${
mode === "manual" ? "text-stone-900 border-b-2 border-stone-900" : "text-stone-400"
mode === "manual" ? "text-emerald-700 border-b-2 border-emerald-600" : "text-stone-400"
}`}
>
Manual
@@ -241,14 +241,14 @@ export default function QRScanModal({ onClose }: { onClose: () => void }) {
onChange={(e) => setManualInput(e.target.value.toUpperCase())}
placeholder="e.g. TC-20260520-001"
autoFocus
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-3.5 text-base font-mono text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-stone-900"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-4 py-3.5 text-base font-mono text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-600"
/>
<p className="text-[10px] text-stone-400 mt-1.5">Enter the lot number from any Route Trace sticker</p>
</div>
<button
type="submit"
disabled={!manualInput.trim() || isLoading}
className="w-full rounded-xl bg-stone-900 py-3.5 text-base font-bold text-white hover:bg-stone-800 disabled:opacity-50 transition-colors"
className="w-full rounded-xl bg-emerald-600 py-3.5 text-base font-bold text-white hover:bg-emerald-700 disabled:opacity-50 transition-colors"
>
{isLoading ? "Loading..." : "🔍 Trace Lot"}
</button>
@@ -374,7 +374,7 @@ export default function RouteTraceDashboard({
brandId,
}: {
stats: RouteTraceStats;
recentLots: Array<{ lot_number: string; crop_type: string; status: string; harvest_date: string; id: string }>;
recentLots: HaulingLot[];
haulingLots: HaulingLot[];
fieldYield: FieldYieldSummary[];
inventoryByCrop: InventoryByCrop[];
@@ -474,7 +474,7 @@ export default function RouteTraceDashboard({
</button>
<button
onClick={() => setShowScan(true)}
className="rounded-xl bg-stone-900 px-4 py-2.5 text-sm font-bold text-white hover:bg-stone-800 transition-colors flex items-center gap-2"
className="rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-bold text-white hover:bg-emerald-700 transition-colors flex items-center gap-2"
>
📷 Scan QR
</button>
@@ -617,9 +617,9 @@ export default function RouteTraceDashboard({
</thead>
<tbody>
{recentLots.map((lot) => (
<tr key={lot.id} className="border-b border-stone-50 last:border-0 hover:bg-stone-50/40 transition-colors">
<tr key={lot.lot_id} className="border-b border-stone-50 last:border-0 hover:bg-stone-50/40 transition-colors">
<td className="px-5 py-3.5">
<Link href={`/admin/route-trace/lots/${lot.id}`} className="font-mono text-sm font-bold text-stone-900 hover:text-blue-600">
<Link href={`/admin/route-trace/lots/${lot.lot_id}`} className="font-mono text-sm font-bold text-stone-900 hover:text-blue-600">
{lot.lot_number}
</Link>
</td>
+1 -1
View File
@@ -23,7 +23,7 @@ export default function RouteTraceNav({
href={tab.href}
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors whitespace-nowrap rounded-t-lg ${
isActive
? "border-stone-900 text-stone-900 bg-white"
? "border-emerald-600 text-emerald-700 bg-white"
: "border-transparent text-stone-500 hover:text-stone-700 hover:border-stone-300"
}`}
>
@@ -0,0 +1,141 @@
"use client";
import { useState } from "react";
import RouteTraceDashboard from "./RouteTraceDashboard";
import AdminLookupPage from "./AdminLookupPage";
import LotListTable from "./LotListTable";
import Link from "next/link";
import {
RouteTraceStats,
HaulingLot,
FieldYieldSummary,
InventoryByCrop,
RecentLotEvent,
} from "@/actions/route-trace/lots";
type Tab = "dashboard" | "lots" | "lookup" | "settings";
const TABS: { id: Tab; label: string }[] = [
{ id: "dashboard", label: "Dashboard" },
{ id: "lots", label: "Lots" },
{ id: "lookup", label: "Lookup" },
{ id: "settings", label: "Settings" },
];
type Props = {
stats: RouteTraceStats;
recentLots: HaulingLot[];
haulingLots: HaulingLot[];
fieldYield: FieldYieldSummary[];
inventoryByCrop: InventoryByCrop[];
recentActivity: RecentLotEvent[];
brandId: string;
lots: HaulingLot[];
};
export default function RouteTracePage({
stats,
recentLots,
haulingLots,
fieldYield,
inventoryByCrop,
recentActivity,
brandId,
lots,
}: Props) {
const [activeTab, setActiveTab] = useState<Tab>("dashboard");
return (
<div className="min-h-screen bg-stone-50 px-6 py-8">
<div className="mx-auto max-w-6xl">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<div>
<h1 className="text-2xl font-semibold text-stone-950 tracking-tight">Route Trace</h1>
<p className="mt-1 text-sm text-stone-500">Track lots from field to delivery</p>
</div>
{activeTab === "lots" && (
<Link
href="/admin/route-trace/lots/new"
className="flex items-center gap-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 px-4 py-2.5 text-sm font-semibold text-white transition-colors shadow-sm shadow-emerald-200"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
New Lot
</Link>
)}
</div>
{/* Tab nav */}
<div className="border-b border-stone-200 mt-4">
<nav className="flex gap-1 -mb-px">
{TABS.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-5 py-3 text-sm font-medium border-b-2 transition-colors rounded-t-lg ${
activeTab === tab.id
? "border-emerald-500 text-emerald-700 bg-white"
: "border-transparent text-stone-500 hover:text-stone-700 hover:bg-stone-50"
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
</div>
{/* Tab content */}
{activeTab === "dashboard" && (
<RouteTraceDashboard
stats={stats}
recentLots={recentLots}
haulingLots={haulingLots}
fieldYield={fieldYield}
inventoryByCrop={inventoryByCrop}
recentActivity={recentActivity}
brandId={brandId}
/>
)}
{activeTab === "lots" && (
<div className="rounded-2xl bg-white border border-stone-200 shadow-sm overflow-hidden">
<div className="px-5 py-4 border-b border-stone-100 bg-stone-50">
<p className="text-sm font-medium text-stone-700">{lots.length} lot{lots.length !== 1 ? "s" : ""} total</p>
</div>
<LotListTable initialLots={lots} brandId={brandId} />
</div>
)}
{activeTab === "lookup" && (
<div className="rounded-2xl bg-white border border-stone-200 shadow-sm p-6">
<AdminLookupPage brandId={brandId} />
</div>
)}
{activeTab === "settings" && (
<div className="rounded-2xl bg-white border border-stone-200 shadow-sm p-6">
<div className="text-center py-12">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-stone-100 mx-auto mb-4">
<svg className="h-8 w-8 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2c3.95 0 6.2 3.25 6.2 6.2 0 1.06-.34 2.06-.95 2.83l2.86 2.86c.77.77 1.2 1.82 1.2 2.93 0 2.11-1.71 3.82-3.82 3.82-1.11 0-2.17-.45-2.93-1.21l-2.86-2.86A3.8 3.8 0 0011.91 18c-.76-.61-1.25-1.5-1.25-2.5 0-1.22.55-2.32 1.41-3.07l.54-.54c.43-.43 1.12-.43 1.55 0 .43.43.43 1.12 0 1.55l-.54.54c.76.86 1.22 1.95 1.22 3.18 0 2.1-1.71 3.82-3.82 3.82-1.23 0-2.32-.56-3.18-1.22l-.54-.54c-.43-.43-1.12-.43-1.55 0-.43.43-.43 1.12 0 1.55l.54.54c.86.76 1.95 1.22 3.18 1.22 2.1 0 3.82-1.71 3.82-3.82 0-1.23-.45-2.32-1.22-3.18l-.54-.54c-.43-.43-.43-1.12 0-1.55.43-.43 1.12-.43 1.55 0l.54.54c.61.76.95 1.76.95 2.83 0 1-.49 1.89-1.25 2.5-.65.42-1.44.68-2.28.68-1.93 0-3.5-1.57-3.5-3.5 0-.78.26-1.52.72-2.09l2.86-2.86c.39-.39.6-.91.6-1.46 0-.55-.21-1.07-.6-1.46-.39-.39-.91-.6-1.46-.6-.55 0-1.07.21-1.46.6l-2.86 2.86c-.58.46-1.31.72-2.09.72-.78 0-1.52-.26-2.09-.72l-.54-.54c-.39-.39-.6-.91-.6-1.46s.21-1.07.6-1.46c.39-.39.91-.6 1.46-.6.55 0 1.07.21 1.46.6l.54.54c.46.58.72 1.31.72 2.09 0 .78-.26 1.52-.72 2.09l-2.86 2.86c-.39.39-.6.91-.6 1.46 0 .55.21 1.07.6 1.46.39.39.91.6 1.46.6.55 0 1.07-.21 1.46-.6l2.86-2.86c.58-.46 1.31-.72 2.09-.72.78 0 1.52.26 2.09.72l.54.54c.39.39.6.91.6 1.46 0 .55-.21 1.07-.6 1.46-.39.39-.91.6-1.46.6-.55 0-1.07-.21-1.46-.6l-.54-.54c-.76-.86-1.22-1.95-1.22-3.18 0-1.23.56-2.32 1.22-3.18l.54-.54c.43-.43 1.12-.43 1.55 0 .43.43.43 1.12 0 1.55l-.54.54c-.76.86-1.22 1.95-1.22 3.18 0 2.1 1.71 3.82 3.82 3.82 1.23 0 2.32-.56 3.18-1.22l.54-.54c.43-.43 1.12-.43 1.55 0 .43.43.43 1.12 0 1.55l-.54.54c-.86.76-1.95 1.22-3.18 1.22-1.1 0-2.16-.26-3.07-.75V4.01c0-.55.21-1.07.6-1.46.39-.39.91-.6 1.46-.6h.01" />
</svg>
</div>
<h2 className="text-lg font-semibold text-stone-700 mb-2">Route Trace Settings</h2>
<p className="text-sm text-stone-500 mb-4">Configure your traceability workflow and defaults</p>
<Link
href="/admin/settings/apps"
className="inline-flex items-center gap-2 rounded-xl border border-stone-200 bg-white px-4 py-2.5 text-sm font-medium text-stone-600 hover:bg-stone-50 transition-colors"
>
Manage Add-ons
</Link>
</div>
</div>
)}
</div>
</div>
);
}
+1 -1
View File
@@ -11,7 +11,7 @@ const STATUS_CONFIG = {
type Status = keyof typeof STATUS_CONFIG;
export default function StatusBadge({ status }: { status: string }) {
const config = STATUS_CONFIG[status as Status] ?? { label: status, bg: "bg-slate-100", text: "text-slate-700" };
const config = STATUS_CONFIG[status as Status] ?? { label: status, bg: "bg-stone-100", text: "text-stone-700" };
return (
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${config.bg} ${config.text}`}>
<span className="h-1.5 w-1.5 rounded-full currentColor" />
@@ -83,7 +83,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
onClick={() => setStickerType(t)}
className={`w-full rounded-xl border px-3 py-2.5 text-left transition-colors ${
stickerType === t
? "border-stone-900 bg-stone-900 text-white"
? "border-emerald-600 bg-emerald-600 text-white"
: "border-stone-200 hover:bg-stone-50"
}`}
>
@@ -106,7 +106,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
onClick={() => setStickerSize(s)}
className={`w-full rounded-xl border px-3 py-2.5 text-left transition-colors ${
stickerSize === s
? "border-stone-900 bg-stone-900 text-white"
? "border-emerald-600 bg-emerald-600 text-white"
: "border-stone-200 hover:bg-stone-50"
}`}
>
@@ -128,7 +128,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
onClick={() => setCopies(n)}
className={`rounded-lg border py-2 text-center text-sm font-bold transition-colors ${
copies === n
? "border-stone-900 bg-stone-900 text-white"
? "border-emerald-600 bg-emerald-600 text-white"
: "border-stone-200 text-stone-600 hover:bg-stone-50"
}`}
>
@@ -264,7 +264,7 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
<button
onClick={handlePrint}
disabled={loading}
className="rounded-xl bg-stone-900 px-5 py-2.5 text-sm font-bold text-white hover:bg-stone-800 disabled:opacity-50 flex items-center gap-2"
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"
>
{loading ? "Generating PDF..." : `🖨 Print ${copies === 1 ? "" : `${copies}× `}${stickerType === "field" ? "Field" : "Shed"} Sticker${copies > 1 ? "s" : ""}`}
</button>