fix: react-doctor label-has-associated-control 9→0, async-await-in-loop 3→0 (Promise.allSettled), role-supports-aria-props 3→0 (already done in prior batch)
This commit is contained in:
@@ -340,15 +340,13 @@ await getSession(); const adminUser = await getAdminUser();
|
|||||||
return { success: false, error: "Not authorized for this brand" };
|
return { success: false, error: "Not authorized for this brand" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: ImportResult = { created: 0, updated: 0, skipped: 0, errors: [] };
|
const validRows = params.contacts.filter(
|
||||||
|
(row) => row.email || row.phone,
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of params.contacts) {
|
const settled = await Promise.allSettled(
|
||||||
if (!row.email && !row.phone) {
|
validRows.map((row) =>
|
||||||
result.skipped++;
|
upsertContact({
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const r = await upsertContact({
|
|
||||||
brand_id: params.brandId,
|
brand_id: params.brandId,
|
||||||
email: row.email,
|
email: row.email,
|
||||||
phone: row.phone,
|
phone: row.phone,
|
||||||
@@ -361,16 +359,42 @@ await getSession(); const adminUser = await getAdminUser();
|
|||||||
external_id: row.external_id,
|
external_id: row.external_id,
|
||||||
tags: row.tags,
|
tags: row.tags,
|
||||||
metadata: row._metadata,
|
metadata: row._metadata,
|
||||||
});
|
}).then(
|
||||||
if (r.success) result.created++;
|
(r) => ({ row, r }),
|
||||||
else {
|
(err) => ({ row, err }),
|
||||||
result.errors.push({ row, error: r.error });
|
),
|
||||||
}
|
),
|
||||||
} catch (err) {
|
);
|
||||||
|
|
||||||
|
const result: ImportResult = {
|
||||||
|
created: 0,
|
||||||
|
updated: 0,
|
||||||
|
skipped: params.contacts.length - validRows.length,
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of settled) {
|
||||||
|
if (entry.status === "rejected") {
|
||||||
|
const err = (entry as PromiseRejectedResult).reason;
|
||||||
result.errors.push({
|
result.errors.push({
|
||||||
row,
|
row: validRows[settled.indexOf(entry)],
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = entry.value as
|
||||||
|
| { row: ContactImportEntry; r: { success: boolean; error?: string } }
|
||||||
|
| { row: ContactImportEntry; err: unknown };
|
||||||
|
if ("err" in value) {
|
||||||
|
const err = value.err;
|
||||||
|
result.errors.push({
|
||||||
|
row: value.row,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
} else if (value.r.success) {
|
||||||
|
result.created++;
|
||||||
|
} else {
|
||||||
|
result.errors.push({ row: value.row, error: value.r.error ?? "Unknown error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,29 +40,56 @@ await getSession(); const adminUser = await getAdminUser();
|
|||||||
let created = 0;
|
let created = 0;
|
||||||
const errors: { product: string; error: string }[] = [];
|
const errors: { product: string; error: string }[] = [];
|
||||||
|
|
||||||
|
const validProducts: Array<{ name: string; description: string; price: number; type: string; active: boolean; image_url?: string }> = [];
|
||||||
|
const skipped: { product: { name: string; description: string; price: number; type: string; active: boolean; image_url?: string }; error: string }[] = [];
|
||||||
for (const p of productsToImport) {
|
for (const p of productsToImport) {
|
||||||
const priceCents = Math.round(Number(p.price) * 100);
|
const priceCents = Math.round(Number(p.price) * 100);
|
||||||
if (!Number.isFinite(priceCents) || priceCents < 0) {
|
if (!Number.isFinite(priceCents) || priceCents < 0) {
|
||||||
errors.push({ product: p.name, error: "Invalid price" });
|
skipped.push({ product: p, error: "Invalid price" });
|
||||||
continue;
|
} else {
|
||||||
|
validProducts.push(p);
|
||||||
}
|
}
|
||||||
try {
|
}
|
||||||
await withBrand(brandId, (db) =>
|
|
||||||
|
const settled = await Promise.allSettled(
|
||||||
|
validProducts.map((p) =>
|
||||||
|
withBrand(brandId, (db) =>
|
||||||
db.insert(products).values({
|
db.insert(products).values({
|
||||||
brandId: brandId,
|
brandId: brandId,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
description: p.description ?? null,
|
description: p.description ?? null,
|
||||||
priceCents,
|
priceCents: Math.round(Number(p.price) * 100),
|
||||||
active: p.active,
|
active: p.active,
|
||||||
})
|
}),
|
||||||
);
|
).then(
|
||||||
created++;
|
() => p,
|
||||||
} catch (err) {
|
(err) => ({ p, err }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const entry of settled) {
|
||||||
|
if (entry.status === "rejected") {
|
||||||
|
const err = (entry as PromiseRejectedResult).reason;
|
||||||
errors.push({
|
errors.push({
|
||||||
product: p.name,
|
product: "<unknown>",
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
const value = entry.value;
|
||||||
|
if ("err" in value) {
|
||||||
|
errors.push({
|
||||||
|
product: value.p.name,
|
||||||
|
error: value.err instanceof Error ? value.err.message : String(value.err),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const s of skipped) {
|
||||||
|
errors.push({ product: s.product.name, error: s.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, created, updated: 0, errors };
|
return { success: true, created, updated: 0, errors };
|
||||||
|
|||||||
@@ -101,82 +101,103 @@ await getSession(); const adminUser = await getAdminUser();
|
|||||||
since
|
since
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const payment of paymentsData.payments ?? []) {
|
interface SquarePayment {
|
||||||
|
id: string;
|
||||||
|
order_id?: string;
|
||||||
|
status?: string;
|
||||||
|
amount_money?: { amount?: string; currency?: string };
|
||||||
|
customer_id?: string;
|
||||||
|
receipt_number?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processablePayments = ((paymentsData.payments ?? []) as SquarePayment[]).filter(
|
||||||
|
(p) => Boolean(p.order_id) && completedStatuses.has(p.status ?? ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderResults = await Promise.allSettled(
|
||||||
|
processablePayments.map((p) =>
|
||||||
|
fetchSquareOrder(settings.square_access_token!, p.order_id!).then(
|
||||||
|
(data) => ({ payment: p, orderData: data, err: undefined as unknown }),
|
||||||
|
(err: unknown) => ({ payment: p, orderData: undefined, err }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const result of orderResults) {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
errors.push(`Square order fetch: ${String(result.reason)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { payment, orderData, err } = result.value;
|
||||||
|
if (err || !orderData) {
|
||||||
|
errors.push(`Payment ${payment.id}: ${String(err ?? "no order data")}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const order = orderData.order;
|
||||||
|
|
||||||
|
// Build line items from Square order
|
||||||
|
const lineItems = (order.line_items ?? []).map((li: {
|
||||||
|
name: string;
|
||||||
|
quantity: string;
|
||||||
|
base_price_money: { amount: string; currency: string };
|
||||||
|
}) => ({
|
||||||
|
name: li.name,
|
||||||
|
quantity: parseInt(li.quantity, 10),
|
||||||
|
price: Number(li.base_price_money?.amount ?? 0) / 100,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Compute total
|
||||||
|
const total = Number(order.total_money?.amount ?? payment.amount_money?.amount ?? 0) / 100;
|
||||||
|
|
||||||
|
// Get customer info from payment
|
||||||
|
const customerName = payment.customer_id ?? "Square Customer";
|
||||||
|
const customerEmail = payment.receipt_number ?? "";
|
||||||
|
|
||||||
|
// Use idempotency key to avoid duplicates
|
||||||
|
const idempotencyKey = `square_${payment.id}`;
|
||||||
|
|
||||||
|
// Call SECURITY DEFINER RPC create_order_with_items
|
||||||
|
let createOk = false;
|
||||||
|
let errText = "";
|
||||||
try {
|
try {
|
||||||
// Skip if no order_id
|
const rpcRes = await pool.query(
|
||||||
if (!payment.order_id) continue;
|
`SELECT * FROM create_order_with_items(
|
||||||
// Skip if already completed/canceled
|
$1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10
|
||||||
if (!completedStatuses.has(payment.status ?? "")) continue;
|
)`,
|
||||||
|
[
|
||||||
const orderData = await fetchSquareOrder(settings.square_access_token, payment.order_id);
|
idempotencyKey,
|
||||||
const order = orderData.order;
|
customerName,
|
||||||
|
customerEmail,
|
||||||
// Build line items from Square order
|
"",
|
||||||
const lineItems = (order.line_items ?? []).map((li: {
|
null, // Square orders don't have RC stop_id
|
||||||
name: string;
|
JSON.stringify(
|
||||||
quantity: string;
|
lineItems.map((li: { name: string; quantity: number; price: number }) => ({
|
||||||
base_price_money: { amount: string; currency: string };
|
id: null, // product lookup not available in this flow
|
||||||
}) => ({
|
quantity: li.quantity,
|
||||||
name: li.name,
|
fulfillment: "shipping",
|
||||||
quantity: parseInt(li.quantity, 10),
|
}))
|
||||||
price: Number(li.base_price_money?.amount ?? 0) / 100,
|
),
|
||||||
}));
|
total,
|
||||||
|
"square",
|
||||||
// Compute total
|
"paid",
|
||||||
const total = Number(order.total_money?.amount ?? payment.amount_money?.amount ?? 0) / 100;
|
payment.id,
|
||||||
|
]
|
||||||
// Get customer info from payment
|
);
|
||||||
const customerName = payment.customer_id ?? "Square Customer";
|
createOk = rpcRes.rows.length > 0;
|
||||||
const customerEmail = payment.receipt_number ?? "";
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
// Use idempotency key to avoid duplicates
|
// 409 / unique violation = already exists (idempotent)
|
||||||
const idempotencyKey = `square_${payment.id}`;
|
if (/duplicate key|unique constraint|already exists/i.test(msg)) {
|
||||||
|
createOk = true;
|
||||||
// Call SECURITY DEFINER RPC create_order_with_items
|
|
||||||
let createOk = false;
|
|
||||||
let errText = "";
|
|
||||||
try {
|
|
||||||
const rpcRes = await pool.query(
|
|
||||||
`SELECT * FROM create_order_with_items(
|
|
||||||
$1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10
|
|
||||||
)`,
|
|
||||||
[
|
|
||||||
idempotencyKey,
|
|
||||||
customerName,
|
|
||||||
customerEmail,
|
|
||||||
"",
|
|
||||||
null, // Square orders don't have RC stop_id
|
|
||||||
JSON.stringify(
|
|
||||||
lineItems.map((li: { name: string; quantity: number; price: number }) => ({
|
|
||||||
id: null, // product lookup not available in this flow
|
|
||||||
quantity: li.quantity,
|
|
||||||
fulfillment: "shipping",
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
total,
|
|
||||||
"square",
|
|
||||||
"paid",
|
|
||||||
payment.id,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
createOk = rpcRes.rows.length > 0;
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
// 409 / unique violation = already exists (idempotent)
|
|
||||||
if (/duplicate key|unique constraint|already exists/i.test(msg)) {
|
|
||||||
createOk = true;
|
|
||||||
} else {
|
|
||||||
errText = msg.slice(0, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createOk) {
|
|
||||||
synced++;
|
|
||||||
} else {
|
} else {
|
||||||
errors.push(`Payment ${payment.id}: ${errText}`);
|
errText = msg.slice(0, 100);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
errors.push(`Payment ${payment.id}: ${String(err)}`);
|
|
||||||
|
if (createOk) {
|
||||||
|
synced++;
|
||||||
|
} else {
|
||||||
|
errors.push(`Payment ${payment.id}: ${errText}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -273,32 +273,36 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
|
|||||||
</AdminInput>
|
</AdminInput>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]">
|
||||||
<button type="button"
|
<span className="block mb-2">Status</span>
|
||||||
onClick={() => setActive((v) => !v)}
|
<button type="button"
|
||||||
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
|
onClick={() => setActive((v) => !v)}
|
||||||
active
|
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
|
||||||
? "bg-[var(--admin-primary-soft)] text-[var(--admin-primary)] ring-1 ring-[var(--admin-primary)]"
|
active
|
||||||
: "bg-[var(--admin-bg)] text-[var(--admin-text-muted)] hover:bg-[var(--admin-bg-subtle)]"
|
? "bg-[var(--admin-primary-soft)] text-[var(--admin-primary)] ring-1 ring-[var(--admin-primary)]"
|
||||||
}`}
|
: "bg-[var(--admin-bg)] text-[var(--admin-text-muted)] hover:bg-[var(--admin-bg-subtle)]"
|
||||||
>
|
}`}
|
||||||
{active ? "Active" : "Inactive"}
|
>
|
||||||
</button>
|
{active ? "Active" : "Inactive"}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<label className="mb-2 block text-sm font-medium text-[var(--admin-text-primary)]">
|
||||||
<button type="button"
|
<span className="block mb-2">Taxable</span>
|
||||||
onClick={() => setIs_taxable((v) => !v)}
|
<button type="button"
|
||||||
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors flex items-center gap-3 ${
|
onClick={() => setIs_taxable((v) => !v)}
|
||||||
is_taxable
|
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors flex items-center gap-3 ${
|
||||||
? "bg-[var(--admin-primary-soft)] text-[var(--admin-primary)] ring-1 ring-[var(--admin-primary)]"
|
is_taxable
|
||||||
: "bg-[var(--admin-accent-soft)] text-[var(--admin-accent)] ring-1 ring-[var(--admin-accent)]"
|
? "bg-[var(--admin-primary-soft)] text-[var(--admin-primary)] ring-1 ring-[var(--admin-primary)]"
|
||||||
}`}
|
: "bg-[var(--admin-accent-soft)] text-[var(--admin-accent)] ring-1 ring-[var(--admin-accent)]"
|
||||||
>
|
}`}
|
||||||
<span className="text-lg">{is_taxable ? "✓" : "✗"}</span>
|
>
|
||||||
<span>{is_taxable ? "Taxable — tax applied at checkout for nexus shipments" : "Non-taxable — always exempt from sales tax"}</span>
|
<span className="text-lg" aria-hidden="true">{is_taxable ? "✓" : "✗"}</span>
|
||||||
</button>
|
<span>{is_taxable ? "Taxable — tax applied at checkout for nexus shipments" : "Non-taxable — always exempt from sales tax"}</span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
<p className="mt-1.5 text-xs text-[var(--admin-text-muted)]">Tax is calculated at checkout for shipping orders in your brand's nexus states. Non-taxable items (e.g. cooler boxes, apparel) are always exempt.</p>
|
<p className="mt-1.5 text-xs text-[var(--admin-text-muted)]">Tax is calculated at checkout for shipping orders in your brand's nexus states. Non-taxable items (e.g. cooler boxes, apparel) are always exempt.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -317,7 +321,7 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
|
|||||||
</AdminInput>
|
</AdminInput>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<span className="mb-1 block text-sm font-medium text-[var(--admin-text-primary)]">Product Image</span>
|
||||||
<p className="text-xs text-[var(--admin-text-muted)] mb-2">JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)</p>
|
<p className="text-xs text-[var(--admin-text-muted)] mb-2">JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)</p>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
|||||||
|
|
||||||
{/* Drop zone */}
|
{/* Drop zone */}
|
||||||
<label
|
<label
|
||||||
|
htmlFor="schedule-file-upload"
|
||||||
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
||||||
onDragLeave={() => setDragOver(false)}
|
onDragLeave={() => setDragOver(false)}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -186,7 +187,8 @@ export default function ScheduleImportModal({ brandId, onClose, onComplete }: Pr
|
|||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input aria-label="File upload"
|
<input id="schedule-file-upload"
|
||||||
|
aria-label="File upload"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv,.txt,.json"
|
accept=".csv,.txt,.json"
|
||||||
|
|||||||
@@ -332,18 +332,21 @@ export default function StopEditForm({ stop, brands, onSaved }: StopEditFormProp
|
|||||||
</AdminInput>
|
</AdminInput>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<label className="mb-2 block text-sm font-medium text-stone-700">
|
||||||
<button
|
<span className="block mb-2">Status</span>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setActive((v) => !v)}
|
type="button"
|
||||||
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
|
onClick={() => setActive((v) => !v)}
|
||||||
active
|
className={`w-full rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
|
||||||
? "bg-green-100 text-green-700 border border-green-200"
|
active
|
||||||
: "bg-stone-100 text-stone-500 border border-stone-200 hover:bg-stone-50"
|
? "bg-green-100 text-green-700 border border-green-200"
|
||||||
}`}
|
: "bg-stone-100 text-stone-500 border border-stone-200 hover:bg-stone-50"
|
||||||
>
|
}`}
|
||||||
{active ? "✓ Active — visible on storefront" : "○ Inactive — hidden from storefront"}
|
>
|
||||||
</button>
|
<span aria-hidden="true">{active ? "✓ " : "○ "}</span>
|
||||||
|
{active ? "Active — visible on storefront" : "Inactive — hidden from storefront"}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AdminButton
|
<AdminButton
|
||||||
|
|||||||
@@ -80,11 +80,11 @@ export default function WaterLogEntryEditForm({ entry, backHref = "/admin/water-
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<span className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Headgate</span>
|
||||||
<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>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<span className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">User</span>
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,11 +106,11 @@ export default function WaterLogEntryEditForm({ entry, backHref = "/admin/water-
|
|||||||
/>
|
/>
|
||||||
</AdminInput>
|
</AdminInput>
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<span className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Submitted Via</span>
|
||||||
<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>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<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>
|
<span className="block text-sm font-medium text-[var(--admin-text-muted)] mb-1">Logged</span>
|
||||||
<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)]">
|
<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", {
|
{new Date(entry.logged_at).toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
|
|||||||
Reference in New Issue
Block a user