fix: react-doctor modal/dialog/dropzone a11y — 18 files to role+tabIndex+onKeyDown or dialog
This commit is contained in:
+99
-39
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user