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:
@@ -136,19 +136,19 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
const analysisLabels = ["Reading your file...", "AI is mapping columns...", "Cleaning and normalizing data...", "Finalizing preview..."];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950">
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div className="mx-auto max-w-4xl px-6 py-10">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center gap-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-zinc-800 border border-zinc-700">
|
||||
<svg className="h-5 w-5 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-stone-200 border border-stone-300">
|
||||
<svg className="h-5 w-5 text-stone-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-zinc-100">Import Center</h1>
|
||||
<p className="mt-1 text-zinc-500 text-sm">AI-powered data import for products, orders, contacts, and stops.</p>
|
||||
<h1 className="text-3xl font-bold text-stone-950">Import Center</h1>
|
||||
<p className="mt-1 text-stone-500 text-sm">AI-powered data import for products, orders, contacts, and stops.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,12 +161,12 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
return (
|
||||
<div key={s} className="flex items-center gap-2">
|
||||
<div className={`flex h-7 w-7 items-center justify-center rounded-full text-xs font-bold ${
|
||||
isActive ? "bg-violet-600 text-white" : isPast ? "bg-green-600 text-white" : "bg-zinc-800 text-zinc-500"
|
||||
isActive ? "bg-violet-600 text-white" : isPast ? "bg-green-600 text-white" : "bg-stone-200 text-stone-500"
|
||||
}`}>
|
||||
{isPast ? "✓" : i + 1}
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${isActive ? "text-zinc-100" : "text-zinc-500"}`}>{labels[i]}</span>
|
||||
{i < 3 && <div className="h-px w-8 bg-zinc-800" />}
|
||||
<span className={`text-sm font-medium ${isActive ? "text-stone-950" : "text-stone-500"}`}>{labels[i]}</span>
|
||||
{i < 3 && <div className="h-px w-8 bg-stone-200" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -174,12 +174,12 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
|
||||
{/* Brand selector */}
|
||||
{isPlatformAdmin && (
|
||||
<div className="mb-6 rounded-xl bg-zinc-900 border border-zinc-800 p-4">
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Importing to brand</label>
|
||||
<div className="mb-6 rounded-xl bg-white border border-stone-200 p-4">
|
||||
<label className="block text-sm font-medium text-stone-500 mb-1">Importing to brand</label>
|
||||
<select
|
||||
value={activeBrandId}
|
||||
onChange={(e) => handleBrandChange(e.target.value)}
|
||||
className="w-full rounded-xl border border-zinc-600 bg-zinc-800 px-4 py-2.5 text-sm text-zinc-100 outline-none focus:border-violet-500"
|
||||
className="w-full rounded-xl border border-stone-300 bg-white px-4 py-2.5 text-sm text-stone-950 outline-none focus:border-violet-500"
|
||||
>
|
||||
{brands.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
|
||||
</select>
|
||||
@@ -193,34 +193,34 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
onDragOver={(e) => { e.preventDefault(); }}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-zinc-700 bg-zinc-900 p-12 cursor-pointer hover:border-violet-500 hover:bg-zinc-800 transition-colors"
|
||||
className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-stone-300 bg-white p-12 cursor-pointer hover:border-violet-500 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
<svg className="h-10 w-10 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="h-10 w-10 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<div className="text-center">
|
||||
<p className="text-base font-medium text-zinc-300">Drag & drop your file here</p>
|
||||
<p className="mt-1 text-sm text-zinc-500">or click to browse</p>
|
||||
<p className="text-base font-medium text-stone-700">Drag & drop your file here</p>
|
||||
<p className="mt-1 text-sm text-stone-500">or click to browse</p>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-600">CSV, XLSX, XLS, TXT · max 5,000 rows · 10MB</p>
|
||||
<p className="text-xs text-stone-400">CSV, XLSX, XLS, TXT · max 5,000 rows · 10MB</p>
|
||||
<input ref={inputRef} type="file" accept=".csv,.xlsx,.xls,.txt" className="hidden" onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }} />
|
||||
</div>
|
||||
|
||||
{fileName && (
|
||||
<div className="rounded-xl bg-green-900/30 border border-green-800 p-4 flex items-center gap-3">
|
||||
<svg className="h-5 w-5 text-green-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div className="rounded-xl bg-emerald-50 border border-emerald-200 p-4 flex items-center gap-3">
|
||||
<svg className="h-5 w-5 text-emerald-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-green-300 truncate">{fileName}</p>
|
||||
<p className="text-xs text-green-500">Ready to analyze</p>
|
||||
<p className="text-sm font-medium text-emerald-700 truncate">{fileName}</p>
|
||||
<p className="text-xs text-emerald-600">Ready to analyze</p>
|
||||
</div>
|
||||
<button onClick={() => { setFileName(""); setBase64Data(""); }} className="text-sm text-green-400 font-medium hover:text-green-300">Change</button>
|
||||
<button onClick={() => { setFileName(""); setBase64Data(""); }} className="text-sm text-emerald-600 font-medium hover:text-emerald-700">Change</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysisError && (
|
||||
<div className="rounded-xl bg-red-900/30 border border-red-800 p-4 text-sm text-red-300">{analysisError}</div>
|
||||
<div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-700">{analysisError}</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
@@ -234,19 +234,19 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
Analyze with AI
|
||||
</button>
|
||||
|
||||
<div className="rounded-xl bg-violet-900/20 border border-violet-800 p-4">
|
||||
<p className="text-sm font-medium text-violet-300 mb-1">AI-assisted import</p>
|
||||
<p className="text-xs text-violet-500">The Import Center auto-detects your data type, maps columns intelligently, and normalizes messy data (phone numbers, dates, prices). Supports products, orders, contacts, and stops.</p>
|
||||
<div className="rounded-xl bg-violet-50 border border-violet-200 p-4">
|
||||
<p className="text-sm font-medium text-violet-700 mb-1">AI-assisted import</p>
|
||||
<p className="text-xs text-violet-600">The Import Center auto-detects your data type, maps columns intelligently, and normalizes messy data (phone numbers, dates, prices). Supports products, orders, contacts, and stops.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Analysis Loading ── */}
|
||||
{step === "analysis" && (
|
||||
<div className="rounded-2xl bg-zinc-900 border border-zinc-800 p-16 text-center">
|
||||
<div className="rounded-2xl bg-white border border-stone-200 p-16 text-center">
|
||||
<div className="mx-auto mb-6 h-12 w-12 rounded-full border-4 border-violet-600 border-t-transparent animate-spin" />
|
||||
<p className="text-xl font-semibold text-zinc-300 mb-2">{analysisLabels[analysisStep]}</p>
|
||||
<p className="text-sm text-zinc-500">This usually takes 5–10 seconds</p>
|
||||
<p className="text-xl font-semibold text-stone-700 mb-2">{analysisLabels[analysisStep]}</p>
|
||||
<p className="text-sm text-stone-500">This usually takes 5–10 seconds</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -254,33 +254,33 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
{step === "preview" && analysis && (
|
||||
<div className="space-y-6">
|
||||
{/* Detected type banner */}
|
||||
<div className="rounded-2xl bg-zinc-900 border border-zinc-800 p-6">
|
||||
<div className="rounded-2xl bg-white border border-stone-200 p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className={`text-xs font-bold uppercase tracking-wider px-2 py-1 rounded-full ${
|
||||
analysis.confidence > 0.8 ? "bg-green-900/60 text-green-300 border border-green-700" : analysis.confidence > 0.5 ? "bg-amber-900/60 text-amber-300 border border-amber-700" : "bg-red-900/60 text-red-300 border border-red-700"
|
||||
analysis.confidence > 0.8 ? "bg-emerald-100 text-emerald-700 border border-emerald-200" : analysis.confidence > 0.5 ? "bg-amber-100 text-amber-700 border border-amber-200" : "bg-red-100 text-red-700 border border-red-200"
|
||||
}`}>
|
||||
{ENTITY_LABELS[analysis.detectedType] ?? "Unknown"}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500">
|
||||
<span className="text-xs text-stone-500">
|
||||
{Math.round(analysis.confidence * 100)}% confidence
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400">
|
||||
<p className="text-sm text-stone-600">
|
||||
{analysis.rowCount.toLocaleString()} rows detected
|
||||
{analysis.autoFixApplied.length > 0 && (
|
||||
<span className="ml-2 text-green-400">· {analysis.autoFixApplied.join(", ")}</span>
|
||||
<span className="ml-2 text-emerald-600">· {analysis.autoFixApplied.join(", ")}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{analysis.confidence < 0.8 && (
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-zinc-500 mb-1">Is this wrong?</p>
|
||||
<p className="text-xs text-stone-500 mb-1">Is this wrong?</p>
|
||||
<div className="flex gap-1">
|
||||
{(["products", "orders", "stops", "contacts"] as ImportEntityType[]).map((t) => (
|
||||
<button key={t} onClick={() => overrideType(t)} className={`text-xs px-2 py-1 rounded-lg border transition-colors ${
|
||||
t === analysis.detectedType ? "bg-violet-600 text-white border-violet-600" : "border-zinc-600 text-zinc-400 hover:bg-zinc-800"
|
||||
t === analysis.detectedType ? "bg-violet-600 text-white border-violet-600" : "border-stone-300 text-stone-600 hover:bg-stone-100"
|
||||
}`}>{ENTITY_LABELS[t]}</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -291,9 +291,9 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
|
||||
{/* Warnings */}
|
||||
{analysis.warnings.length > 0 && (
|
||||
<div className="rounded-xl bg-amber-900/30 border border-amber-800 p-4">
|
||||
<p className="text-sm font-medium text-amber-300 mb-2">Issues detected</p>
|
||||
<ul className="text-xs text-amber-500 space-y-1">
|
||||
<div className="rounded-xl bg-amber-50 border border-amber-200 p-4">
|
||||
<p className="text-sm font-medium text-amber-700 mb-2">Issues detected</p>
|
||||
<ul className="text-xs text-amber-600 space-y-1">
|
||||
{analysis.warnings.slice(0, 5).map((w, i) => <li key={i}>• {w}</li>)}
|
||||
{analysis.warnings.length > 5 && <li>...and {analysis.warnings.length - 5} more</li>}
|
||||
</ul>
|
||||
@@ -301,18 +301,18 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
)}
|
||||
|
||||
{/* Column mapping table */}
|
||||
<div className="rounded-2xl bg-zinc-900 border border-zinc-800 overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-zinc-800 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-zinc-300">Column Mappings</h3>
|
||||
<span className="text-xs text-zinc-600">AI auto-detected — click to change</span>
|
||||
<div className="rounded-2xl bg-white border border-stone-200 overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stone-200 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-stone-700">Column Mappings</h3>
|
||||
<span className="text-xs text-stone-400">AI auto-detected — click to change</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-zinc-800/50">
|
||||
<th className="px-5 py-2.5 text-left font-medium text-zinc-400">Your Column</th>
|
||||
<th className="px-5 py-2.5 text-left font-medium text-zinc-400">Maps To</th>
|
||||
<th className="px-5 py-2.5 text-left font-medium text-zinc-400">Sample Values</th>
|
||||
<tr className="bg-stone-50">
|
||||
<th className="px-5 py-2.5 text-left font-medium text-stone-500">Your Column</th>
|
||||
<th className="px-5 py-2.5 text-left font-medium text-stone-500">Maps To</th>
|
||||
<th className="px-5 py-2.5 text-left font-medium text-stone-500">Sample Values</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -321,19 +321,19 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
const sampleIdx = analysis.headers.indexOf(header);
|
||||
const samples = analysis.rawRows.slice(0, 3).map((r) => r[sampleIdx] ?? "").filter(Boolean);
|
||||
return (
|
||||
<tr key={header} className="border-t border-zinc-800 hover:bg-zinc-800/50">
|
||||
<td className="px-5 py-2.5 font-medium text-zinc-200 whitespace-nowrap">{header}</td>
|
||||
<tr key={header} className="border-t border-stone-200 hover:bg-stone-50">
|
||||
<td className="px-5 py-2.5 font-medium text-stone-800 whitespace-nowrap">{header}</td>
|
||||
<td className="px-5 py-2.5">
|
||||
<select
|
||||
value={mappedField ?? ""}
|
||||
onChange={(e) => updateMapping(header, e.target.value)}
|
||||
className="rounded-lg border border-zinc-600 bg-zinc-800 px-2 py-1 text-xs text-zinc-100 outline-none focus:border-violet-500"
|
||||
className="rounded-lg border border-stone-300 bg-white px-2 py-1 text-xs text-stone-800 outline-none focus:border-violet-500"
|
||||
>
|
||||
<option value="">— ignore —</option>
|
||||
{ALL_FIELDS.map((f) => <option key={f} value={f}>{f || "(same)"}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-zinc-500 text-xs max-w-xs truncate">{samples.join(", ") || "—"}</td>
|
||||
<td className="px-5 py-2.5 text-stone-500 text-xs max-w-xs truncate">{samples.join(", ") || "—"}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -343,28 +343,28 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
</div>
|
||||
|
||||
{/* Data preview */}
|
||||
<div className="rounded-2xl bg-zinc-900 border border-zinc-800 overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-zinc-800">
|
||||
<h3 className="text-sm font-semibold text-zinc-300">Data Preview <span className="text-zinc-600 font-normal">(first 5 rows)</span></h3>
|
||||
<div className="rounded-2xl bg-white border border-stone-200 overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stone-200">
|
||||
<h3 className="text-sm font-semibold text-stone-700">Data Preview <span className="text-stone-400 font-normal">(first 5 rows)</span></h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="bg-zinc-800/50">
|
||||
<tr className="bg-stone-50">
|
||||
{analysis.headers.map((h) => (
|
||||
<th key={h} className="px-4 py-2 text-left font-medium text-zinc-400 whitespace-nowrap">{h}</th>
|
||||
<th key={h} className="px-4 py-2 text-left font-medium text-stone-500 whitespace-nowrap">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analysis.rawRows.slice(0, 5).map((row, ri) => (
|
||||
<tr key={ri} className="border-t border-zinc-800">
|
||||
<tr key={ri} className="border-t border-stone-200">
|
||||
{analysis.headers.map((h, ci) => {
|
||||
const field = editedMappings[h];
|
||||
const val = row[ci] ?? "";
|
||||
const isMapped = field && field !== "ignore";
|
||||
return (
|
||||
<td key={ci} className={`px-4 py-2 whitespace-nowrap ${isMapped ? "text-zinc-300" : "text-zinc-600 italic"}`}>
|
||||
<td key={ci} className={`px-4 py-2 whitespace-nowrap ${isMapped ? "text-stone-700" : "text-stone-400 italic"}`}>
|
||||
{String(val).slice(0, 60)}
|
||||
</td>
|
||||
);
|
||||
@@ -378,7 +378,7 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
<button onClick={() => { setStep("upload"); setAnalysis(null); }} className="rounded-xl border border-zinc-600 px-6 py-3 text-sm font-medium text-zinc-300 hover:bg-zinc-800">
|
||||
<button onClick={() => { setStep("upload"); setAnalysis(null); }} className="rounded-xl border border-stone-300 px-6 py-3 text-sm font-medium text-stone-700 hover:bg-stone-100">
|
||||
← Upload Different File
|
||||
</button>
|
||||
<button
|
||||
@@ -396,19 +396,19 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
{step === "import" && (
|
||||
<div className="space-y-4">
|
||||
{importResult?.success ? (
|
||||
<div className="rounded-2xl bg-zinc-900 border border-green-800 p-8 text-center">
|
||||
<div className="mx-auto mb-4 h-12 w-12 rounded-full bg-green-900/50 border border-green-700 flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div className="rounded-2xl bg-white border border-emerald-200 p-8 text-center">
|
||||
<div className="mx-auto mb-4 h-12 w-12 rounded-full bg-emerald-100 border border-emerald-200 flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-zinc-100 mb-2">Import Complete</h2>
|
||||
{importResult.created !== undefined && <p className="text-zinc-400">{importResult.created} created{importResult.updated ? `, ${importResult.updated} updated` : ""}</p>}
|
||||
<h2 className="text-xl font-bold text-stone-950 mb-2">Import Complete</h2>
|
||||
{importResult.created !== undefined && <p className="text-stone-600">{importResult.created} created{importResult.updated ? `, ${importResult.updated} updated` : ""}</p>}
|
||||
{importResult.errors.length > 0 && (
|
||||
<p className="mt-2 text-sm text-amber-400">{importResult.errors.length} rows had errors</p>
|
||||
<p className="mt-2 text-sm text-amber-600">{importResult.errors.length} rows had errors</p>
|
||||
)}
|
||||
<div className="mt-6 flex gap-3 justify-center">
|
||||
<button onClick={() => { setStep("upload"); setAnalysis(null); setFileName(""); setBase64Data(""); setImportResult(null); }} className="rounded-xl border border-zinc-600 px-6 py-2.5 text-sm font-medium text-zinc-300 hover:bg-zinc-800">
|
||||
<button onClick={() => { setStep("upload"); setAnalysis(null); setFileName(""); setBase64Data(""); setImportResult(null); }} className="rounded-xl border border-stone-300 px-6 py-2.5 text-sm font-medium text-stone-700 hover:bg-stone-100">
|
||||
Import Another
|
||||
</button>
|
||||
<a href={analysis?.detectedType === "products" ? "/admin/products" : analysis?.detectedType === "orders" ? "/admin/orders" : "/admin"} className="rounded-xl bg-violet-600 px-6 py-2.5 text-sm font-bold text-white hover:bg-violet-700">
|
||||
@@ -417,10 +417,10 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl bg-zinc-900 border border-red-800 p-8 text-center">
|
||||
<h2 className="text-xl font-bold text-zinc-100 mb-2">Import Failed</h2>
|
||||
<p className="text-zinc-400">{importError ?? "An error occurred during import."}</p>
|
||||
<button onClick={() => setStep("preview")} className="mt-4 rounded-xl border border-zinc-600 px-6 py-2.5 text-sm font-medium text-zinc-300 hover:bg-zinc-800">
|
||||
<div className="rounded-2xl bg-white border border-red-200 p-8 text-center">
|
||||
<h2 className="text-xl font-bold text-stone-950 mb-2">Import Failed</h2>
|
||||
<p className="text-stone-600">{importError ?? "An error occurred during import."}</p>
|
||||
<button onClick={() => setStep("preview")} className="mt-4 rounded-xl border border-stone-300 px-6 py-2.5 text-sm font-medium text-stone-700 hover:bg-stone-100">
|
||||
← Back to Preview
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -71,44 +71,44 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-zinc-950 px-6 py-12">
|
||||
<main className="min-h-screen bg-stone-50 px-6 py-12">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<Link href="/admin" className="text-sm text-zinc-500 hover:text-zinc-300">
|
||||
<Link href="/admin" className="text-sm text-stone-500 hover:text-stone-700">
|
||||
← Back to dashboard
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-6 text-3xl font-bold text-zinc-100">My Profile</h1>
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
<h1 className="mt-6 text-3xl font-bold text-stone-950">My Profile</h1>
|
||||
<p className="mt-1 text-sm text-stone-500">
|
||||
Manage your profile information and preferences.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 rounded-xl bg-red-900/30 p-4 text-sm text-red-400">{error}</div>
|
||||
<div className="mt-4 rounded-xl bg-red-50 p-4 text-sm text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Profile card */}
|
||||
<div className="mt-6 rounded-2xl bg-zinc-900 p-6 shadow-black/20 ring-1 ring-zinc-700">
|
||||
<div className="mt-6 rounded-2xl bg-white p-6 shadow-black/20 ring-1 ring-stone-200">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-zinc-100">
|
||||
<h2 className="text-lg font-semibold text-stone-950">
|
||||
{currentUser.display_name || currentUser.email}
|
||||
</h2>
|
||||
{currentUser.display_name && (
|
||||
<p className="text-sm text-zinc-500">{currentUser.email}</p>
|
||||
<p className="text-sm text-stone-500">{currentUser.email}</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="rounded-full bg-zinc-950 px-2.5 py-0.5 text-xs font-semibold capitalize text-zinc-400">
|
||||
<span className="rounded-full bg-stone-50 px-2.5 py-0.5 text-xs font-semibold capitalize text-stone-500">
|
||||
{currentUser.role.replace("_", " ")}
|
||||
</span>
|
||||
{currentUser.brand_name && (
|
||||
<span className="text-xs text-zinc-500">{currentUser.brand_name}</span>
|
||||
<span className="text-xs text-stone-500">{currentUser.brand_name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!editing && (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="rounded-lg border border-zinc-800 px-4 py-2 text-sm font-medium text-zinc-400 hover:bg-zinc-800"
|
||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-500 hover:bg-stone-100"
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
@@ -120,12 +120,12 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-slate-400">Phone</p>
|
||||
<p className="mt-1 text-sm text-zinc-300">{currentUser.phone_number ?? "—"}</p>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-stone-500">Phone</p>
|
||||
<p className="mt-1 text-sm text-stone-700">{currentUser.phone_number ?? "—"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-slate-400">Email</p>
|
||||
<p className="mt-1 text-sm text-zinc-300">{currentUser.email}</p>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-stone-500">Email</p>
|
||||
<p className="mt-1 text-sm text-stone-700">{currentUser.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,22 +135,22 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
{editing && (
|
||||
<form onSubmit={handleSaveProfile} className="mt-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300">Display Name</label>
|
||||
<label className="block text-sm font-medium text-stone-700">Display Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
className="mt-1 w-full rounded-lg border border-zinc-600 px-3 py-2 text-sm focus:border-violet-500 focus:outline-none focus:ring-1 focus:ring-slate-500"
|
||||
className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300">Phone Number</label>
|
||||
<label className="block text-sm font-medium text-stone-700">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
className="mt-1 w-full rounded-lg border border-zinc-600 px-3 py-2 text-sm focus:border-violet-500 focus:outline-none focus:ring-1 focus:ring-slate-500"
|
||||
className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
/>
|
||||
</div>
|
||||
@@ -158,14 +158,14 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEditing(false); setError(null); }}
|
||||
className="rounded-lg border border-zinc-800 px-4 py-2 text-sm font-medium text-zinc-400 hover:bg-zinc-800"
|
||||
className="rounded-lg border border-stone-200 px-4 py-2 text-sm font-medium text-stone-500 hover:bg-stone-100"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800 disabled:opacity-50"
|
||||
className="rounded-lg bg-stone-800 px-4 py-2 text-sm font-medium text-white hover:bg-stone-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving…" : "Save Changes"}
|
||||
</button>
|
||||
@@ -175,35 +175,35 @@ export default function AdminMeClient({ currentUser }: ProfilePageProps) {
|
||||
</div>
|
||||
|
||||
{/* Email change section */}
|
||||
<div className="mt-6 rounded-2xl bg-zinc-900 p-6 shadow-black/20 ring-1 ring-zinc-700">
|
||||
<h3 className="text-base font-semibold text-zinc-100">Email Address</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
Current: <span className="font-medium text-zinc-300">{currentUser.email}</span>
|
||||
<div className="mt-6 rounded-2xl bg-white p-6 shadow-black/20 ring-1 ring-stone-200">
|
||||
<h3 className="text-base font-semibold text-stone-950">Email Address</h3>
|
||||
<p className="mt-1 text-sm text-stone-500">
|
||||
Current: <span className="font-medium text-stone-700">{currentUser.email}</span>
|
||||
</p>
|
||||
|
||||
{emailChangeSent ? (
|
||||
<div className="mt-4 rounded-lg bg-green-900/30 p-4 text-sm text-green-400">
|
||||
<div className="mt-4 rounded-lg bg-green-50 p-4 text-sm text-green-700">
|
||||
A confirmation email has been sent to <strong>{newEmail}</strong>. Click the link in the email to confirm the change.
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleEmailChange} className="mt-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300">New Email Address</label>
|
||||
<label className="block text-sm font-medium text-stone-700">New Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
className="mt-1 w-full rounded-lg border border-zinc-600 px-3 py-2 text-sm focus:border-violet-500 focus:outline-none focus:ring-1 focus:ring-slate-500"
|
||||
className="mt-1 w-full rounded-lg border border-stone-300 px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
placeholder="new@example.com"
|
||||
/>
|
||||
</div>
|
||||
{emailError && (
|
||||
<div className="rounded-lg bg-red-900/30 p-3 text-sm text-red-400">{emailError}</div>
|
||||
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-700">{emailError}</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={changingEmail || !newEmail || !newEmail.includes("@")}
|
||||
className="rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800 disabled:opacity-50"
|
||||
className="rounded-lg bg-stone-800 px-4 py-2 text-sm font-medium text-white hover:bg-stone-700 disabled:opacity-50"
|
||||
>
|
||||
{changingEmail ? "Sending…" : "Change Email"}
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||
import AdminLookupPage from "@/components/route-trace/AdminLookupPage";
|
||||
import RouteTraceNav from "@/components/route-trace/RouteTraceNav";
|
||||
import RouteTracePage from "@/components/route-trace/RouteTracePage";
|
||||
import {
|
||||
getRouteTraceStats,
|
||||
getRouteTraceLots,
|
||||
getHarvestLotsReadyToHaul,
|
||||
getFieldYieldSummary,
|
||||
getInventoryByCrop,
|
||||
getRecentLotEvents,
|
||||
} from "@/actions/route-trace/lots";
|
||||
|
||||
export default async function LookupPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -13,16 +20,35 @@ export default async function LookupPage() {
|
||||
const enabled = await isFeatureEnabled(effectiveBrandId, "route_trace");
|
||||
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
|
||||
|
||||
const [statsResult, lotsResult, haulingResult, yieldResult, invResult, eventsResult] = await Promise.all([
|
||||
getRouteTraceStats(effectiveBrandId),
|
||||
getRouteTraceLots(effectiveBrandId),
|
||||
getHarvestLotsReadyToHaul(effectiveBrandId),
|
||||
getFieldYieldSummary(effectiveBrandId),
|
||||
getInventoryByCrop(effectiveBrandId),
|
||||
getRecentLotEvents(effectiveBrandId, 10),
|
||||
]);
|
||||
|
||||
const stats = statsResult.success ? statsResult.stats : {
|
||||
active_count: 0, in_transit_count: 0, at_shed_count: 0, total_lots_today: 0, total_harvested_today: 0, total_lots: 0,
|
||||
};
|
||||
const recentLots = lotsResult.success ? lotsResult.lots.slice(0, 8) : [];
|
||||
const allLots = lotsResult.success ? lotsResult.lots : [];
|
||||
const haulingLots = haulingResult.success ? haulingResult.lots : [];
|
||||
const fieldYield = yieldResult.success ? yieldResult.summary : [];
|
||||
const inventoryByCrop = invResult.success ? invResult.inventory : [];
|
||||
const recentActivity = eventsResult.success ? eventsResult.events : [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 px-6 py-10">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-semibold text-zinc-100">Lookup</h1>
|
||||
<p className="mt-1 text-sm text-zinc-500">Find lots by lot number or crop type</p>
|
||||
</div>
|
||||
<RouteTraceNav activeTab="lookup" />
|
||||
<AdminLookupPage brandId={effectiveBrandId} />
|
||||
</div>
|
||||
</div>
|
||||
<RouteTracePage
|
||||
stats={stats}
|
||||
recentLots={recentLots}
|
||||
haulingLots={haulingLots}
|
||||
fieldYield={fieldYield}
|
||||
inventoryByCrop={inventoryByCrop}
|
||||
recentActivity={recentActivity}
|
||||
brandId={effectiveBrandId}
|
||||
lots={allLots}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,14 @@ import { redirect } from "next/navigation";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||
import { getRouteTraceLots } from "@/actions/route-trace/lots";
|
||||
import RouteTraceNav from "@/components/route-trace/RouteTraceNav";
|
||||
import LotListTable from "@/components/route-trace/LotListTable";
|
||||
import Link from "next/link";
|
||||
import RouteTracePage from "@/components/route-trace/RouteTracePage";
|
||||
import {
|
||||
getRouteTraceStats,
|
||||
getHarvestLotsReadyToHaul,
|
||||
getFieldYieldSummary,
|
||||
getInventoryByCrop,
|
||||
getRecentLotEvents,
|
||||
} from "@/actions/route-trace/lots";
|
||||
|
||||
export default async function LotsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -15,27 +20,35 @@ export default async function LotsPage() {
|
||||
const enabled = await isFeatureEnabled(effectiveBrandId, "route_trace");
|
||||
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
|
||||
|
||||
const result = await getRouteTraceLots(effectiveBrandId);
|
||||
const lots = result.success ? result.lots : [];
|
||||
const [statsResult, lotsResult, haulingResult, yieldResult, invResult, eventsResult] = await Promise.all([
|
||||
getRouteTraceStats(effectiveBrandId),
|
||||
getRouteTraceLots(effectiveBrandId),
|
||||
getHarvestLotsReadyToHaul(effectiveBrandId),
|
||||
getFieldYieldSummary(effectiveBrandId),
|
||||
getInventoryByCrop(effectiveBrandId),
|
||||
getRecentLotEvents(effectiveBrandId, 10),
|
||||
]);
|
||||
|
||||
const stats = statsResult.success ? statsResult.stats : {
|
||||
active_count: 0, in_transit_count: 0, at_shed_count: 0, total_lots_today: 0, total_harvested_today: 0, total_lots: 0,
|
||||
};
|
||||
const recentLots = lotsResult.success ? lotsResult.lots.slice(0, 8) : [];
|
||||
const allLots = lotsResult.success ? lotsResult.lots : [];
|
||||
const haulingLots = haulingResult.success ? haulingResult.lots : [];
|
||||
const fieldYield = yieldResult.success ? yieldResult.summary : [];
|
||||
const inventoryByCrop = invResult.success ? invResult.inventory : [];
|
||||
const recentActivity = eventsResult.success ? eventsResult.events : [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 px-6 py-10">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-zinc-100">Lots</h1>
|
||||
<p className="mt-1 text-sm text-zinc-500">{lots.length} lot{lots.length !== 1 ? "s" : ""} total</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/route-trace/lots/new"
|
||||
className="rounded-xl bg-stone-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
+ New Lot
|
||||
</Link>
|
||||
</div>
|
||||
<RouteTraceNav activeTab="lots" />
|
||||
<LotListTable initialLots={lots} brandId={effectiveBrandId} />
|
||||
</div>
|
||||
</div>
|
||||
<RouteTracePage
|
||||
stats={stats}
|
||||
recentLots={recentLots}
|
||||
haulingLots={haulingLots}
|
||||
fieldYield={fieldYield}
|
||||
inventoryByCrop={inventoryByCrop}
|
||||
recentActivity={recentActivity}
|
||||
brandId={effectiveBrandId}
|
||||
lots={allLots}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getInventoryByCrop,
|
||||
getRecentLotEvents,
|
||||
} from "@/actions/route-trace/lots";
|
||||
import RouteTraceDashboard from "@/components/route-trace/RouteTraceDashboard";
|
||||
import RouteTracePage from "@/components/route-trace/RouteTracePage";
|
||||
|
||||
export default async function RouteTraceDashboardPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -33,24 +33,22 @@ export default async function RouteTraceDashboardPage() {
|
||||
active_count: 0, in_transit_count: 0, at_shed_count: 0, total_lots_today: 0, total_harvested_today: 0, total_lots: 0,
|
||||
};
|
||||
const recentLots = lotsResult.success ? lotsResult.lots.slice(0, 8) : [];
|
||||
const allLots = lotsResult.success ? lotsResult.lots : [];
|
||||
const haulingLots = haulingResult.success ? haulingResult.lots : [];
|
||||
const fieldYield = yieldResult.success ? yieldResult.summary : [];
|
||||
const inventoryByCrop = invResult.success ? invResult.inventory : [];
|
||||
const recentActivity = eventsResult.success ? eventsResult.events : [];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-8">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<RouteTraceDashboard
|
||||
stats={stats}
|
||||
recentLots={recentLots}
|
||||
haulingLots={haulingLots}
|
||||
fieldYield={fieldYield}
|
||||
inventoryByCrop={inventoryByCrop}
|
||||
recentActivity={recentActivity}
|
||||
brandId={effectiveBrandId}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<RouteTracePage
|
||||
stats={stats}
|
||||
recentLots={recentLots}
|
||||
haulingLots={haulingLots}
|
||||
fieldYield={fieldYield}
|
||||
inventoryByCrop={inventoryByCrop}
|
||||
recentActivity={recentActivity}
|
||||
brandId={effectiveBrandId}
|
||||
lots={allLots}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { isFeatureEnabled } from "@/lib/feature-flags";
|
||||
import RouteTraceNav from "@/components/route-trace/RouteTraceNav";
|
||||
import RouteTracePage from "@/components/route-trace/RouteTracePage";
|
||||
import {
|
||||
getRouteTraceStats,
|
||||
getRouteTraceLots,
|
||||
getHarvestLotsReadyToHaul,
|
||||
getFieldYieldSummary,
|
||||
getInventoryByCrop,
|
||||
getRecentLotEvents,
|
||||
} from "@/actions/route-trace/lots";
|
||||
|
||||
export default async function RouteTraceSettingsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -12,24 +20,35 @@ export default async function RouteTraceSettingsPage() {
|
||||
const enabled = await isFeatureEnabled(effectiveBrandId, "route_trace");
|
||||
if (!enabled) redirect("/admin/settings/apps?reason=route_trace");
|
||||
|
||||
const [statsResult, lotsResult, haulingResult, yieldResult, invResult, eventsResult] = await Promise.all([
|
||||
getRouteTraceStats(effectiveBrandId),
|
||||
getRouteTraceLots(effectiveBrandId),
|
||||
getHarvestLotsReadyToHaul(effectiveBrandId),
|
||||
getFieldYieldSummary(effectiveBrandId),
|
||||
getInventoryByCrop(effectiveBrandId),
|
||||
getRecentLotEvents(effectiveBrandId, 10),
|
||||
]);
|
||||
|
||||
const stats = statsResult.success ? statsResult.stats : {
|
||||
active_count: 0, in_transit_count: 0, at_shed_count: 0, total_lots_today: 0, total_harvested_today: 0, total_lots: 0,
|
||||
};
|
||||
const recentLots = lotsResult.success ? lotsResult.lots.slice(0, 8) : [];
|
||||
const allLots = lotsResult.success ? lotsResult.lots : [];
|
||||
const haulingLots = haulingResult.success ? haulingResult.lots : [];
|
||||
const fieldYield = yieldResult.success ? yieldResult.summary : [];
|
||||
const inventoryByCrop = invResult.success ? invResult.inventory : [];
|
||||
const recentActivity = eventsResult.success ? eventsResult.events : [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 px-6 py-10">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-semibold text-zinc-100">Route Trace Settings</h1>
|
||||
<p className="mt-1 text-sm text-zinc-500">Configure your traceability workflow and defaults</p>
|
||||
</div>
|
||||
<RouteTraceNav activeTab="settings" />
|
||||
<div className="rounded-2xl border border-zinc-800 bg-zinc-900 shadow-black/20 p-6">
|
||||
<p className="text-sm text-zinc-500">Settings coming soon. For now, use the Add-ons page to manage Route Trace.</p>
|
||||
<a
|
||||
href="/admin/settings/apps"
|
||||
className="mt-4 inline-flex items-center gap-1.5 rounded-xl bg-stone-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
→ Manage Add-ons
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RouteTracePage
|
||||
stats={stats}
|
||||
recentLots={recentLots}
|
||||
haulingLots={haulingLots}
|
||||
fieldYield={fieldYield}
|
||||
inventoryByCrop={inventoryByCrop}
|
||||
recentActivity={recentActivity}
|
||||
brandId={effectiveBrandId}
|
||||
lots={allLots}
|
||||
/>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAdminUsers, getBrands } from "@/actions/admin/users";
|
||||
import TimeTrackingSettingsClient from "@/components/admin/TimeTrackingSettingsClient";
|
||||
import UsersPage from "@/components/admin/UsersPage";
|
||||
import SettingsSections from "@/components/admin/SettingsSections";
|
||||
import IntegrationsInner from "@/components/admin/IntegrationsInner";
|
||||
import SettingsClient from "@/components/admin/SettingsClient";
|
||||
|
||||
export default async function AdminSettingsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -17,75 +14,28 @@ export default async function AdminSettingsPage() {
|
||||
getBrands(),
|
||||
]);
|
||||
|
||||
// Breadcrumb nav (for page context)
|
||||
const breadcrumb = (
|
||||
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-3">
|
||||
<a href="/admin" className="hover:text-stone-600 transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span className="text-stone-600">Settings</span>
|
||||
</nav>
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-10">
|
||||
<div className="mx-auto max-w-5xl space-y-10">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-3">
|
||||
<a href="/admin" className="hover:text-stone-600 transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span className="text-stone-600">Settings</span>
|
||||
</nav>
|
||||
<h1 className="text-3xl font-bold text-stone-950 tracking-tight">Settings</h1>
|
||||
<p className="mt-1.5 text-sm text-stone-500">Manage your brand, workers, tasks, users, and integrations.</p>
|
||||
</div>
|
||||
|
||||
{/* Nav to anchor sections */}
|
||||
<div className="flex flex-wrap gap-3 border-b border-stone-200 pb-4">
|
||||
<a href="#general" className="text-xs font-medium text-stone-500 hover:text-stone-800 transition-colors">General</a>
|
||||
<span className="text-stone-300">·</span>
|
||||
<a href="#workers" className="text-xs font-medium text-stone-500 hover:text-stone-800 transition-colors">Workers & PINs</a>
|
||||
<span className="text-stone-300">·</span>
|
||||
<a href="#tasks" className="text-xs font-medium text-stone-500 hover:text-stone-800 transition-colors">Tasks</a>
|
||||
<span className="text-stone-300">·</span>
|
||||
<a href="#users" className="text-xs font-medium text-stone-500 hover:text-stone-800 transition-colors">Users & Permissions</a>
|
||||
<span className="text-stone-300">·</span>
|
||||
<a href="#integrations" className="text-xs font-medium text-stone-500 hover:text-stone-800 transition-colors">Integrations</a>
|
||||
</div>
|
||||
|
||||
{/* Section 1: General Settings */}
|
||||
<section id="general">
|
||||
<SettingsSections brandId={brandId} />
|
||||
</section>
|
||||
|
||||
{/* Section 4: Users & Permissions */}
|
||||
<section id="users">
|
||||
<div className="border-t border-stone-200 pt-10">
|
||||
<h2 className="text-lg font-bold text-stone-950 mb-4">Users & Permissions</h2>
|
||||
<UsersPage
|
||||
initialUsers={error ? [] : users}
|
||||
brands={brands}
|
||||
currentUser={{
|
||||
id: adminUser.id ?? adminUser.user_id,
|
||||
role: adminUser.role,
|
||||
can_manage_users: adminUser.can_manage_users,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 5: Integrations */}
|
||||
<section id="integrations">
|
||||
<div className="border-t border-stone-200 pt-10">
|
||||
<h2 className="text-lg font-bold text-stone-950 mb-4">Integrations & Exports</h2>
|
||||
<IntegrationsInner brandId={brandId} brands={brands} />
|
||||
|
||||
{/* Time Tracking Exports */}
|
||||
<div className="mt-6 border-t border-stone-200 pt-6">
|
||||
<h3 className="text-base font-semibold text-stone-800 mb-4">Time Tracking Exports</h3>
|
||||
<TimeTrackingSettingsClient brandId={brandId} />
|
||||
</div>
|
||||
<p className="text-xs text-stone-500 mt-3">
|
||||
For more settings, see{" "}
|
||||
<a href="/admin/settings/ai" className="text-emerald-600 hover:text-emerald-700 underline underline-offset-2">AI Tools</a>
|
||||
{" "}and{" "}
|
||||
<a href="/admin/settings/shipping" className="text-emerald-600 hover:text-emerald-700 underline underline-offset-2">Shipping</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<main>
|
||||
{breadcrumb}
|
||||
<SettingsClient
|
||||
brandId={brandId}
|
||||
users={error ? [] : users}
|
||||
brands={brands}
|
||||
currentUser={{
|
||||
id: adminUser.id ?? adminUser.user_id,
|
||||
role: adminUser.role,
|
||||
can_manage_users: adminUser.can_manage_users,
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import SettingsSections from "@/components/admin/SettingsSections";
|
||||
import UsersPage from "@/components/admin/UsersPage";
|
||||
import IntegrationsInner from "@/components/admin/IntegrationsInner";
|
||||
import TimeTrackingSettingsClient from "@/components/admin/TimeTrackingSettingsClient";
|
||||
|
||||
type Tab = "general" | "users" | "integrations";
|
||||
|
||||
const TABS: { id: Tab; label: string }[] = [
|
||||
{ id: "general", label: "General" },
|
||||
{ id: "users", label: "Users" },
|
||||
{ id: "integrations", label: "Integrations" },
|
||||
];
|
||||
|
||||
type Brand = { id: string; name: string };
|
||||
|
||||
type Props = {
|
||||
brandId: string;
|
||||
users: import("@/actions/admin/users").AdminUserRow[];
|
||||
brands: Brand[];
|
||||
currentUser: {
|
||||
id: string;
|
||||
role: string;
|
||||
can_manage_users: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export default function SettingsClient({
|
||||
brandId,
|
||||
users,
|
||||
brands,
|
||||
currentUser,
|
||||
}: Props) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("general");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50 px-6 py-10">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-3">
|
||||
<a href="/admin" className="hover:text-stone-600 transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span className="text-stone-600">Settings</span>
|
||||
</nav>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-stone-950 tracking-tight">Settings</h1>
|
||||
<p className="mt-1.5 text-sm text-stone-500">Manage your brand, workers, tasks, users, and integrations.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab nav */}
|
||||
<div className="border-b border-stone-200 mt-6">
|
||||
<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 === "general" && (
|
||||
<div className="rounded-2xl bg-white border border-stone-200 shadow-sm p-6">
|
||||
<SettingsSections brandId={brandId} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "users" && (
|
||||
<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">
|
||||
<h2 className="text-lg font-bold text-stone-950">Users & Permissions</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<UsersPage
|
||||
initialUsers={users}
|
||||
brands={brands}
|
||||
currentUser={{
|
||||
id: currentUser.id,
|
||||
role: currentUser.role,
|
||||
can_manage_users: currentUser.can_manage_users,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "integrations" && (
|
||||
<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">
|
||||
<h2 className="text-lg font-bold text-stone-950">Integrations & Exports</h2>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<IntegrationsInner brandId={brandId} brands={brands} />
|
||||
|
||||
{/* Time Tracking Exports */}
|
||||
<div className="border-t border-stone-200 pt-6">
|
||||
<h3 className="text-base font-semibold text-stone-800 mb-4">Time Tracking Exports</h3>
|
||||
<TimeTrackingSettingsClient brandId={brandId} />
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-stone-500">
|
||||
For more settings, see{" "}
|
||||
<a href="/admin/settings/ai" className="text-emerald-600 hover:text-emerald-700 underline underline-offset-2">AI Tools</a>
|
||||
{" "}and{" "}
|
||||
<a href="/admin/settings/shipping" className="text-emerald-600 hover:text-emerald-700 underline underline-offset-2">Shipping</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user