fix: react-doctor label-has-associated-control (-15, nested-interactive -1)

Add htmlFor/id pairs to label/input pairs across ~24 files.
Convert section-header labels (Role/Permissions/Visibility/Environment/
Send via/Quick messages/Campaign Type/When to Send/Preview) to <p>.
Convert clickable divs to buttons (AbandonedCartDashboard → native dialog).
Hoist regex patterns out of loops in ai-import.ts.
This commit is contained in:
Nora
2026-06-26 04:02:10 -06:00
parent f6bf91951e
commit 16a6756ad1
30 changed files with 220 additions and 192 deletions
+11 -8
View File
@@ -259,10 +259,12 @@ function fallbackParse(
};
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const keywordPatterns: Record<string, RegExp> = {};
for (const [etype, kws] of Object.entries(keywordMaps)) {
keywordPatterns[etype] = new RegExp(kws.map(escapeRegex).join("|"));
}
const keywordPatterns: Record<string, RegExp> = {
products: new RegExp(productKeywords.map(escapeRegex).join("|")),
orders: new RegExp(orderKeywords.map(escapeRegex).join("|")),
contacts: new RegExp(contactKeywords.map(escapeRegex).join("|")),
stops: new RegExp(stopKeywords.map(escapeRegex).join("|")),
};
for (const header of h) {
for (const [etype, pattern] of Object.entries(keywordPatterns)) {
@@ -314,10 +316,11 @@ function fallbackParse(
notes: ["notes", "note", "special_instructions", "comments", "memo", "instruction"],
};
const semanticPatterns: Record<string, RegExp> = {};
for (const [field, kws] of Object.entries(semanticMap)) {
semanticPatterns[field] = new RegExp(kws.map(escapeRegex).join("|"));
}
const buildPattern = (kws: readonly string[]) =>
new RegExp(kws.map(escapeRegex).join("|"));
const semanticPatterns: Record<string, RegExp> = Object.fromEntries(
Object.entries(semanticMap).map(([field, kws]) => [field, buildPattern(kws)]),
);
for (let i = 0; i < h.length; i++) {
const header = h[i];
+7 -7
View File
@@ -897,12 +897,12 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
{/* Price Tiers */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Price Tiers</label>
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-price-tiers">Price Tiers</label>
<button type="button" onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Tier</button>
</div>
{priceTiers.map((tier, i) => (
<div key={`tier-${i}-${tier.tier}-${tier.price}`} className="flex items-center gap-2">
<input aria-label="., Wholesale"
<input id="fld-price-tiers" aria-label="., Wholesale"
type="text"
value={tier.tier}
onChange={(e) => updateTier(i, "tier", e.target.value)}
@@ -931,7 +931,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
{/* Historical Sales */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Historical Sales</label>
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-historical-sales">Historical Sales</label>
<button type="button" onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
@@ -943,7 +943,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
</div>
{historicalSales.map((sale, i) => (
<div key={`sale-${i}-${sale.date}`} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input aria-label="2026 04 01"
<input id="fld-historical-sales" aria-label="2026 04 01"
type="text"
value={sale.date}
onChange={(e) => updateSale(i, "date", e.target.value)}
@@ -1405,7 +1405,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Stops</label>
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-stops">Stops</label>
<button type="button"
onClick={addStop}
className="text-xs flex items-center gap-1 transition-colors"
@@ -1637,7 +1637,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
{/* Historical Data */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Historical Sales</label>
<label className="block text-sm font-medium" style={labelStyle} htmlFor="fld-historical-sales-2">Historical Sales</label>
<button type="button" onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
@@ -1649,7 +1649,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
</div>
{historicalData.map((row, i) => (
<div key={`hist-${i}-${row.date}-${row.stop}`} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input aria-label="2026 04 01"
<input id="fld-historical-sales-2" aria-label="2026 04 01"
type="text"
value={row.date}
onChange={(e) => updateRow(i, "date", e.target.value)}
@@ -308,15 +308,16 @@ function IntegrationCard({
<p className="text-xs text-stone-400 mt-0.5">{integration.description}</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<div className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
aria-label={`Enable ${integration.name}`}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-zinc-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-4 rtl:peer-checked:after:-translate-x-4 peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-zinc-900 after:border-zinc-600 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-600"></div>
</label>
</div>
</div>
{/* Sync options */}
@@ -393,7 +394,7 @@ function IntegrationCard({
{/* Environment toggle */}
<div className="mb-5">
<label className="block text-xs font-medium text-zinc-400 mb-1">Environment</label>
<p className="block text-xs font-medium text-zinc-400 mb-1">Environment</p>
<div className="flex gap-2">
{["sandbox", "production"].map((e) => (
<button type="button"
+5 -5
View File
@@ -1583,7 +1583,7 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: {
className="rounded-lg border border-[var(--admin-border)] px-3 py-1.5 text-sm outline-none focus:border-[var(--admin-accent)]" />
</div>
<div className="flex-1 min-w-[180px]">
<label className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1">Search</label>
<label className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-search">Search</label>
<AdminSearchInput
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
@@ -1653,7 +1653,7 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: {
<thead className="bg-[var(--admin-bg-subtle)] text-[var(--admin-text-muted)]">
<tr>
<th className="px-5 py-3 font-semibold text-left w-10">
<input aria-label="Checkbox" type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAll} className="rounded" />
<input id="fld-search" aria-label="Checkbox" type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAll} className="rounded" />
</th>
<th className="px-5 py-3 font-semibold text-left">Invoice</th>
<th className="px-5 py-3 font-semibold text-left">Customer</th>
@@ -2112,7 +2112,7 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
{/* Team Notification Recipients */}
<div className="col-span-2 rounded-2xl bg-[var(--admin-bg-subtle)] p-5 ring-1 ring-[var(--admin-border)]">
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1">Team Notification Recipients</label>
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1" htmlFor="fld-team-notification-recipients">Team Notification Recipients</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-4 leading-relaxed">
These team members receive all wholesale notifications for this brand new orders,
deposits, fulfillments, price sheets, and pickup reminders. If no recipients are
@@ -2131,7 +2131,7 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
<div className="space-y-2 mb-4">
{form.notificationRecipients.map((r: NotificationRecipient, idx: number) => (
<div key={r.email || `recipient-${idx}`} className={`flex items-center gap-2 rounded-xl px-3 py-2.5 bg-white ring-1 ${r.active ? "ring-[var(--admin-border)]" : "ring-[var(--admin-border-light)] opacity-70"}`}>
<input aria-label="Checkbox"
<input id="fld-team-notification-recipients" aria-label="Checkbox"
type="checkbox" checked={r.active} onChange={() => toggleRecipient(idx)}
className="rounded border-[var(--admin-border)] mt-0.5" title={r.active ? "Active — receives notifications" : "Inactive"} />
<div className="flex-1 min-w-0">
@@ -2162,7 +2162,7 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
</div>
{/* Webhook Settings */}
<div className="col-span-2 rounded-2xl bg-[var(--admin-bg-subtle)] p-5 ring-1 ring-[var(--admin-border)]">
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1">Webhook Integration</label>
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1" htmlFor="fld-webhook-integration">Webhook Integration</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-4 leading-relaxed">
Send order events to external systems (Harvest Point, ERPs, etc.). Payload is signed
with HMAC-SHA256. Enable and configure the URL and secret below.
@@ -697,11 +697,11 @@ export default function WholesalePortalClient({
<div className="rounded-2xl bg-white shadow-sm ring-1 ring-slate-200 p-5">
<div className="flex flex-col sm:flex-row gap-4 sm:items-end">
<div className="flex-1">
<label className="block text-sm font-semibold text-slate-700 mb-1.5">
<label className="block text-sm font-semibold text-slate-700 mb-1.5" htmlFor="fld-preferred-pickup-date-optional">
Preferred Pickup Date
<span className="text-slate-400 font-normal ml-1">(optional)</span>
</label>
<input aria-label="Date"
<input id="fld-preferred-pickup-date-optional" aria-label="Date"
type="date"
value={pickupDate}
onChange={e => setPickupDate(e.target.value)}
+86 -63
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useEffect, useRef } from "react";
import { getAbandonedCarts, manuallyCloseAbandonedCart, resendAbandonedCartEmail, type AbandonedCart } from "@/actions/email-automation/abandoned-cart";
type Props = { brandId: string };
@@ -190,68 +190,91 @@ export default function AbandonedCartDashboard({ brandId }: Props) {
)}
{/* Cart detail modal */}
{cart && (
<button
type="button"
aria-label="Close abandoned cart details"
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm border-0 cursor-default"
onClick={() => setSelectedCart(null)}
>
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-lg shadow-xl"
onClick={e => e.stopPropagation()}>
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
<div>
<h3 className="text-lg font-bold text-[var(--admin-text-primary)]">Abandoned Cart</h3>
<p className="text-xs text-[var(--admin-text-muted)]">{cart.contact_email}</p>
</div>
<button type="button" onClick={() => setSelectedCart(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none">&times;</button>
</div>
<div className="p-6 space-y-4">
<div className="flex items-start gap-3 bg-stone-50 rounded-xl p-3">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ${STATUS_LABELS[cart.status]?.color ?? ""}`}>
{STATUS_LABELS[cart.status]?.en ?? cart.status}
</span>
<div className="flex-1">
<p className="text-sm text-[var(--admin-text-primary)] font-medium">{cart.contact_name ?? "—"}</p>
<p className="text-xs text-[var(--admin-text-muted)]">{cart.contact_email}</p>
<p className="text-xs text-[var(--admin-text-muted)] mt-1">Step: {STEP_LABELS[cart.sequence_step] ?? cart.sequence_step}</p>
</div>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">Cart Items</p>
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-[var(--admin-text-muted)] border-b border-[var(--admin-border)]">
<th className="text-left pb-2 font-medium">Item</th>
<th className="text-right pb-2 font-medium">Qty</th>
<th className="text-right pb-2 font-medium">Price</th>
</tr>
</thead>
<tbody>
{cart.cart_snapshot.items.map((item, i) => (
<CartItemRow key={`${item.name}-${item.unit_price}-${item.quantity}-${i}`} item={item} />
))}
</tbody>
<tfoot>
<tr className="border-t border-[var(--admin-border)]">
<td className="pt-2 text-[var(--admin-text-primary)] font-semibold" colSpan={2}>Total</td>
<td className="pt-2 text-[var(--admin-text-primary)] font-bold text-right">${Number(cart.cart_snapshot.subtotal).toFixed(2)}</td>
</tr>
</tfoot>
</table>
</div>
{cart.last_email_sent_at && (
<p className="text-xs text-[var(--admin-text-muted)]">Last email sent: {new Date(cart.last_email_sent_at).toLocaleString()}</p>
)}
{cart.next_email_at && (
<p className="text-xs text-[var(--admin-text-muted)]">Next email: {new Date(cart.next_email_at).toLocaleString()}</p>
)}
</div>
</div>
</button>
)}
<CartDetailModal cart={cart} onClose={() => setSelectedCart(null)} />
</div>
);
}
function CartDetailModal({ cart, onClose }: { cart: AbandonedCart | null; onClose: () => void }) {
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (cart && !dialog.open) dialog.showModal();
if (!cart && dialog.open) dialog.close();
const onCancel = (e: Event) => {
e.preventDefault();
onClose();
};
dialog.addEventListener("cancel", onCancel);
return () => {
dialog.removeEventListener("cancel", onCancel);
};
}, [cart, onClose]);
if (!cart) return null;
return (
<dialog
ref={dialogRef}
aria-label="Abandoned cart details"
className="w-full max-w-full max-h-full m-0 p-0 bg-transparent"
style={{ backgroundColor: "transparent" }}
>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ backgroundColor: "rgba(60, 56, 37, 0.5)" }}>
<div className="bg-white border border-[var(--admin-border)] rounded-2xl w-full max-w-lg shadow-xl">
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
<div>
<h3 className="text-lg font-bold text-[var(--admin-text-primary)]">Abandoned Cart</h3>
<p className="text-xs text-[var(--admin-text-muted)]">{cart.contact_email}</p>
</div>
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-xl leading-none" aria-label="Close abandoned cart details">&times;</button>
</div>
<div className="p-6 space-y-4">
<div className="flex items-start gap-3 bg-stone-50 rounded-xl p-3">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold ${STATUS_LABELS[cart.status]?.color ?? ""}`}>
{STATUS_LABELS[cart.status]?.en ?? cart.status}
</span>
<div className="flex-1">
<p className="text-sm text-[var(--admin-text-primary)] font-medium">{cart.contact_name ?? "—"}</p>
<p className="text-xs text-[var(--admin-text-muted)]">{cart.contact_email}</p>
<p className="text-xs text-[var(--admin-text-muted)] mt-1">Step: {STEP_LABELS[cart.sequence_step] ?? cart.sequence_step}</p>
</div>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">Cart Items</p>
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-[var(--admin-text-muted)] border-b border-[var(--admin-border)]">
<th className="text-left pb-2 font-medium">Item</th>
<th className="text-right pb-2 font-medium">Qty</th>
<th className="text-right pb-2 font-medium">Price</th>
</tr>
</thead>
<tbody>
{cart.cart_snapshot.items.map((item, i) => (
<CartItemRow key={`${item.name}-${item.unit_price}-${item.quantity}-${i}`} item={item} />
))}
</tbody>
<tfoot>
<tr className="border-t border-[var(--admin-border)]">
<td className="pt-2 text-[var(--admin-text-primary)] font-semibold" colSpan={2}>Total</td>
<td className="pt-2 text-[var(--admin-text-primary)] font-bold text-right">${Number(cart.cart_snapshot.subtotal).toFixed(2)}</td>
</tr>
</tfoot>
</table>
</div>
{cart.last_email_sent_at && (
<p className="text-xs text-[var(--admin-text-muted)]">Last email sent: {new Date(cart.last_email_sent_at).toLocaleString()}</p>
)}
{cart.next_email_at && (
<p className="text-xs text-[var(--admin-text-muted)]">Next email: {new Date(cart.next_email_at).toLocaleString()}</p>
)}
</div>
</div>
</div>
</dialog>
);
}
+2 -2
View File
@@ -318,9 +318,9 @@ export default function AddStopModal({ isOpen, onClose, brandId, duplicateFrom,
{/* Row 5 — Status: segmented control (compact, replaces two giant buttons) */}
<div className="ha-field pt-0.5">
<div className="flex items-center justify-between">
<label className="ha-field-label">
<p className="ha-field-label">
<span>Visibility</span>
</label>
</p>
<span className="text-[10px] text-[var(--admin-text-muted)] leading-none">
Draft is hidden from customers
</span>
+4 -4
View File
@@ -481,7 +481,7 @@ export default function AdminOrdersPanel({
key={stop.id}
className="flex items-center gap-3 rounded-lg px-3 py-2 cursor-pointer transition-colors"
style={{ color: "var(--admin-text-primary)" }}
>
htmlFor="fld-togglestopstopid-classnameh-4-w-4-rounde">
<input
type="checkbox"
checked={selectedStops.includes(stop.id)}
@@ -637,7 +637,7 @@ export default function AdminOrdersPanel({
<thead style={{ backgroundColor: "var(--admin-bg)" }}>
<tr style={{ borderBottom: "1px solid var(--admin-border)" }}>
<th className="w-10 px-4 py-3">
<input aria-label="Checkbox"
<input id="fld-togglestopstopid-classnameh-4-w-4-rounde" aria-label="Checkbox"
type="checkbox"
checked={selectedOrders.size === paginatedOrders.length && paginatedOrders.length > 0}
onChange={toggleSelectAll}
@@ -945,7 +945,7 @@ export default function AdminOrdersPanel({
<label
className="block text-xs font-semibold"
style={{ color: "var(--admin-text-secondary)" }}
>
htmlFor="fld-items">
Items
</label>
<span
@@ -1006,7 +1006,7 @@ export default function AdminOrdersPanel({
{prod?.name ?? item.product_id}
</div>
<div className="col-span-2">
<input aria-label="Number"
<input id="fld-items" aria-label="Number"
type="number"
min={1}
value={item.quantity}
+2 -2
View File
@@ -374,7 +374,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
{/* Role */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-2">Role</label>
<p className="block text-sm font-medium text-[var(--admin-text-primary)] mb-2">Role</p>
<div className="space-y-2">
{availableRoles.map((r) => (
<label key={r} className="flex items-center gap-3 rounded-lg border border-[var(--admin-border)] px-3 py-2.5 cursor-pointer hover:bg-[var(--admin-bg)] transition-colors">
@@ -417,7 +417,7 @@ export default function CreateUserModal({ isOpen, onClose, onSuccess, brands, cu
{/* Permissions */}
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-2">Permissions</label>
<p className="block text-sm font-medium text-[var(--admin-text-primary)] mb-2">Permissions</p>
<div className="space-y-2">
{ALL_FLAGS.map((flag) => (
<label key={flag} className="flex items-center justify-between rounded-lg border border-[var(--admin-border)] px-3 sm:px-4 py-2 sm:py-2.5 hover:bg-[var(--admin-bg)] cursor-pointer transition-colors">
+2 -2
View File
@@ -357,9 +357,9 @@ export default function EditStopModal({ isOpen, onClose, brandId, stop, onSucces
{/* Row 5 — Status: segmented control */}
<div className="ha-field pt-0.5">
<div className="flex items-center justify-between">
<label className="ha-field-label">
<p className="ha-field-label">
<span>Visibility</span>
</label>
</p>
<span className="text-[10px] text-[var(--admin-text-muted)] leading-none">
Draft is hidden from customers
</span>
@@ -544,7 +544,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{/* Preview */}
<div>
<label className="block text-sm font-semibold text-stone-700 mb-1.5">Preview</label>
<p className="block text-sm font-semibold text-stone-700 mb-1.5">Preview</p>
<EmailPreview subject={subject} body={bodyText} />
</div>
</div>
@@ -583,10 +583,10 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{/* Campaign settings */}
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-campaign-name" className="block text-sm font-semibold text-stone-700 mb-1.5">
Campaign Name <span className="text-red-500">*</span>
</label>
<input aria-label=". May Sweet Corn Promotion"
<input id="fld-campaign-name" aria-label=". May Sweet Corn Promotion"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
@@ -721,7 +721,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Campaign type selection */}
<div>
<label className="block text-sm font-semibold text-stone-700 mb-3">Campaign Type</label>
<p className="block text-sm font-semibold text-stone-700 mb-3">Campaign Type</p>
<div className="space-y-3">
{CAMPAIGN_TYPES.map((ct) => (
<button type="button"
@@ -751,7 +751,7 @@ export default function CampaignComposerPage({ brandId, campaigns, templates, se
{/* Send timing */}
<div>
<label className="block text-sm font-semibold text-stone-700 mb-3">When to Send</label>
<p className="block text-sm font-semibold text-stone-700 mb-3">When to Send</p>
<div className="space-y-3">
<label className={`flex items-center gap-4 p-4 rounded-xl border-2 cursor-pointer transition-all ${
sendNow ? "border-emerald-500 bg-emerald-50" : "border-stone-200 hover:border-stone-300"
@@ -87,10 +87,10 @@ export default function SegmentEditModal({ initialName = "", initialDescription
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">
<label className="block text-sm font-medium text-[var(--admin-text-primary)] mb-1.5" htmlFor="fld-description-optional">
Description <span className="text-[var(--admin-text-muted)] font-normal">(optional)</span>
</label>
<textarea aria-label=". Customers Who Pick Up At The Fort Pierce Stop Regularly"
<textarea id="fld-description-optional" aria-label=". Customers Who Pick Up At The Fort Pierce Stop Regularly"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="e.g. Customers who pick up at the Fort Pierce stop regularly"
@@ -161,7 +161,7 @@ export default function MessageCustomersSection({
<>
{/* Audience selector */}
<div>
<label className="mb-2 block text-sm font-medium text-zinc-300">
<label className="mb-2 block text-sm font-medium text-zinc-300" htmlFor="fld-audience---recipientslength-with-contact">
Audience {" "}
<span className="text-zinc-500">
{recipients.length} with contact info
@@ -194,7 +194,7 @@ export default function MessageCustomersSection({
{/* Channel selector */}
<div>
<label className="mb-2 block text-sm font-medium text-zinc-300">
<label className="mb-2 block text-sm font-medium text-zinc-300" htmlFor="fld-channel">
Channel
</label>
<div className="flex gap-2">
+14 -14
View File
@@ -127,10 +127,10 @@ export default function NewStopForm({ duplicateFrom }: Props) {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-city" className="block text-xs font-semibold text-stone-700 mb-1.5">
City <span className="text-red-500">*</span>
</label>
<input aria-label=". Denver"
<input id="fld-city" aria-label=". Denver"
type="text"
value={city}
onChange={(e) => {
@@ -152,10 +152,10 @@ export default function NewStopForm({ duplicateFrom }: Props) {
</div>
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-state" className="block text-xs font-semibold text-stone-700 mb-1.5">
State <span className="text-red-500">*</span>
</label>
<input aria-label=". CO"
<input id="fld-state" aria-label=". CO"
type="text"
value={state}
onChange={(e) => {
@@ -178,10 +178,10 @@ export default function NewStopForm({ duplicateFrom }: Props) {
</div>
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-location-name" className="block text-xs font-semibold text-stone-700 mb-1.5">
Location Name <span className="text-red-500">*</span>
</label>
<input aria-label=". Southwest Plaza Parking Lot"
<input id="fld-location-name" aria-label=". Southwest Plaza Parking Lot"
type="text"
value={location}
onChange={(e) => {
@@ -204,10 +204,10 @@ export default function NewStopForm({ duplicateFrom }: Props) {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-date" className="block text-xs font-semibold text-stone-700 mb-1.5">
Date <span className="text-red-500">*</span>
</label>
<input aria-label="Date"
<input id="fld-date" aria-label="Date"
type="date"
value={date}
onChange={(e) => {
@@ -228,10 +228,10 @@ export default function NewStopForm({ duplicateFrom }: Props) {
</div>
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-time" className="block text-xs font-semibold text-stone-700 mb-1.5">
Time <span className="text-red-500">*</span>
</label>
<input aria-label=". 8:00 AM 2:00 PM"
<input id="fld-time" aria-label=". 8:00 AM 2:00 PM"
type="text"
value={time}
onChange={(e) => {
@@ -254,10 +254,10 @@ export default function NewStopForm({ duplicateFrom }: Props) {
</div>
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label htmlFor="fld-brand" className="block text-xs font-semibold text-stone-700 mb-1.5">
Brand <span className="text-red-500">*</span>
</label>
<select aria-label="Select"
<select id="fld-brand" aria-label="Select"
value={brandId}
onChange={(e) => {
setBrandId(e.target.value);
@@ -318,9 +318,9 @@ export default function NewStopForm({ duplicateFrom }: Props) {
</div>
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">Order Cutoff</label>
<label htmlFor="fld-cutoff" className="block text-xs font-semibold text-stone-700 mb-1.5">Order Cutoff</label>
<p className="text-[10px] text-stone-500 mb-2">Customers must order before this time to be included at this stop.</p>
<input aria-label="Datetime Local"
<input id="fld-cutoff" aria-label="Datetime Local"
type="datetime-local"
value={cutoffTime}
onChange={(e) => setCutoffTime(e.target.value)}
+2 -2
View File
@@ -277,10 +277,10 @@ export default function OrderPaymentSection({
)}
<form onSubmit={handleRefund} className="space-y-3">
<div>
<label className="mb-1 block text-xs font-semibold text-stone-500">Amount</label>
<label className="mb-1 block text-xs font-semibold text-stone-500" htmlFor="fld-amount">Amount</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400 text-sm">$</span>
<input aria-label="Number"
<input id="fld-amount" aria-label="Number"
type="number"
step="0.01"
min="0.01"
+3 -3
View File
@@ -273,7 +273,7 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
</AdminInput>
<div>
<label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]">Status</label>
<label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]" htmlFor="fld-status">Status</label><label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]">Status</label>
<button type="button"
onClick={() => setActive((v) => !v)}
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
@@ -287,7 +287,7 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
</div>
<div>
<label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]">Taxable</label>
<label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]" htmlFor="fld-taxable">Taxable</label><label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]">Taxable</label>
<button type="button"
onClick={() => setIs_taxable((v) => !v)}
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors flex items-center gap-3 ${
@@ -317,7 +317,7 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
</AdminInput>
<div>
<label className="mb-1 block text-sm font-medium text-[var(--admin-text-primary)]">Product Image</label>
<label className="mb-1 block text-sm font-medium text-[var(--admin-text-primary)]" htmlFor="fld-product-image">Product Image</label><label className="mb-1 block text-sm font-medium text-[var(--admin-text-primary)]">Product Image</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-2">JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)</p>
<div
+10 -10
View File
@@ -331,27 +331,27 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
</select>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2">Pay period length</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2" htmlFor="fld-pay-period-length">Pay period length</label>
<div className="flex items-center gap-3">
<input aria-label="Number" type="number" min={1} max={31} value={payPeriodLength}
<input id="fld-pay-period-length" aria-label="Number" type="number" min={1} max={31} value={payPeriodLength}
onChange={e => setPayPeriodLength(Number(e.target.value))}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" />
<span className="text-sm text-stone-500">days</span>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2">Daily overtime threshold</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2" htmlFor="fld-daily-overtime-threshold">Daily overtime threshold</label>
<div className="flex items-center gap-3">
<input aria-label="Number" type="number" min={1} max={24} value={dailyOvertimeThreshold}
<input id="fld-daily-overtime-threshold" aria-label="Number" type="number" min={1} max={24} value={dailyOvertimeThreshold}
onChange={e => setDailyOvertimeThreshold(Number(e.target.value))}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" />
<span className="text-sm text-stone-500">hrs</span>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2">Weekly overtime threshold</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2" htmlFor="fld-weekly-overtime-threshold">Weekly overtime threshold</label>
<div className="flex items-center gap-3">
<input aria-label="Number" type="number" min={1} max={80} value={weeklyOvertimeThreshold}
<input id="fld-weekly-overtime-threshold" aria-label="Number" type="number" min={1} max={80} value={weeklyOvertimeThreshold}
onChange={e => setWeeklyOvertimeThreshold(Number(e.target.value))}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 focus:outline-none focus:border-emerald-500 transition-colors" />
<span className="text-sm text-stone-500">hrs</span>
@@ -405,9 +405,9 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
<h3 className="text-sm font-semibold text-stone-800 mb-4">Notification Recipients</h3>
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2">Email addresses</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2" htmlFor="fld-email-addresses">Email addresses</label>
<div className="flex gap-2">
<input aria-label="Manager@farm.com" value={newEmail}
<input id="fld-email-addresses" aria-label="Manager@farm.com" value={newEmail}
onChange={e => setNewEmail(e.target.value)}
onKeyDown={e => e.key === "Enter" && (addEmail(), e.preventDefault())}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500"
@@ -428,9 +428,9 @@ export default function SettingsSections({ brandId, workersOnly, tasksOnly }: Pr
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2">SMS numbers</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-stone-500 mb-2" htmlFor="fld-sms-numbers">SMS numbers</label>
<div className="flex gap-2">
<input aria-label="+1234567890" value={newSms}
<input id="fld-sms-numbers" aria-label="+1234567890" value={newSms}
onChange={e => setNewSms(e.target.value)}
onKeyDown={e => e.key === "Enter" && (addSms(), e.preventDefault())}
className="flex-1 px-4 py-3 rounded-xl border border-stone-200 bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-emerald-500"
+7 -7
View File
@@ -188,10 +188,10 @@ export default function StopEditForm({ stop, brands, onSaved }: StopEditFormProp
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label className="block text-xs font-semibold text-stone-700 mb-1.5" htmlFor="fld-city-">
City <span className="text-red-500">*</span>
</label>
<input aria-label="City Name"
<input id="fld-city-" aria-label="City Name"
type="text"
value={city}
onChange={(e) => {
@@ -213,10 +213,10 @@ export default function StopEditForm({ stop, brands, onSaved }: StopEditFormProp
</div>
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label className="block text-xs font-semibold text-stone-700 mb-1.5" htmlFor="fld-state-">
State <span className="text-red-500">*</span>
</label>
<input aria-label="State Code"
<input id="fld-state-" aria-label="State Code"
type="text"
value={state}
onChange={(e) => {
@@ -240,10 +240,10 @@ export default function StopEditForm({ stop, brands, onSaved }: StopEditFormProp
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-stone-700 mb-1.5">
<label className="block text-xs font-semibold text-stone-700 mb-1.5" htmlFor="fld-date-">
Date <span className="text-red-500">*</span>
</label>
<input aria-label="Date"
<input id="fld-date-" aria-label="Date"
type="date"
value={date}
onChange={(e) => {
@@ -332,7 +332,7 @@ export default function StopEditForm({ stop, brands, onSaved }: StopEditFormProp
</AdminInput>
<div>
<label className="mb-2 block text-sm font-medium text-stone-700">Status</label>
<label className="mb-2 block text-sm font-medium text-stone-700" htmlFor="fld-status">Status</label><label className="mb-2 block text-sm font-medium text-stone-700">Status</label>
<button
type="button"
onClick={() => setActive((v) => !v)}
+6 -6
View File
@@ -121,9 +121,9 @@ export default function StopMessagingForm({
{/* Channel */}
<div>
<label className="ha-field-label mb-2">
<p className="ha-field-label mb-2">
<span>Send via</span>
</label>
</p>
<div className="flex gap-2">
{(["sms", "email", "both"] as const).map((ch) => (
<button type="button"
@@ -143,9 +143,9 @@ export default function StopMessagingForm({
{/* Quick messages */}
<div>
<label className="ha-field-label mb-2">
<p className="ha-field-label mb-2">
<span>Quick messages</span>
</label>
</p>
<div className="flex flex-wrap gap-2">
{quickMessages.map((qm) => (
<button type="button"
@@ -165,10 +165,10 @@ export default function StopMessagingForm({
{/* Message preview */}
<div>
<label className="ha-field-label mb-2">
<label htmlFor="fld-stop-message" className="ha-field-label mb-2">
<span>Message</span>
</label>
<textarea aria-label="Type Your Message..."
<textarea id="fld-stop-message" aria-label="Type Your Message..."
value={message}
onChange={(e) => {
setMessage(e.target.value);
+3 -3
View File
@@ -515,7 +515,7 @@ export function TemplateEditForm({
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-4 sm:p-6 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-xs sm:text-sm font-semibold text-[var(--admin-text-muted)] uppercase tracking-wide">Message Body</h3>
<label className="flex items-center gap-2 text-xs sm:text-sm">
<label className="flex items-center gap-2 text-xs sm:text-sm" htmlFor="fld-sethtmlmodeetargetchecked-classnameround">
<input
type="checkbox"
checked={htmlMode}
@@ -555,7 +555,7 @@ export function TemplateEditForm({
/>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Plain Text Fallback *</label>
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5" htmlFor="fld-plain-text-fallback-">Plain Text Fallback *</label>
<textarea aria-label="Plain Text Version..."
id="body-textarea"
value={bodyText}
@@ -568,7 +568,7 @@ export function TemplateEditForm({
</>
) : (
<div>
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5">Body (Plain Text) *</label>
<label className="block text-xs sm:text-sm font-medium text-[var(--admin-text-primary)] mb-1.5" htmlFor="fld-body-plain-text-">Body (Plain Text) *</label>
<textarea aria-label="Message Body With Variables Like {{first Name}}..."
id="body-textarea"
value={bodyText}
@@ -535,27 +535,27 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
</select>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">Pay period length</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2" htmlFor="fld-pay-period-length">Pay period length</label>
<div className="flex items-center gap-3">
<input aria-label="Number" type="number" min={1} max={31} value={payPeriodLength}
<input id="fld-pay-period-length" aria-label="Number" type="number" min={1} max={31} value={payPeriodLength}
onChange={e => setPayPeriodLength(Number(e.target.value))}
className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" />
<span className="text-sm text-[var(--admin-text-muted)]">days</span>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">Daily overtime threshold</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2" htmlFor="fld-daily-overtime-threshold">Daily overtime threshold</label>
<div className="flex items-center gap-3">
<input aria-label="Number" type="number" min={1} max={24} value={dailyOvertimeThreshold}
<input id="fld-daily-overtime-threshold" aria-label="Number" type="number" min={1} max={24} value={dailyOvertimeThreshold}
onChange={e => setDailyOvertimeThreshold(Number(e.target.value))}
className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" />
<span className="text-sm text-[var(--admin-text-muted)]">hrs</span>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">Weekly overtime threshold</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2" htmlFor="fld-weekly-overtime-threshold">Weekly overtime threshold</label>
<div className="flex items-center gap-3">
<input aria-label="Number" type="number" min={1} max={80} value={weeklyOvertimeThreshold}
<input id="fld-weekly-overtime-threshold" aria-label="Number" type="number" min={1} max={80} value={weeklyOvertimeThreshold}
onChange={e => setWeeklyOvertimeThreshold(Number(e.target.value))}
className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors" />
<span className="text-sm text-[var(--admin-text-muted)]">hrs</span>
@@ -616,9 +616,9 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
<div className="bg-white border border-[var(--admin-border)] rounded-xl p-6 space-y-4">
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Notification Recipients</h3>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">Email addresses</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2" htmlFor="fld-email-addresses">Email addresses</label>
<div className="flex gap-2">
<input aria-label="Manager@farm.com" value={newEmail}
<input id="fld-email-addresses" aria-label="Manager@farm.com" value={newEmail}
onChange={e => setNewEmail(e.target.value)}
onKeyDown={e => e.key === "Enter" && (addEmail(), e.preventDefault())}
className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors"
@@ -635,9 +635,9 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2">SMS numbers</label>
<label className="block text-xs font-semibold uppercase tracking-widest text-[var(--admin-text-muted)] mb-2" htmlFor="fld-sms-numbers">SMS numbers</label>
<div className="flex gap-2">
<input aria-label="+1234567890" value={newSms}
<input id="fld-sms-numbers" aria-label="+1234567890" value={newSms}
onChange={e => setNewSms(e.target.value)}
onKeyDown={e => e.key === "Enter" && (addSms(), e.preventDefault())}
className="flex-1 px-4 py-3 rounded-xl border border-[var(--admin-border)] bg-white text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:outline-none focus:border-[var(--admin-accent)] transition-colors"
+8 -8
View File
@@ -561,9 +561,9 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
{/* Password (create only) */}
{editing.isNew && (
<div>
<label className="block text-sm font-medium text-stone-700">Password</label>
<label htmlFor="fld-password" className="block text-sm font-medium text-stone-700">Password</label>
<p className="mt-1 text-xs text-stone-500 mb-1.5">Minimum 6 characters.</p>
<input aria-label="Choose A Password"
<input id="fld-password" aria-label="Choose A Password"
type="password"
value={editing.password ?? ""}
onChange={(e) => setEditing((p) => ({ ...p, password: e.target.value }))}
@@ -576,9 +576,9 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
{/* Display Name */}
<div>
<label className="block text-sm font-medium text-stone-700">Display Name</label>
<label htmlFor="fld-display-name" className="block text-sm font-medium text-stone-700">Display Name</label>
<p className="mt-1 text-xs text-stone-500 mb-1.5">Shown in the admin user list.</p>
<input aria-label="Kyle Martinez"
<input id="fld-display-name" aria-label="Kyle Martinez"
type="text"
value={editing.display_name ?? ""}
onChange={(e) => setEditing((p) => ({ ...p, display_name: e.target.value }))}
@@ -589,9 +589,9 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
{/* Phone Number */}
<div>
<label className="block text-sm font-medium text-stone-700">Phone Number</label>
<label htmlFor="fld-phone" className="block text-sm font-medium text-stone-700">Phone Number</label>
<p className="mt-1 text-xs text-stone-500 mb-1.5">Optional.</p>
<input aria-label="+1 (555) 000 0000"
<input id="fld-phone" aria-label="+1 (555) 000 0000"
type="tel"
value={editing.phone_number ?? ""}
onChange={(e) => setEditing((p) => ({ ...p, phone_number: e.target.value }))}
@@ -602,7 +602,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
{/* Role */}
<div>
<label className="block text-sm font-medium text-stone-700">Role</label>
<p className="block text-sm font-medium text-stone-700">Role</p>
<div className="mt-2 space-y-2">
{availableRoles.map((r) => (
<label key={r} className="flex items-center gap-3 rounded-lg border border-stone-200 px-3 py-2.5 cursor-pointer hover:bg-stone-50">
@@ -662,7 +662,7 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
{/* Permissions */}
<div>
<label className="block text-sm font-medium text-stone-700 mb-3">Permissions</label>
<p className="block text-sm font-medium text-stone-700 mb-3">Permissions</p>
<div className="space-y-2">
{ALL_FLAGS.map((flag) => (
<label key={flag} className="flex items-center justify-between rounded-lg border border-stone-200 px-4 py-2.5 hover:bg-stone-50 cursor-pointer">
@@ -80,11 +80,11 @@ export default function WaterLogEntryEditForm({ entry, backHref = "/admin/water-
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Headgate</label>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-headgate">Headgate</label><label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Headgate</label>
<p className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] px-3 py-2 text-sm text-[var(--admin-text-secondary)]">{entry.headgate_name}</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">User</label>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-user">User</label><label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">User</label>
<p className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] px-3 py-2 text-sm text-[var(--admin-text-secondary)]">{entry.user_name}</p>
</div>
</div>
@@ -106,11 +106,11 @@ export default function WaterLogEntryEditForm({ entry, backHref = "/admin/water-
/>
</AdminInput>
<div>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Submitted Via</label>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-submitted-via">Submitted Via</label><label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Submitted Via</label>
<p className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] px-3 py-2 text-sm text-[var(--admin-text-secondary)]">{entry.submitted_via}</p>
</div>
<div>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Logged</label>
<label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-logged">Logged</label><label className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Logged</label>
<p className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] px-3 py-2 text-sm text-[var(--admin-text-secondary)]">
{new Date(entry.logged_at).toLocaleDateString("en-US", {
month: "short",
@@ -199,12 +199,13 @@ export default function StopsLocations({ stops }: Props) {
className="w-full rounded-lg border border-[var(--admin-border)] bg-white pl-9 pr-3 py-2 text-sm text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:border-[var(--admin-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/15"
/>
</div>
<label className="flex items-center gap-2 cursor-pointer select-none">
<div className="flex items-center gap-2 cursor-pointer select-none">
<button
type="button"
role="switch"
aria-checked={showOnlyActive}
onClick={() => setShowOnlyActive((v) => !v)}
aria-label="Show only active locations"
className={`relative h-5 w-9 rounded-full transition-colors ${showOnlyActive ? "bg-[var(--admin-accent)]" : "bg-stone-300"}`}
>
<span
@@ -214,7 +215,7 @@ export default function StopsLocations({ stops }: Props) {
<span className="font-mono text-[10px] uppercase tracking-wider text-[var(--admin-text-secondary)]">
Active only
</span>
</label>
</div>
<span className="ml-auto font-mono text-[10px] uppercase tracking-wider text-[var(--admin-text-muted)]">
{filtered.length} of {groups.length} locations
</span>
+2 -2
View File
@@ -184,9 +184,9 @@ export default function LotCreateForm({ brandId }: { brandId: string }) {
/>
</div>
<div>
<label className="block text-xs font-medium text-stone-600 mb-1.5">Yield Est.</label>
<label className="block text-xs font-medium text-stone-600 mb-1.5" htmlFor="fld-yield-est">Yield Est.</label>
<div className="flex rounded-xl border border-stone-200 bg-stone-50 overflow-hidden focus-within:border-emerald-600 focus-within:bg-white transition-colors">
<input aria-label=". 5000"
<input id="fld-yield-est" aria-label=". 5000"
type="number"
value={yield_estimate_lbs}
onChange={(e) => setYieldEstimateLbs(e.target.value)}
@@ -277,9 +277,9 @@ export default function LotCreateModal({ isOpen, onClose, brandId }: Props) {
/>
</div>
<div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }}>Yield Est.</label>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--admin-text-primary)" }} htmlFor="fld-yield-est">Yield Est.</label>
<div className="flex rounded-xl border overflow-hidden transition-colors" style={{ borderColor: "var(--admin-border)", backgroundColor: "var(--admin-bg-subtle)" }}>
<input aria-label=". 5000"
<input id="fld-yield-est" aria-label=". 5000"
type="number"
value={yield_estimate_lbs}
onChange={(e) => setYieldEstimateLbs(e.target.value)}
@@ -79,10 +79,10 @@ export default function QuickNewLotModal({ brandId, onCreated, onClose }: Props)
)}
<div>
<label className="block text-sm font-medium text-stone-700 mb-1.5">
<label className="block text-sm font-medium text-stone-700 mb-1.5" htmlFor="fld-crop-type-">
Crop Type <span className="text-red-500">*</span>
</label>
<input aria-label=". Sweet Corn"
<input id="fld-crop-type-" aria-label=". Sweet Corn"
type="text"
value={crop_type}
onChange={(e) => setCropType(e.target.value)}
+5 -5
View File
@@ -551,7 +551,7 @@ function WaterFieldInner() {
{/* Headgate selector */}
<div>
<label className="block text-base font-semibold text-stone-700 mb-2">
<label className="block text-base font-semibold text-stone-700 mb-2" htmlFor="fld-tselectheadgate">
{t.selectHeadgate}
</label>
{isHeadgateLocked ? (
@@ -575,7 +575,7 @@ function WaterFieldInner() {
<span className="text-stone-500 text-base font-medium">Loading headgates...</span>
</div>
) : (
<select aria-label="Select"
<select id="fld-tselectheadgate" aria-label="Select"
value={effectiveSelectedHeadgate}
onChange={(e) => setSelectedHeadgate(e.target.value)}
required
@@ -629,7 +629,7 @@ function WaterFieldInner() {
{/* GPS Capture */}
<div>
<label className="block text-base font-semibold text-stone-700 mb-2">
<label className="block text-base font-semibold text-stone-700 mb-2" htmlFor="fld-gps-location">
GPS Location
</label>
<button
@@ -658,7 +658,7 @@ function WaterFieldInner() {
{/* Photo Upload */}
<div>
<label className="block text-base font-semibold text-stone-700 mb-2">
<label className="block text-base font-semibold text-stone-700 mb-2" htmlFor="fld-tphotolabel">
{t.photoLabel}
</label>
{photoPreview ? (
@@ -673,7 +673,7 @@ function WaterFieldInner() {
</button>
</div>
) : (
<label className="flex items-center justify-center w-full rounded-xl border-2 border-dashed border-stone-300 cursor-pointer py-5 text-base font-semibold text-stone-500 hover:border-stone-500 hover:text-stone-700 transition-colors min-h-[80px]">
<label className="flex items-center justify-center w-full rounded-xl border-2 border-dashed border-stone-300 cursor-pointer py-5 text-base font-semibold text-stone-500 hover:border-stone-500 hover:text-stone-700 transition-colors min-h-[80px]" htmlFor="fld--taddphoto">
<span>📷 {t.addPhoto}</span>
<input
type="file"
+2 -2
View File
@@ -231,7 +231,7 @@ export default function OrdersTab({ orders, customers, brandId, onMsg, onRefresh
className="rounded-lg border border-[var(--admin-border)] px-3 py-1.5 text-sm outline-none focus:border-[var(--admin-accent)]" />
</div>
<div className="flex-1 min-w-[180px]">
<label className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1">Search</label>
<label className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-search">Search</label>
<AdminSearchInput
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
@@ -293,7 +293,7 @@ export default function OrdersTab({ orders, customers, brandId, onMsg, onRefresh
<thead className="bg-[var(--admin-bg-subtle)] text-[var(--admin-text-muted)]">
<tr>
<th className="px-5 py-3 font-semibold text-left w-10">
<input type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAll} className="rounded" />
<input id="fld-search" type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAll} className="rounded" />
</th>
<th className="px-5 py-3 font-semibold text-left">Invoice</th>
<th className="px-5 py-3 font-semibold text-left">Customer</th>
@@ -217,7 +217,7 @@ export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canMa
{/* Team Notification Recipients */}
<div className="col-span-2 rounded-2xl bg-[var(--admin-bg-subtle)] p-5 ring-1 ring-[var(--admin-border)]">
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1">Team Notification Recipients</label>
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1" htmlFor="fld-team-notification-recipients">Team Notification Recipients</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-4 leading-relaxed">
These team members receive all wholesale notifications for this brand new orders,
deposits, fulfillments, price sheets, and pickup reminders. If no recipients are
@@ -236,7 +236,7 @@ export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canMa
<div className="space-y-2 mb-4">
{form.notificationRecipients.map((r: NotificationRecipient, idx: number) => (
<div key={r.email} className={`flex items-center gap-2 rounded-xl px-3 py-2.5 bg-white ring-1 ${r.active ? "ring-[var(--admin-border)]" : "ring-[var(--admin-border-light)] opacity-70"}`}>
<input
<input id="fld-team-notification-recipients"
type="checkbox" checked={r.active} onChange={() => toggleRecipient(idx)}
className="rounded border-[var(--admin-border)] mt-0.5" title={r.active ? "Active — receives notifications" : "Inactive"} />
<div className="flex-1 min-w-0">
@@ -267,7 +267,7 @@ export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canMa
</div>
{/* Webhook Settings */}
<div className="col-span-2 rounded-2xl bg-[var(--admin-bg-subtle)] p-5 ring-1 ring-[var(--admin-border)]">
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1">Webhook Integration</label>
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1" htmlFor="fld-webhook-integration">Webhook Integration</label><label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1">Webhook Integration</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-4 leading-relaxed">
Send order events to external systems (Harvest Point, ERPs, etc.). Payload is signed
with HMAC-SHA256. Enable and configure the URL and secret below.