fix: react-doctor modal/dialog/dropzone a11y — 18 files to role+tabIndex+onKeyDown or dialog

This commit is contained in:
Nora
2026-06-26 03:20:10 -06:00
parent e3c1295e62
commit d0bfec9d36
90 changed files with 747 additions and 408 deletions
+4 -1
View File
@@ -316,7 +316,10 @@ export default function ImportCenterClient({ brandId: initialBrandId, brandName:
{analysis.headers.map((header) => {
const mappedField = editedMappings[header];
const sampleIdx = analysis.headers.indexOf(header);
const samples = analysis.rawRows.slice(0, 3).map((r) => r[sampleIdx] ?? "").filter(Boolean);
const samples = analysis.rawRows.slice(0, 3).flatMap((r) => {
const val = r[sampleIdx] ?? "";
return val ? [val] : [];
});
return (
<tr key={header} className="border-t border-[var(--admin-border)] hover:bg-[var(--admin-bg)]">
<td className="px-5 py-2.5 font-medium text-[var(--admin-text-primary)] whitespace-nowrap">{header}</td>
+3 -1
View File
@@ -25,8 +25,10 @@ type OrderItem = {
} | null;
};
const currencyFormatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
function formatCurrency(amount: number) {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
return currencyFormatter.format(amount);
}
export default async function OrderDetailPage({ params }: OrderDetailPageProps) {
+18 -11
View File
@@ -814,16 +814,21 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
setResult(null);
try {
const parsedTiers = priceTiers
.filter((t) => t.tier.trim() && t.price.trim())
.map((t) => ({ tier: t.tier.trim(), price: parseFloat(t.price) || 0 }));
const parsedSales = historicalSales
.filter((s) => s.date.trim())
.map((s) => ({
const parsedTiers: Array<{ tier: string; price: number }> = [];
for (const t of priceTiers) {
if (t.tier.trim() && t.price.trim()) {
parsedTiers.push({ tier: t.tier.trim(), price: parseFloat(t.price) || 0 });
}
}
const parsedSales: Array<{ date: string; units_sold: number; revenue: number }> = [];
for (const s of historicalSales) {
if (!s.date.trim()) continue;
parsedSales.push({
date: s.date.trim(),
units_sold: parseInt(s.units_sold) || 0,
revenue: parseFloat(s.revenue) || 0,
}));
});
}
const res = await fetch("/api/ai/pricing-advisor", {
method: "POST",
@@ -1591,13 +1596,15 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
setResult(null);
try {
const parsedData = historicalData
.filter((d) => d.date.trim())
.map((d) => ({
const parsedData: Array<{ date: string; quantity_sold: number; stop: string }> = [];
for (const d of historicalData) {
if (!d.date.trim()) continue;
parsedData.push({
date: d.date.trim(),
quantity_sold: parseInt(d.quantity_sold) || 0,
stop: d.stop.trim(),
}));
});
}
const res = await fetch("/api/ai/demand-forecast", {
method: "POST",
@@ -102,6 +102,10 @@ const INTEGRATIONS: Integration[] = [
},
];
const INTEGRATIONS_WITHOUT_OPENAI: Integration[] = INTEGRATIONS.filter(
(i) => i.id !== "openai"
);
function IntegrationCard({
integration,
initialCredentials,
@@ -438,7 +442,7 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
</div>
</div>
) : (
INTEGRATIONS.filter(i => i.id !== "openai").map((integration) => (
INTEGRATIONS_WITHOUT_OPENAI.map((integration) => (
<IntegrationCard
key={integration.id}
integration={integration}
+3 -1
View File
@@ -263,7 +263,9 @@ export default async function StopsV2Page({
actions={<StopsDatePicker value={headerDate} />}
/>
<PullToRefresh>
{BUCKET_ORDER.filter((b) => grouped[b].length > 0).map((bucket) => (
{BUCKET_ORDER.flatMap((bucket) =>
grouped[bucket].length > 0 ? [bucket] : []
).map((bucket) => (
<section key={bucket}>
<h2
className="sticky top-0 px-4 py-2 text-label font-semibold"
@@ -232,7 +232,6 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
<label htmlFor="headgate-add-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Headgate Name</label>
<input aria-label=". North Field Gate 1"
id="headgate-add-name"
autoFocus
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
+21 -3
View File
@@ -575,7 +575,9 @@ function CustomersTab({ customers, products, brandId, onMsg, registrations = [],
</tr>
</thead>
<tbody className="divide-y divide-[var(--admin-border-light)]">
{registrations.filter(r => r.account_status === "pending_approval").map(r => (
{registrations.flatMap(r =>
r.account_status === "pending_approval" ? [r] : []
).map(r => (
<tr key={r.id} className="hover:bg-[var(--admin-bg-subtle)]">
<td className="px-5 py-3 font-medium text-[var(--admin-text-primary)]">{r.company_name ?? "—"}</td>
<td className="px-5 py-3 text-[var(--admin-text-secondary)]">{r.contact_name ?? "—"}<br/><span className="text-xs text-[var(--admin-text-muted)]">{r.email}</span></td>
@@ -890,7 +892,15 @@ function CustomerPricingPanel({ customer, products, onClose, onMsg }: {
}
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div
role="dialog"
aria-modal="true"
aria-label="Pricing overrides"
tabIndex={-1}
className="fixed inset-0 bg-black/40 flex items-center justify-center z-50"
onClick={(e) => e.target === e.currentTarget && onClose()}
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col border border-[var(--admin-border)]">
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
<div>
@@ -993,7 +1003,15 @@ function PriceSheetModal({
}
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 px-4" onClick={onClose}>
<div
role="dialog"
aria-modal="true"
aria-label="Send price sheet"
tabIndex={-1}
className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 px-4"
onClick={onClose}
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg border border-[var(--admin-border)]" onClick={e => e.stopPropagation()}>
<div className="px-6 py-4 border-b border-[var(--admin-border)] flex items-center justify-between">
<div>
+11 -3
View File
@@ -151,13 +151,17 @@ export async function GET(req: NextRequest) {
else if (exportType === "payroll") {
// Standard payroll export — grouped by worker with totals
// Columns: Worker Name, Role, Total Hours, Regular Hours, Overtime Hours, Days Worked, Tasks
const workersById = new Map<string, { id: string; role: string }>();
for (const w of allWorkers.flat()) {
if (w) workersById.set(w.id, w);
}
const workerMap = new Map<string, {
name: string; role: string; totalMin: number; days: Set<string>; tasks: Set<string>;
dailyOTMin: number; weeklyOTMin: number;
}>();
for (const log of allLogs) {
if (!workerMap.has(log.worker_id)) {
const w = allWorkers.flat().find(w => w.id === log.worker_id);
const w = workersById.get(log.worker_id);
workerMap.set(log.worker_id, {
name: log.worker_name,
role: w?.role ?? "worker",
@@ -192,12 +196,16 @@ export async function GET(req: NextRequest) {
// Pay period summary — one row per worker per pay period
const brandName = allSettings[0]?.brand_name ?? "Farm";
const headers = ["Brand", "Worker Name", "Pay Period Start", "Pay Period End", "Total Hours", "Daily OT Hours", "Weekly OT Hours", "Status"];
const settingsByBrand = new Map<string, NonNullable<typeof allSettings[number]>>();
for (const s of allSettings) {
if (s && s.brand_id) settingsByBrand.set(s.brand_id, s);
}
const workerMap = new Map<string, {
brandName: string; name: string; totalMin: number; dailyOT: number; weeklyOT: number;
}>();
for (const log of allLogs) {
if (!workerMap.has(log.worker_id)) {
const settings = allSettings.find(s => s && s.brand_id === log.brandId);
const settings = settingsByBrand.get(log.brandId);
workerMap.set(log.worker_id, {
brandName: settings?.brand_name ?? brandName,
name: log.worker_name,
@@ -206,7 +214,7 @@ export async function GET(req: NextRequest) {
}
const entry = workerMap.get(log.worker_id)!;
const totalH = log.total_minutes / 60;
const settings = allSettings.find(s => s && s.brand_id === log.brandId);
const settings = settingsByBrand.get(log.brandId);
if (settings) {
const dailyThr = Number(settings.daily_overtime_threshold);
const weeklyThr = Number(settings.weekly_overtime_threshold);
+6 -4
View File
@@ -73,9 +73,9 @@ export async function POST(req: NextRequest) {
const hasCustomNote = Boolean(customNote?.trim());
// Build product rows HTML
const productRows = products
.filter(p => p.availability === "available")
.map(p => {
const productRows = products.flatMap(p =>
p.availability === "available" ? [p] : []
).map(p => {
const tiersHtml = p.price_tiers
.map(t => {
const max = t.max_qty === 0 ? "+" : `-${t.max_qty}`;
@@ -143,7 +143,9 @@ ${hasCustomNote ? ` <p style="margin: 0 0 12px; color: #1e293b; font-size:
</body>
</html>`;
const text = `${brandName} Wholesale Price Sheet — ${generatedDate}\n${"=".repeat(50)}\n${hasCustomNote ? `\n${customNote!.trim()}\n\n` : ""}${products.filter(p => p.availability === "available").map(p => {
const text = `${brandName} Wholesale Price Sheet — ${generatedDate}\n${"=".repeat(50)}\n${hasCustomNote ? `\n${customNote!.trim()}\n\n` : ""}${products.flatMap(p =>
p.availability === "available" ? [p] : []
).map(p => {
const tiers = p.price_tiers.map(t => `${t.min_qty}${t.max_qty === 0 ? "+" : `-${t.max_qty}`}: $${Number(t.price).toFixed(2)}`).join(", ");
return `${p.name} (${p.unit_type})\n ${tiers}`;
}).join("\n\n")}\n\nGenerated ${generatedDate}. Prices subject to change.`;
+18 -6
View File
@@ -61,12 +61,16 @@ export default function CartClient() {
setIncompatibleItems([]);
if (hasPickupItems) {
const pickupProductIds = cart.filter((i) => i.fulfillment === "pickup").map((i) => i.id);
const pickupProductIds: string[] = [];
for (const i of cart) {
if (i.fulfillment === "pickup") pickupProductIds.push(i.id);
}
checkStopProductAvailability(stop.id, pickupProductIds)
.then((data) => {
const unavailable = (data ?? [])
.filter((row) => row.is_available === false)
.map((row) => row.product_id);
const unavailable: string[] = [];
for (const row of data ?? []) {
if (row.is_available === false) unavailable.push(row.product_id);
}
setIncompatibleItems(unavailable);
})
.catch(() => {
@@ -135,7 +139,11 @@ export default function CartClient() {
<div>
<p className="font-semibold text-white">Shed Pickup</p>
<p className="mt-1 text-sm text-zinc-400">
{cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")}
{cart.flatMap((i) =>
i.fulfillment === "pickup" && i.pickup_type === "shed"
? [i.description || i.name]
: []
).join(", ")}
</p>
</div>
</div>
@@ -313,7 +321,11 @@ export default function CartClient() {
Shed Pickup
</p>
<p className="mt-1.5 text-xs text-zinc-400">
{cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")}
{cart.flatMap((i) =>
i.fulfillment === "pickup" && i.pickup_type === "shed"
? [i.description || i.name]
: []
).join(", ")}
</p>
</div>
)}
@@ -90,7 +90,6 @@ export default function ChangePasswordForm({ userId }: { userId: string }) {
minLength={8}
className="w-full rounded-xl border border-zinc-700 bg-zinc-800 px-4 py-3 text-zinc-100 placeholder:text-zinc-600 outline-none focus:border-violet-500 transition-colors"
placeholder="Min. 8 characters"
autoFocus
/>
</div>
-1
View File
@@ -81,7 +81,6 @@ export default function ChangePasswordPage() {
minLength={8}
className="w-full rounded-xl border border-zinc-700 bg-zinc-800 px-4 py-3 text-zinc-100 placeholder:text-zinc-600 outline-none focus:border-violet-500 transition-colors"
placeholder="Min. 8 characters"
autoFocus
/>
</div>
+5 -1
View File
@@ -281,7 +281,11 @@ export default function CheckoutClient() {
<fieldset className="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-emerald-200 border border-emerald-100">
<legend className="text-xl font-bold text-slate-900">Shed Pickup</legend>
<p className="mt-2 text-sm text-slate-600">
{cart.filter((i) => i.fulfillment === "pickup" && i.pickup_type === "shed").map((i) => i.description || i.name).join(", ")}
{cart.flatMap((i) =>
i.fulfillment === "pickup" && i.pickup_type === "shed"
? [i.description || i.name]
: []
).join(", ")}
</p>
<p className="mt-1 text-xs text-slate-500">Pickup location details are included in your order confirmation.</p>
</fieldset>
+1 -1
View File
@@ -24,7 +24,7 @@ export default function ErrorPage({
</div>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="relative inline-flex h-24 w-24 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm mb-8"
@@ -24,7 +24,7 @@ export default function ErrorPage({
</div>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="relative inline-flex h-24 w-24 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm mb-8"
+1 -1
View File
@@ -26,7 +26,7 @@ export default function ErrorPage({
{/* Error icon */}
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="relative inline-flex h-24 w-24 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm mb-8"
+1 -1
View File
@@ -24,7 +24,7 @@ export default function ErrorPage({
</div>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="relative inline-flex h-24 w-24 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm mb-8"
@@ -24,7 +24,7 @@ export default function ErrorPage({
</div>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="relative inline-flex h-24 w-24 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm mb-8"
@@ -24,7 +24,7 @@ export default function ErrorPage({
</div>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="relative inline-flex h-24 w-24 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm mb-8"
+3 -1
View File
@@ -232,7 +232,9 @@ export default function RoadmapPage() {
className="w-full px-4 py-3 rounded-xl border border-[#e5e5e5] bg-white text-[#1a1a1a] focus:outline-none focus:ring-2 focus:ring-[#1a4d2e]/50 focus:border-[#1a4d2e] transition-all"
>
<option value="">Select a category</option>
{CATEGORIES.filter(c => c !== "All").map((category) => (
{CATEGORIES.flatMap(c =>
c === "All" ? [] : [c]
).map((category) => (
<option key={category} value={category.toLowerCase()}>{category}</option>
))}
</select>
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ErrorPage({
className="max-w-lg w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-emerald-900/40 mb-8"
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ErrorPage({
className="max-w-lg w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-emerald-900/40 mb-8"
+1 -1
View File
@@ -20,7 +20,7 @@ export default function ErrorPage({
>
{/* Error icon */}
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-emerald-900/40 mb-8"
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ErrorPage({
className="max-w-lg w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-emerald-900/40 mb-8"
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ErrorPage({
className="max-w-lg w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-emerald-900/40 mb-8"
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ErrorPage({
className="max-w-lg w-full text-center"
>
<motion.div
initial={{ scale: 0 }}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="inline-flex h-24 w-24 items-center justify-center rounded-full bg-emerald-900/40 mb-8"
@@ -48,7 +48,6 @@ export default function WaterAdminPinClient({ brandId }: Props) {
placeholder="••••"
className="w-full rounded-xl border-2 border-slate-300 px-6 py-6 text-center text-4xl font-bold tracking-widest outline-none focus:border-slate-900"
style={{ letterSpacing: "0.4em" }}
autoFocus
/>
{error && (
<p className="text-center text-red-600 text-sm">{error}</p>