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
+99 -39
View File
@@ -266,7 +266,15 @@ function fallbackParse(
}
}
const detectedType = (Object.entries(typeScores).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown") as ImportEntityType;
let bestEtype: string | undefined;
let bestScore = -1;
for (const [etype, score] of Object.entries(typeScores)) {
if (score > bestScore) {
bestScore = score;
bestEtype = etype;
}
}
const detectedType = (bestEtype ?? "unknown") as ImportEntityType;
// Map headers to semantic fields
const columnMappings: ColumnMapping = {};
@@ -335,14 +343,26 @@ function fallbackParse(
// ── Import Executors ─────────────────────────────────────────────────────────
async function executeProductsImport(brandId: string, rows: Record<string, unknown>[]) {
const products = rows.map((r) => ({
name: String(r.product_name ?? r.name ?? ""),
description: String(r.description ?? ""),
price: parseFloat(String(r.price ?? "0").replace(/[^0-9.]/g, "")) || 0,
type: normalizeFulfillmentType(String(r.product_type ?? r.type ?? "Pickup")),
active: String(r.active ?? "true").toLowerCase() !== "false",
image_url: r.image_url ? String(r.image_url) : undefined,
})).filter((p) => p.name !== "");
const products: Array<{
name: string;
description: string;
price: number;
type: string;
active: boolean;
image_url: string | undefined;
}> = [];
for (const r of rows) {
const name = String(r.product_name ?? r.name ?? "");
if (name === "") continue;
products.push({
name,
description: String(r.description ?? ""),
price: parseFloat(String(r.price ?? "0").replace(/[^0-9.]/g, "")) || 0,
type: normalizeFulfillmentType(String(r.product_type ?? r.type ?? "Pickup")),
active: String(r.active ?? "true").toLowerCase() !== "false",
image_url: r.image_url ? String(r.image_url) : undefined,
});
}
const result = await importProductsBatch(brandId, products);
if (!result.success) {
@@ -380,15 +400,24 @@ async function executeOrdersImport(brandId: string, rows: Record<string, unknown
}
}
const orders = Object.values(orderMap)
.filter((o) => o.customer_email && o.stop_id)
.map((o) => ({
customer_name: o.customer_name || "",
customer_email: o.customer_email,
customer_phone: o.customer_phone || "",
stop_id: o.stop_id,
items: o.items,
}));
const orders: Array<{
customer_name: string;
customer_email: string;
customer_phone: string;
stop_id: string;
items: { product_id: string; quantity: number; fulfillment: string }[];
}> = [];
for (const o of Object.values(orderMap)) {
if (o.customer_email && o.stop_id) {
orders.push({
customer_name: o.customer_name || "",
customer_email: o.customer_email,
customer_phone: o.customer_phone || "",
stop_id: o.stop_id,
items: o.items,
});
}
}
return importOrdersBatch(brandId, orders).then((r) => {
if (r.success) {
@@ -403,16 +432,31 @@ async function executeOrdersImport(brandId: string, rows: Record<string, unknown
}
async function executeStopsImport(brandId: string, rows: Record<string, unknown>[]) {
const stops = rows.map((r) => ({
city: String(r.city ?? ""),
state: String(r.state ?? ""),
location: String(r.location ?? r.address ?? ""),
date: normalizeDate(String(r.date ?? "")),
time: String(r.time ?? ""),
address: r.address ? String(r.address) : undefined,
zip: r.zip ? String(r.zip) : undefined,
notes: r.notes ? String(r.notes) : undefined,
})).filter((s) => s.city !== "" && s.state !== "");
const stops: Array<{
city: string;
state: string;
location: string;
date: string;
time: string;
address: string | undefined;
zip: string | undefined;
notes: string | undefined;
}> = [];
for (const r of rows) {
const city = String(r.city ?? "");
const state = String(r.state ?? "");
if (city === "" || state === "") continue;
stops.push({
city,
state,
location: String(r.location ?? r.address ?? ""),
date: normalizeDate(String(r.date ?? "")),
time: String(r.time ?? ""),
address: r.address ? String(r.address) : undefined,
zip: r.zip ? String(r.zip) : undefined,
notes: r.notes ? String(r.notes) : undefined,
});
}
return createStopsBatch(brandId, stops).then((r) => {
if (r.success) {
@@ -423,17 +467,33 @@ async function executeStopsImport(brandId: string, rows: Record<string, unknown>
}
async function executeContactsImport(brandId: string, rows: Record<string, unknown>[]) {
const contacts = rows.map((r) => ({
email: r.email ? String(r.email).toLowerCase().trim() : undefined,
phone: r.phone ? String(r.phone).trim() : undefined,
first_name: r.first_name ? String(r.first_name).trim() : undefined,
last_name: r.last_name ? String(r.last_name).trim() : undefined,
full_name: r.full_name ? String(r.full_name).trim() : undefined,
tags: r.tags ? String(r.tags).split(",").map((t: string) => t.trim()) : [],
email_opt_in: r.email_opt_in !== undefined ? String(r.email_opt_in).toLowerCase() === "true" : true,
sms_opt_in: r.sms_opt_in !== undefined ? String(r.sms_opt_in).toLowerCase() === "true" : false,
external_id: r.external_id ? String(r.external_id).trim() : undefined,
})).filter((c) => c.email || c.phone);
const contacts: Array<{
email: string | undefined;
phone: string | undefined;
first_name: string | undefined;
last_name: string | undefined;
full_name: string | undefined;
tags: string[];
email_opt_in: boolean;
sms_opt_in: boolean;
external_id: string | undefined;
}> = [];
for (const r of rows) {
const email = r.email ? String(r.email).toLowerCase().trim() : undefined;
const phone = r.phone ? String(r.phone).trim() : undefined;
if (!email && !phone) continue;
contacts.push({
email,
phone,
first_name: r.first_name ? String(r.first_name).trim() : undefined,
last_name: r.last_name ? String(r.last_name).trim() : undefined,
full_name: r.full_name ? String(r.full_name).trim() : undefined,
tags: r.tags ? String(r.tags).split(",").map((t: string) => t.trim()) : [],
email_opt_in: r.email_opt_in !== undefined ? String(r.email_opt_in).toLowerCase() === "true" : true,
sms_opt_in: r.sms_opt_in !== undefined ? String(r.sms_opt_in).toLowerCase() === "true" : false,
external_id: r.external_id ? String(r.external_id).trim() : undefined,
});
}
const result = await importContactsBatch({ brandId, contacts });
if (!result.success) {
+12 -4
View File
@@ -198,15 +198,23 @@ await getSession();
LIMIT 10`,
);
const recentOrders = recentRes.rows
.filter((o) => o.status !== "cancelled")
.map((o) => ({
const recentOrders: Array<{
id: string;
customer_name: string;
total: number;
status: string;
created_at: string;
}> = [];
for (const o of recentRes.rows) {
if (o.status === "cancelled") continue;
recentOrders.push({
id: o.id || "",
customer_name: o.customer_name || "Guest",
total: (o.total_cents || 0) / 100,
status: o.status || "unknown",
created_at: formatTimeAgo(o.placed_at),
}));
});
}
return {
todayOrders: todayOrderCount,
+2 -1
View File
@@ -213,6 +213,7 @@ await getSession(); const adminUser = await getAdminUser();
const allowedServices = perishable
? (["FEDEX_OVERNIGHT", "FEDEX_2_DAY_AIR"] as FedExServiceType[])
: ALL_SERVICES;
const allowedServicesSet = new Set<FedExServiceType>(allowedServices);
// Build FedEx rate request. The SaaS rebuild doesn't store the
// recipient's city/state/zip on the order — FedEx rate quotes
@@ -283,7 +284,7 @@ await getSession(); const adminUser = await getAdminUser();
for (const detail of output) {
const serviceType = detail.serviceType as FedExServiceType;
if (!allowedServices.includes(serviceType)) continue;
if (!allowedServicesSet.has(serviceType)) continue;
const dayOption = detail.derivedDetails?.deliveryDayOfWeek ?? "";
const deliveryDate = detail.derivedDetails?.deliveryDate ?? "";
+2 -1
View File
@@ -87,6 +87,7 @@ await getSession(); const adminUser = await getAdminUser();
const errors: string[] = [];
let synced = 0;
const completedStatuses = new Set(["COMPLETED", "APPROVED"]);
// Determine sync start time — last sync or 30 days ago
const since = settings.square_last_sync_at
@@ -105,7 +106,7 @@ await getSession(); const adminUser = await getAdminUser();
// Skip if no order_id
if (!payment.order_id) continue;
// Skip if already completed/canceled
if (!["COMPLETED", "APPROVED"].includes(payment.status ?? "")) continue;
if (!completedStatuses.has(payment.status ?? "")) continue;
const orderData = await fetchSquareOrder(settings.square_access_token, payment.order_id);
const order = orderData.order;
+6 -4
View File
@@ -66,13 +66,15 @@ await getSession(); // 1. If not collecting tax, return zero
const stripe = new Stripe(stripeKey);
// Build Stripe line items for tax calculation — only taxable items
const lineItems: Stripe.Tax.CalculationCreateParams.LineItem[] = params.items
.filter((item) => item.is_taxable !== false)
.map((item) => ({
const lineItems: Stripe.Tax.CalculationCreateParams.LineItem[] = [];
for (const item of params.items) {
if (item.is_taxable === false) continue;
lineItems.push({
amount: Math.round(item.price * item.quantity * 100), // cents
reference: item.id,
tax_behavior: "exclusive",
}));
});
}
// If no taxable items, return zero tax
if (lineItems.length === 0) {