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
+69 -69
View File
@@ -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 510 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 510 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>
+30 -30
View File
@@ -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>
+38 -12
View File
@@ -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}
/>
);
}
+36 -23
View File
@@ -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}
/>
);
}
+12 -14
View File
@@ -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}
/>
);
}
+38 -19
View File
@@ -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
+22 -72
View File
@@ -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>
);
}