feat(admin): design system audit fixes
- Add CSS design tokens for consistency (--admin-danger-hover, --admin-accent-dot, shadow-md warm tone) - Replace unicode icons with inline SVG (⋮, ✕, ▼, ▶) - Consolidate duplicate PageHeader/AdminPageHeader components - Standardize border-radius (rounded-xl → rounded-lg for ViewModeTabs) - Remove hardcoded brand UUID in products page - Replace hardcoded bg-red-600 with CSS variables in delete buttons - Add AdminButton, AdminFilterTabs, AdminSearchInput, PageHeader design system components - Fix GlassModal and AdminModal close button styling - Remove duplicate 'New Lot' button on Route Trace dashboard
This commit is contained in:
@@ -3,6 +3,7 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getBrands } from "@/actions/admin/users";
|
||||
import { getStripeConnectStatus } from "@/actions/stripe-connect";
|
||||
import AdvancedSettingsClient from "@/components/admin/AdvancedSettingsClient";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
|
||||
export const metadata = {
|
||||
title: "Advanced Settings - Route Commerce Admin",
|
||||
@@ -25,10 +26,28 @@ export default async function AdvancedSettingsPage() {
|
||||
const stripeConnect = brandId ? await getStripeConnectStatus(brandId) : null;
|
||||
|
||||
return (
|
||||
<AdvancedSettingsClient
|
||||
brandId={brandId}
|
||||
brands={brands}
|
||||
stripeConnect={stripeConnect}
|
||||
/>
|
||||
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-10">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<PageHeader
|
||||
breadcrumb={[
|
||||
{ label: "Admin", href: "/admin" },
|
||||
{ label: "Settings", href: "/admin/settings" },
|
||||
{ label: "Advanced" },
|
||||
]}
|
||||
title="Advanced Settings"
|
||||
subtitle="Developer settings, APIs, and integrations"
|
||||
icon={
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z"/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
<AdvancedSettingsClient
|
||||
brandId={brandId}
|
||||
brands={brands}
|
||||
stripeConnect={stripeConnect}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -136,22 +136,8 @@ 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-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-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-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>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-[var(--admin-border)] overflow-hidden">
|
||||
<div className="p-6">
|
||||
{/* Step indicator */}
|
||||
<div className="mb-8 flex items-center gap-2">
|
||||
{(["upload", "analysis", "preview", "import"] as Step[]).map((s, i) => {
|
||||
@@ -161,12 +147,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-stone-200 text-stone-500"
|
||||
isActive ? "bg-[var(--admin-accent)] text-white" : isPast ? "bg-green-600 text-white" : "bg-[var(--admin-bg)] text-[var(--admin-text-muted)] border border-[var(--admin-border)]"
|
||||
}`}>
|
||||
{isPast ? "✓" : i + 1}
|
||||
</div>
|
||||
<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" />}
|
||||
<span className={`text-sm font-medium ${isActive ? "text-[var(--admin-text-primary)]" : "text-[var(--admin-text-muted)]"}`}>{labels[i]}</span>
|
||||
{i < 3 && <div className="h-px w-8 bg-[var(--admin-border)]" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -174,12 +160,12 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
|
||||
{/* Brand selector */}
|
||||
{isPlatformAdmin && (
|
||||
<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>
|
||||
<div className="mb-6 rounded-xl bg-[var(--admin-bg)] border border-[var(--admin-border)] p-4">
|
||||
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Importing to brand</label>
|
||||
<select
|
||||
value={activeBrandId}
|
||||
onChange={(e) => handleBrandChange(e.target.value)}
|
||||
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"
|
||||
className="w-full rounded-xl border border-[var(--admin-border)] bg-white px-4 py-2.5 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)]"
|
||||
>
|
||||
{brands.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
|
||||
</select>
|
||||
@@ -193,29 +179,29 @@ 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-stone-300 bg-white p-12 cursor-pointer hover:border-violet-500 hover:bg-stone-50 transition-colors"
|
||||
className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-[var(--admin-border)] bg-white p-12 cursor-pointer hover:border-[var(--admin-accent)] hover:bg-[var(--admin-bg)] transition-colors"
|
||||
>
|
||||
<svg className="h-10 w-10 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="h-10 w-10 text-[var(--admin-text-muted)]" 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-stone-700">Drag & drop your file here</p>
|
||||
<p className="mt-1 text-sm text-stone-500">or click to browse</p>
|
||||
<p className="text-base font-medium text-[var(--admin-text-primary)]">Drag & drop your file here</p>
|
||||
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">or click to browse</p>
|
||||
</div>
|
||||
<p className="text-xs text-stone-400">CSV, XLSX, XLS, TXT · max 5,000 rows · 10MB</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">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-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">
|
||||
<div className="rounded-xl bg-green-50 border border-green-200 p-4 flex items-center gap-3">
|
||||
<svg className="h-5 w-5 text-green-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-emerald-700 truncate">{fileName}</p>
|
||||
<p className="text-xs text-emerald-600">Ready to analyze</p>
|
||||
<p className="text-sm font-medium text-green-700 truncate">{fileName}</p>
|
||||
<p className="text-xs text-green-600">Ready to analyze</p>
|
||||
</div>
|
||||
<button onClick={() => { setFileName(""); setBase64Data(""); }} className="text-sm text-emerald-600 font-medium hover:text-emerald-700">Change</button>
|
||||
<button onClick={() => { setFileName(""); setBase64Data(""); }} className="text-sm text-green-600 font-medium hover:text-green-700">Change</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -226,7 +212,7 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
disabled={!base64Data}
|
||||
className="w-full rounded-xl bg-violet-600 px-6 py-3.5 text-base font-bold text-white hover:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
className="w-full rounded-xl bg-[var(--admin-accent)] px-6 py-3.5 text-base font-bold text-white hover:bg-[var(--admin-accent-hover)] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
|
||||
@@ -234,19 +220,19 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
Analyze with AI
|
||||
</button>
|
||||
|
||||
<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 className="rounded-xl bg-[var(--admin-accent-light)] border border-[var(--admin-accent)] p-4">
|
||||
<p className="text-sm font-medium text-[var(--admin-accent-text)] mb-1">AI-assisted import</p>
|
||||
<p className="text-xs text-[var(--admin-accent-text)]">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-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-stone-700 mb-2">{analysisLabels[analysisStep]}</p>
|
||||
<p className="text-sm text-stone-500">This usually takes 5–10 seconds</p>
|
||||
<div className="rounded-2xl bg-[var(--admin-bg)] border border-[var(--admin-border)] p-16 text-center">
|
||||
<div className="mx-auto mb-6 h-12 w-12 rounded-full border-4 border-[var(--admin-accent)] border-t-transparent animate-spin" />
|
||||
<p className="text-xl font-semibold text-[var(--admin-text-primary)] mb-2">{analysisLabels[analysisStep]}</p>
|
||||
<p className="text-sm text-[var(--admin-text-muted)]">This usually takes 5–10 seconds</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -254,33 +240,33 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
{step === "preview" && analysis && (
|
||||
<div className="space-y-6">
|
||||
{/* Detected type banner */}
|
||||
<div className="rounded-2xl bg-white border border-stone-200 p-6">
|
||||
<div className="rounded-2xl bg-[var(--admin-bg)] border border-[var(--admin-border)] 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-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"
|
||||
analysis.confidence > 0.8 ? "bg-green-100 text-green-700 border border-green-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-stone-500">
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">
|
||||
{Math.round(analysis.confidence * 100)}% confidence
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-stone-600">
|
||||
<p className="text-sm text-[var(--admin-text-secondary)]">
|
||||
{analysis.rowCount.toLocaleString()} rows detected
|
||||
{analysis.autoFixApplied.length > 0 && (
|
||||
<span className="ml-2 text-emerald-600">· {analysis.autoFixApplied.join(", ")}</span>
|
||||
<span className="ml-2 text-green-600">· {analysis.autoFixApplied.join(", ")}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{analysis.confidence < 0.8 && (
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-stone-500 mb-1">Is this wrong?</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] 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-stone-300 text-stone-600 hover:bg-stone-100"
|
||||
t === analysis.detectedType ? "bg-[var(--admin-accent)] text-white border-[var(--admin-accent)]" : "border-[var(--admin-border)] text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)]"
|
||||
}`}>{ENTITY_LABELS[t]}</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -301,18 +287,18 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
)}
|
||||
|
||||
{/* Column mapping table */}
|
||||
<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 className="rounded-2xl bg-white border border-[var(--admin-border)] overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-[var(--admin-border)] flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Column Mappings</h3>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">AI auto-detected — click to change</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<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 className="bg-[var(--admin-bg)]">
|
||||
<th className="px-5 py-2.5 text-left font-medium text-[var(--admin-text-muted)]">Your Column</th>
|
||||
<th className="px-5 py-2.5 text-left font-medium text-[var(--admin-text-muted)]">Maps To</th>
|
||||
<th className="px-5 py-2.5 text-left font-medium text-[var(--admin-text-muted)]">Sample Values</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -321,19 +307,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-stone-200 hover:bg-stone-50">
|
||||
<td className="px-5 py-2.5 font-medium text-stone-800 whitespace-nowrap">{header}</td>
|
||||
<tr key={header} className="border-t border-[var(--admin-border)] hover:bg-[var(--admin-bg)]">
|
||||
<td className="px-5 py-2.5 font-medium text-[var(--admin-text-primary)] 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-stone-300 bg-white px-2 py-1 text-xs text-stone-800 outline-none focus:border-violet-500"
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-white px-2 py-1 text-xs text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-accent)]"
|
||||
>
|
||||
<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-stone-500 text-xs max-w-xs truncate">{samples.join(", ") || "—"}</td>
|
||||
<td className="px-5 py-2.5 text-[var(--admin-text-muted)] text-xs max-w-xs truncate">{samples.join(", ") || "—"}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -343,28 +329,28 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
</div>
|
||||
|
||||
{/* Data preview */}
|
||||
<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 className="rounded-2xl bg-white border border-[var(--admin-border)] overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-[var(--admin-border)]">
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Data Preview <span className="text-[var(--admin-text-muted)] font-normal">(first 5 rows)</span></h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="bg-stone-50">
|
||||
<tr className="bg-[var(--admin-bg)]">
|
||||
{analysis.headers.map((h) => (
|
||||
<th key={h} className="px-4 py-2 text-left font-medium text-stone-500 whitespace-nowrap">{h}</th>
|
||||
<th key={h} className="px-4 py-2 text-left font-medium text-[var(--admin-text-muted)] whitespace-nowrap">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analysis.rawRows.slice(0, 5).map((row, ri) => (
|
||||
<tr key={ri} className="border-t border-stone-200">
|
||||
<tr key={ri} className="border-t border-[var(--admin-border)]">
|
||||
{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-stone-700" : "text-stone-400 italic"}`}>
|
||||
<td key={ci} className={`px-4 py-2 whitespace-nowrap ${isMapped ? "text-[var(--admin-text-secondary)]" : "text-[var(--admin-text-muted)] italic"}`}>
|
||||
{String(val).slice(0, 60)}
|
||||
</td>
|
||||
);
|
||||
@@ -378,13 +364,13 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3">
|
||||
<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">
|
||||
<button onClick={() => { setStep("upload"); setAnalysis(null); }} className="rounded-xl border border-[var(--admin-border)] px-6 py-3 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)]">
|
||||
← Upload Different File
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!analysis.detectedType || analysis.detectedType === "unknown" || importing}
|
||||
className="flex-1 rounded-xl bg-violet-600 px-6 py-3 text-base font-bold text-white hover:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex-1 rounded-xl bg-[var(--admin-accent)] px-6 py-3 text-base font-bold text-white hover:bg-[var(--admin-accent-hover)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{importing ? "Importing..." : `Import ${analysis.rowCount.toLocaleString()} ${analysis.detectedType !== "unknown" ? ENTITY_LABELS[analysis.detectedType] : "rows"}`}
|
||||
</button>
|
||||
@@ -396,38 +382,37 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
|
||||
{step === "import" && (
|
||||
<div className="space-y-4">
|
||||
{importResult?.success ? (
|
||||
<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">
|
||||
<div className="rounded-2xl bg-[var(--admin-bg)] border border-green-200 p-8 text-center">
|
||||
<div className="mx-auto mb-4 h-12 w-12 rounded-full bg-green-100 border border-green-200 flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-green-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-stone-950 mb-2">Import Complete</h2>
|
||||
{importResult.created !== undefined && <p className="text-stone-600">{importResult.created} created{importResult.updated ? `, ${importResult.updated} updated` : ""}</p>}
|
||||
<h2 className="text-xl font-bold text-[var(--admin-text-primary)] mb-2">Import Complete</h2>
|
||||
{importResult.created !== undefined && <p className="text-[var(--admin-text-secondary)]">{importResult.created} created{importResult.updated ? `, ${importResult.updated} updated` : ""}</p>}
|
||||
{importResult.errors.length > 0 && (
|
||||
<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-stone-300 px-6 py-2.5 text-sm font-medium text-stone-700 hover:bg-stone-100">
|
||||
<button onClick={() => { setStep("upload"); setAnalysis(null); setFileName(""); setBase64Data(""); setImportResult(null); }} className="rounded-xl border border-[var(--admin-border)] px-6 py-2.5 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)]">
|
||||
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">
|
||||
<a href={analysis?.detectedType === "products" ? "/admin/products" : analysis?.detectedType === "orders" ? "/admin/orders" : "/admin"} className="rounded-xl bg-[var(--admin-accent)] px-6 py-2.5 text-sm font-bold text-white hover:bg-[var(--admin-accent-hover)]">
|
||||
View {analysis ? ENTITY_LABELS[analysis.detectedType] : ""}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
<div className="rounded-2xl bg-[var(--admin-bg)] border border-red-200 p-8 text-center">
|
||||
<h2 className="text-xl font-bold text-[var(--admin-text-primary)] mb-2">Import Failed</h2>
|
||||
<p className="text-[var(--admin-text-secondary)]">{importError ?? "An error occurred during import."}</p>
|
||||
<button onClick={() => setStep("preview")} className="mt-4 rounded-xl border border-[var(--admin-border)] px-6 py-2.5 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)]">
|
||||
← Back to Preview
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import ImportCenterClient from "./ImportCenterClient";
|
||||
import { getBrands } from "@/actions/admin/users";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
|
||||
export default async function ImportCenterPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -20,13 +21,21 @@ export default async function ImportCenterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-100 px-6 py-10">
|
||||
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-10">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
|
||||
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<span className="text-stone-600">Import</span>
|
||||
</nav>
|
||||
<PageHeader
|
||||
breadcrumb={[
|
||||
{ label: "Admin", href: "/admin" },
|
||||
{ label: "Import" },
|
||||
]}
|
||||
title="Import Center"
|
||||
subtitle="AI-powered data import for products, orders, contacts, and stops"
|
||||
icon={
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path 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>
|
||||
}
|
||||
/>
|
||||
<ImportCenterClient
|
||||
brandId={brandId}
|
||||
brandName={brandName}
|
||||
|
||||
@@ -2,6 +2,7 @@ import AdminOrdersPanel from "@/components/admin/AdminOrdersPanel";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAdminOrders } from "@/actions/orders";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -30,21 +31,18 @@ export default async function AdminOrdersPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-xl bg-emerald-600">
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<PageHeader
|
||||
title="Orders"
|
||||
subtitle="Manage customer orders and pickup status"
|
||||
icon={
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/>
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M16 10a4 4 0 0 1-8 0"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-black text-[var(--admin-text-primary)] tracking-tight">Orders</h1>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Manage customer orders and pickup status</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -3,17 +3,15 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import ProductsClient from "@/components/admin/ProductsClient";
|
||||
|
||||
// Icon components
|
||||
const Icons = {
|
||||
package: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m7.5 4.27 9 5.15"/>
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/>
|
||||
<path d="m3.3 7 8.7 5 8.7-5"/>
|
||||
<path d="M12 22V12"/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
// Icon for page header
|
||||
const PackageIcon = () => (
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m7.5 4.27 9 5.15"/>
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/>
|
||||
<path d="m3.3 7 8.7 5 8.7-5"/>
|
||||
<path d="M12 22V12"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default async function AdminProductsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
@@ -31,7 +29,7 @@ export default async function AdminProductsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const brandId = adminUser.brand_id ?? "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||
const brandId = adminUser.brand_id;
|
||||
|
||||
let query = supabase
|
||||
.from("products")
|
||||
@@ -71,25 +69,7 @@ export default async function AdminProductsPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-xl bg-emerald-600">
|
||||
{Icons.package("h-5 w-5 sm:h-6 sm:w-6 text-white")}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-black text-[var(--admin-text-primary)] tracking-tight">Products</h1>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Manage your product catalog</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||
<ProductsClient products={products ?? []} brandId={brandId} />
|
||||
</div>
|
||||
</div>
|
||||
<ProductsClient products={products ?? []} brandId={brandId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import ReportsDashboard from "@/components/admin/ReportsDashboard";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ReportsPage() {
|
||||
@@ -23,12 +24,10 @@ export default async function ReportsPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-100 px-6 py-10">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-stone-950">Reports</h1>
|
||||
<p className="mt-1 text-sm text-stone-500">
|
||||
Operational visibility across orders, fulfillment, contacts, and campaigns.
|
||||
</p>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="Reports"
|
||||
subtitle="Operational visibility across orders, fulfillment, contacts, and campaigns."
|
||||
/>
|
||||
|
||||
<ReportsDashboard
|
||||
brands={brands ?? []}
|
||||
|
||||
@@ -3,8 +3,16 @@ import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const StopIcon = () => (
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default async function AdminStopsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
@@ -67,32 +75,17 @@ export default async function AdminStopsPage() {
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-xl bg-emerald-600">
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-black text-[var(--admin-text-primary)] tracking-tight">Stops & Routes</h1>
|
||||
<p className="text-xs sm:text-sm text-[var(--admin-text-muted)]">
|
||||
{adminUser?.brand_id ? "Managing stops for your brand." : "Manage routes, pickup locations, dates, and cutoff times."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<StopsHeaderActions brandId={adminUser?.brand_id ?? ""} />
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 mt-4 text-xs sm:text-sm">
|
||||
<a href="/admin" className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]">Admin</a>
|
||||
<span className="text-[var(--admin-text-muted)]">/</span>
|
||||
<span className="text-[var(--admin-text-primary)] font-medium">Stops & Routes</span>
|
||||
</div>
|
||||
<PageHeader
|
||||
breadcrumb={[
|
||||
{ label: "Admin", href: "/admin" },
|
||||
{ label: "Stops & Routes" }
|
||||
]}
|
||||
icon={<StopIcon />}
|
||||
title="Stops & Routes"
|
||||
subtitle={adminUser?.brand_id ? "Managing stops for your brand." : "Manage routes, pickup locations, dates, and cutoff times."}
|
||||
actions={<StopsHeaderActions brandId={adminUser?.brand_id ?? ""} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import TaxDashboard from "@/components/admin/TaxDashboard";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ brandId?: string }>;
|
||||
@@ -55,12 +56,10 @@ export default async function TaxesPage({ params }: Props) {
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-100">
|
||||
<div className="mx-auto max-w-6xl px-6 py-10">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-stone-950">Tax Dashboard</h1>
|
||||
<p className="mt-1 text-stone-500">
|
||||
Sales tax collected on orders shipped to nexus states.
|
||||
</p>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="Tax Dashboard"
|
||||
subtitle="Sales tax collected on orders shipped to nexus states."
|
||||
/>
|
||||
|
||||
<TaxDashboard
|
||||
brands={allBrands}
|
||||
|
||||
@@ -2,6 +2,7 @@ import TimeTrackingAdminPanel from "@/components/admin/TimeTrackingAdminPanel";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
|
||||
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||
const IRD_BRAND_ID = "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28";
|
||||
@@ -33,6 +34,15 @@ export default async function AdminTimeTrackingPage() {
|
||||
const effectiveBrandId = adminUser?.brand_id ?? TUXEDO_BRAND_ID;
|
||||
|
||||
return (
|
||||
<TimeTrackingAdminPanel brandId={effectiveBrandId} />
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<PageHeader
|
||||
title="Time Tracking"
|
||||
subtitle="Manage workers, tasks, and time logs"
|
||||
className="px-6 pt-6"
|
||||
/>
|
||||
<div className="px-6 pb-8">
|
||||
<TimeTrackingAdminPanel brandId={effectiveBrandId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
regenerateHeadgateToken,
|
||||
updateWaterHeadgate,
|
||||
} from "@/actions/water-log/admin";
|
||||
import { AdminButton } from "@/components/admin/design-system";
|
||||
|
||||
type Headgate = {
|
||||
id: string;
|
||||
@@ -159,20 +160,20 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950">
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
{/* Header */}
|
||||
<div className="bg-zinc-900 border-b border-zinc-800 px-6 py-4">
|
||||
<div className="bg-white border-b border-[var(--admin-border)] px-6 py-4">
|
||||
<div className="mx-auto max-w-4xl flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => router.back()} className="text-stone-400 hover:text-stone-600 text-sm">← Back</button>
|
||||
<h1 className="text-2xl font-bold text-zinc-100">Headgates & QR Codes</h1>
|
||||
<button onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm">← Back</button>
|
||||
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Headgates & QR Codes</h1>
|
||||
</div>
|
||||
<p className="text-xs text-stone-500 mt-0.5">Manage headgates and generate QR codes for field access</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">Manage headgates and generate QR codes for field access</p>
|
||||
</div>
|
||||
<button onClick={() => setShowAdd(true)} className="rounded-lg bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700">
|
||||
<AdminButton onClick={() => setShowAdd(true)}>
|
||||
+ Add Headgate
|
||||
</button>
|
||||
</AdminButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -180,138 +181,137 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
|
||||
{/* Bulk actions bar */}
|
||||
{headgates.length > 0 && (
|
||||
<div className="flex items-center gap-3 rounded-xl bg-zinc-900 ring-1 ring-stone-200 px-4 py-3">
|
||||
<button onClick={toggleAll} className="text-sm text-stone-600 hover:text-zinc-100">
|
||||
<div className="flex items-center gap-3 rounded-xl border border-[var(--admin-border)] bg-white px-4 py-3">
|
||||
<button onClick={toggleAll} className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
{selected.size === headgates.length ? "Deselect all" : "Select all"}
|
||||
</button>
|
||||
<span className="text-xs text-stone-400">|</span>
|
||||
<span className="text-xs text-stone-500">{selected.size} selected</span>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">|</span>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">{selected.size} selected</span>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
<AdminButton
|
||||
onClick={printSelected}
|
||||
disabled={selected.size === 0 || printLoading}
|
||||
className="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white disabled:opacity-40 hover:bg-blue-700"
|
||||
disabled={selected.size === 0}
|
||||
isLoading={printLoading}
|
||||
>
|
||||
{printLoading ? "..." : `Print ${selected.size > 0 ? `(${selected.size})` : ""} QR Codes`}
|
||||
</button>
|
||||
Print {selected.size > 0 ? `(${selected.size})` : ""} QR Codes
|
||||
</AdminButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add form */}
|
||||
{showAdd && (
|
||||
<div className="rounded-xl bg-zinc-900 ring-1 ring-stone-200 p-5">
|
||||
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-5">
|
||||
<form onSubmit={handleAdd} className="flex gap-3 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1">Headgate Name</label>
|
||||
<label className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Headgate Name</label>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="e.g. North Field Gate 1"
|
||||
className="w-full rounded-xl border border-stone-300 px-4 py-3 text-sm outline-none focus:border-stone-900"
|
||||
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1">Unit</label>
|
||||
<label className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
|
||||
<select
|
||||
value={newUnit}
|
||||
onChange={(e) => setNewUnit(e.target.value)}
|
||||
className="rounded-xl border border-stone-300 px-3 py-3 text-sm outline-none focus:border-stone-900"
|
||||
className="rounded-xl border border-[var(--admin-border)] px-3 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
|
||||
>
|
||||
{["CFS", "GPM", "Inches", "AF/Day"].map((u) => (
|
||||
<option key={u} value={u}>{u}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" disabled={saving} className="rounded-xl bg-green-600 px-5 py-3 text-sm font-bold text-white disabled:opacity-50">
|
||||
{saving ? "..." : "Create"}
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowAdd(false)} className="rounded-xl border border-stone-300 px-4 py-3 text-sm text-stone-600">Cancel</button>
|
||||
<AdminButton type="submit" disabled={saving} isLoading={saving}>
|
||||
Create
|
||||
</AdminButton>
|
||||
<AdminButton type="button" variant="secondary" onClick={() => setShowAdd(false)}>
|
||||
Cancel
|
||||
</AdminButton>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
{headgates.length === 0 ? (
|
||||
<div className="rounded-xl bg-zinc-900 py-16 text-center text-stone-400 ring-1 ring-stone-200">
|
||||
<div className="rounded-xl bg-white py-16 text-center text-[var(--admin-text-muted)] border border-[var(--admin-border)]">
|
||||
No headgates yet. Create one to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl bg-zinc-900 shadow-black/20 ring-1 ring-stone-200 overflow-hidden">
|
||||
<div className="rounded-xl border border-[var(--admin-border)] bg-white overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-stone-100 bg-zinc-950 text-left">
|
||||
<tr className="border-b border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] text-left">
|
||||
<th className="px-4 py-3 w-8">
|
||||
<input type="checkbox" className="rounded" onChange={toggleAll} checked={selected.size === headgates.length} />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-stone-500">Name</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-stone-500">Token</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-stone-500">Unit</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-stone-500">Last Used</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-stone-500">Status</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-stone-500 text-right">QR Code</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Name</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Token</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Unit</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Last Used</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Status</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)] text-right">QR Code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{headgates.map((hg) => (
|
||||
<tr key={hg.id} className={`border-b border-stone-50 last:border-0 hover:bg-zinc-950 ${selected.has(hg.id) ? "bg-blue-900/30" : ""}`}>
|
||||
<tr key={hg.id} className={`border-b border-[var(--admin-border)] last:border-0 hover:bg-[var(--admin-bg-subtle)] ${selected.has(hg.id) ? "bg-blue-50" : ""}`}>
|
||||
<td className="px-4 py-3">
|
||||
<input type="checkbox" className="rounded" checked={selected.has(hg.id)} onChange={() => toggleSelect(hg.id)} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-zinc-100">{hg.name}</span>
|
||||
<span className="font-semibold text-[var(--admin-text-primary)]">{hg.name}</span>
|
||||
{hg.high_threshold != null && (
|
||||
<span className="inline-flex items-center rounded-full bg-red-900/30 border border-red-200 px-1.5 py-0.5 text-xs font-semibold text-red-400">
|
||||
<span className="inline-flex items-center rounded-full bg-red-100 border border-red-200 px-1.5 py-0.5 text-xs font-semibold text-red-700">
|
||||
High: {hg.high_threshold}
|
||||
</span>
|
||||
)}
|
||||
{hg.low_threshold != null && (
|
||||
<span className="inline-flex items-center rounded-full bg-blue-900/30 border border-blue-200 px-1.5 py-0.5 text-xs font-semibold text-blue-700">
|
||||
<span className="inline-flex items-center rounded-full bg-blue-100 border border-blue-200 px-1.5 py-0.5 text-xs font-semibold text-blue-700">
|
||||
Low: {hg.low_threshold}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<code className="text-xs text-stone-500">{hg.headgate_token?.slice(0, 8)}…</code>
|
||||
<code className="text-xs text-[var(--admin-text-muted)]">{hg.headgate_token?.slice(0, 8)}…</code>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-xs text-stone-500">{hg.unit}</span>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">{hg.unit}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-xs text-stone-500">
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">
|
||||
{hg.last_used_at
|
||||
? new Date(hg.last_used_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })
|
||||
: "Never"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold ${hg.active ? "bg-green-900/40 text-green-400" : "bg-stone-100 text-stone-500"}`}>
|
||||
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold ${hg.active ? "bg-green-100 text-green-700" : "bg-stone-100 text-stone-500"}`}>
|
||||
{hg.active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button onClick={() => openEdit(hg)} className="rounded-lg border border-zinc-800 bg-zinc-900 px-2 py-1 text-xs font-medium text-stone-600 hover:bg-zinc-950">
|
||||
<AdminButton variant="secondary" size="sm" onClick={() => openEdit(hg)}>
|
||||
Edit
|
||||
</button>
|
||||
</AdminButton>
|
||||
{/* QR preview thumbnail */}
|
||||
<button onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
|
||||
<img
|
||||
src={`/api/water-qr?token=${hg.headgate_token}&size=80`}
|
||||
alt={`QR for ${hg.name}`}
|
||||
className="w-10 h-10 rounded border border-zinc-800"
|
||||
className="w-10 h-10 rounded border border-[var(--admin-border)]"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setQrModal(hg)}
|
||||
className="rounded-lg border border-zinc-800 bg-zinc-900 px-2 py-1 text-xs font-medium text-stone-600 hover:bg-zinc-950"
|
||||
>
|
||||
<AdminButton variant="secondary" size="sm" onClick={() => setQrModal(hg)}>
|
||||
Print
|
||||
</button>
|
||||
</AdminButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -324,7 +324,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className={`fixed bottom-6 right-6 rounded-xl px-4 py-3 text-sm font-semibold shadow-lg ${toast.ok ? "bg-green-900/40 text-green-800 border border-green-200" : "bg-red-900/40 text-red-800 border border-red-200"}`}>
|
||||
<div className={`fixed bottom-6 right-6 rounded-xl px-4 py-3 text-sm font-semibold shadow-lg ${toast.ok ? "bg-green-100 text-green-800 border border-green-200" : "bg-red-100 text-red-800 border border-red-200"}`}>
|
||||
{toast.msg}
|
||||
</div>
|
||||
)}
|
||||
@@ -332,29 +332,33 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
{/* Edit Modal */}
|
||||
{editHg && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setEditHg(null)}>
|
||||
<div className="bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800">
|
||||
<p className="font-bold text-zinc-100">Edit Headgate</p>
|
||||
<button onClick={() => setEditHg(null)} className="text-stone-400 hover:text-stone-600 text-xl leading-none">✕</button>
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
|
||||
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
|
||||
<button onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleEditSave} className="p-5 space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1">Name</label>
|
||||
<label className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="w-full rounded-xl border border-stone-300 px-4 py-3 text-sm outline-none focus:border-stone-900"
|
||||
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1">Unit</label>
|
||||
<label className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
|
||||
<select
|
||||
value={editUnit}
|
||||
onChange={(e) => setEditUnit(e.target.value)}
|
||||
className="w-full rounded-xl border border-stone-300 px-3 py-3 text-sm outline-none focus:border-stone-900"
|
||||
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
|
||||
>
|
||||
{["CFS", "GPM", "Inches", "AF/Day"].map((u) => (
|
||||
<option key={u} value={u}>{u}</option>
|
||||
@@ -362,7 +366,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center gap-2 text-sm text-stone-700 cursor-pointer">
|
||||
<label className="flex items-center gap-2 text-sm text-[var(--admin-text-secondary)] cursor-pointer">
|
||||
<input type="checkbox" checked={editActive} onChange={(e) => setEditActive(e.target.checked)} className="rounded" />
|
||||
Active
|
||||
</label>
|
||||
@@ -370,34 +374,36 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1">High Alert Threshold</label>
|
||||
<label className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">High Alert Threshold</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={editHigh}
|
||||
onChange={(e) => setEditHigh(e.target.value)}
|
||||
placeholder="e.g. 15.0"
|
||||
className="w-full rounded-xl border border-stone-300 px-4 py-3 text-sm outline-none focus:border-stone-900"
|
||||
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1">Low Alert Threshold</label>
|
||||
<label className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Low Alert Threshold</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={editLow}
|
||||
onChange={(e) => setEditLow(e.target.value)}
|
||||
placeholder="e.g. 5.0"
|
||||
className="w-full rounded-xl border border-stone-300 px-4 py-3 text-sm outline-none focus:border-stone-900"
|
||||
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-stone-400">Leave thresholds blank to disable alerts for this headgate.</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Leave thresholds blank to disable alerts for this headgate.</p>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={editSaving} className="flex-1 rounded-xl bg-green-600 py-3 text-sm font-bold text-white disabled:opacity-50">
|
||||
{editSaving ? "..." : "Save Changes"}
|
||||
</button>
|
||||
<button type="button" onClick={() => setEditHg(null)} className="rounded-xl border border-stone-300 px-4 py-3 text-sm text-stone-600">Cancel</button>
|
||||
<AdminButton type="submit" disabled={editSaving} isLoading={editSaving} fullWidth>
|
||||
Save Changes
|
||||
</AdminButton>
|
||||
<AdminButton type="button" variant="secondary" onClick={() => setEditHg(null)}>
|
||||
Cancel
|
||||
</AdminButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -454,21 +460,25 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
||||
<div className="bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
|
||||
<div>
|
||||
<p className="font-bold text-zinc-100">{hg.name}</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">QR Label Preview</p>
|
||||
<p className="font-bold text-[var(--admin-text-primary)]">{hg.name}</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">QR Label Preview</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-zinc-400 text-xl leading-none">✕</button>
|
||||
<button onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Simplified label preview ── */}
|
||||
<div className="flex justify-center bg-zinc-900 py-6 px-4">
|
||||
<div className="flex justify-center bg-[var(--admin-bg-subtle)] py-6 px-4">
|
||||
{/* Mini version of the new label design */}
|
||||
<div className="w-40 border-2 border-black rounded-lg p-3 text-center bg-zinc-900 shadow-black/20">
|
||||
<div className="w-40 border-2 border-black rounded-lg p-3 text-center bg-white shadow-sm">
|
||||
<div className="text-xl font-black text-black leading-tight tracking-tight">{hg.name}</div>
|
||||
<div className="text-[9px] font-bold text-gray-500 uppercase tracking-widest mt-0.5 mb-3">{code}</div>
|
||||
<div className="flex justify-center mb-2">
|
||||
@@ -478,17 +488,17 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
||||
className="w-28 h-28 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[5.5px] text-gray-300 leading-tight break-all">{waterUrl}</div>
|
||||
<div className="text-[5.5px] text-gray-500 leading-tight break-all">{waterUrl}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab toggle */}
|
||||
<div className="flex border-b border-slate-100">
|
||||
<div className="flex border-b border-[var(--admin-border)]">
|
||||
{(["preview", "print", "download"] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
className={`flex-1 py-3 text-sm font-semibold transition-colors ${tab === t ? "text-zinc-100 border-b-2 border-slate-900" : "text-slate-400"}`}
|
||||
className={`flex-1 py-3 text-sm font-semibold transition-colors ${tab === t ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-accent)]" : "text-[var(--admin-text-muted)]"}`}
|
||||
>
|
||||
{t === "preview" ? "Preview" : t === "print" ? "🖨️ Print" : "💾 Download"}
|
||||
</button>
|
||||
@@ -497,38 +507,39 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
||||
|
||||
<div className="p-5 space-y-3">
|
||||
{tab === "preview" ? (
|
||||
<div className="space-y-2 text-sm text-zinc-400">
|
||||
<div className="space-y-2 text-sm text-[var(--admin-text-secondary)]">
|
||||
<p>This label is optimized for physical signs and thermal printing:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-xs text-zinc-500">
|
||||
<ul className="list-disc list-inside space-y-1 text-xs text-[var(--admin-text-muted)]">
|
||||
<li><strong>Large name</strong> (24pt bold) — scannable from distance</li>
|
||||
<li><strong>Short code</strong> (e.g. <code className="font-mono bg-zinc-950 px-1 rounded">{code}</code>) — field reference</li>
|
||||
<li><strong>Short code</strong> (e.g. <code className="font-mono bg-[var(--admin-bg-subtle)] px-1 rounded">{code}</code>) — field reference</li>
|
||||
<li><strong>High-contrast QR</strong> — error correction H for outdoor use</li>
|
||||
<li><strong>Tiny URL</strong> at bottom — reference only</li>
|
||||
</ul>
|
||||
<button onClick={() => setTab("print")} className="w-full rounded-xl bg-slate-900 py-3 text-base font-bold text-white hover:bg-slate-800 transition-colors mt-2">
|
||||
<AdminButton onClick={() => setTab("print")} fullWidth>
|
||||
🖨️ Print This Label
|
||||
</button>
|
||||
</AdminButton>
|
||||
</div>
|
||||
) : tab === "print" ? (
|
||||
<button onClick={handlePrintLabel} disabled={loading} className="w-full rounded-xl bg-slate-900 py-3 text-base font-bold text-white disabled:opacity-50">
|
||||
{loading ? "..." : "🖨️ Print Label"}
|
||||
</button>
|
||||
<AdminButton onClick={handlePrintLabel} disabled={loading} isLoading={loading} fullWidth>
|
||||
🖨️ Print Label
|
||||
</AdminButton>
|
||||
) : (
|
||||
<button onClick={handleDownload} disabled={loading} className="w-full rounded-xl bg-blue-600 py-3 text-base font-bold text-white disabled:opacity-50">
|
||||
{loading ? "..." : "💾 Download PNG"}
|
||||
</button>
|
||||
<AdminButton onClick={handleDownload} disabled={loading} isLoading={loading} fullWidth>
|
||||
💾 Download PNG
|
||||
</AdminButton>
|
||||
)}
|
||||
|
||||
<button
|
||||
<AdminButton
|
||||
onClick={() => { onRegenerate(hg); onClose(); }}
|
||||
className="w-full rounded-xl border border-zinc-800 py-2.5 text-sm font-medium text-zinc-400 hover:bg-zinc-800"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
>
|
||||
🔄 Regenerate Token
|
||||
</button>
|
||||
</AdminButton>
|
||||
|
||||
<div className="rounded-lg bg-zinc-900 p-3">
|
||||
<p className="text-xs text-zinc-500 mb-1">Token</p>
|
||||
<code className="text-xs text-zinc-300 font-mono break-all">{hg.headgate_token}</code>
|
||||
<div className="rounded-lg border border-[var(--admin-border)] p-3">
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-1">Token</p>
|
||||
<code className="text-xs text-[var(--admin-text-secondary)] font-mono break-all">{hg.headgate_token}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import WaterLogAdminPanel from "@/components/admin/WaterLogAdminPanel";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getWaterIrrigators, getWaterHeadgatesAdmin, getWaterEntries } from "@/actions/water-log/admin";
|
||||
import { redirect } from "next/navigation";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
|
||||
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||
|
||||
@@ -29,12 +30,21 @@ export default async function AdminWaterLogPage() {
|
||||
]);
|
||||
|
||||
return (
|
||||
<WaterLogAdminPanel
|
||||
initialUsers={users}
|
||||
initialHeadgates={headgates}
|
||||
initialEntries={entries}
|
||||
brandId={brandId}
|
||||
canManage={isAuthorized}
|
||||
/>
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<PageHeader
|
||||
title="Water Log"
|
||||
subtitle="PIN-based access · Tuxedo only · separate from site admin"
|
||||
className="px-6 pt-6"
|
||||
/>
|
||||
<div className="px-6 pb-8">
|
||||
<WaterLogAdminPanel
|
||||
initialUsers={users}
|
||||
initialHeadgates={headgates}
|
||||
initialEntries={entries}
|
||||
brandId={brandId}
|
||||
canManage={isAuthorized}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user