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:
@@ -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">×</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">×</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user