16a6756ad1
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.
537 lines
29 KiB
TypeScript
537 lines
29 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { AdminButton, AdminSearchInput } from "@/components/admin/design-system";
|
|
import { openHtmlInPopup } from "@/lib/safe-window";
|
|
import {
|
|
type WholesaleOrder,
|
|
type WholesaleCustomer,
|
|
bulkFulfillWholesaleOrders,
|
|
bulkRecordWholesaleDeposit,
|
|
markWholesaleOrderFulfilled,
|
|
recordWholesaleDeposit,
|
|
updateWholesaleOrderStatus,
|
|
deleteWholesaleOrder,
|
|
getWholesaleSettings,
|
|
enqueueWholesaleNotification,
|
|
} from "@/actions/wholesale";
|
|
import OrderDetailsModal from "@/components/wholesale/OrderDetailsModal";
|
|
import StatusBadge from "./StatusBadge";
|
|
import type { MsgFn } from "./types";
|
|
|
|
interface OrdersTabProps {
|
|
orders: WholesaleOrder[];
|
|
customers: WholesaleCustomer[];
|
|
brandId: string;
|
|
onMsg: MsgFn;
|
|
onRefresh: () => void;
|
|
}
|
|
|
|
// Orders tab — list/filter orders, fulfill, record deposits, bulk operations,
|
|
// cancel/delete, generate manifests, and dispatch notification emails on fulfillment.
|
|
export default function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: OrdersTabProps) {
|
|
const [showDepForm, setShowDepForm] = useState<string | null>(null);
|
|
const [depAmount, setDepAmount] = useState("");
|
|
const [depMethod, setDepMethod] = useState("cash");
|
|
const [fulfilling, setFulfilling] = useState<string | null>(null);
|
|
const [manifestLoading, setManifestLoading] = useState(false);
|
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [dateFrom, setDateFrom] = useState("");
|
|
const [dateTo, setDateTo] = useState("");
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [bulkDepAmount, setBulkDepAmount] = useState("");
|
|
const [bulkDepMethod, setBulkDepMethod] = useState("cash");
|
|
const [showBulkDep, setShowBulkDep] = useState(false);
|
|
const [bulkLoading, setBulkLoading] = useState(false);
|
|
const [openActions, setOpenActions] = useState<string | null>(null);
|
|
const [showViewOrder, setShowViewOrder] = useState<WholesaleOrder | null>(null);
|
|
const [deleting, setDeleting] = useState<string | null>(null);
|
|
|
|
const filtered = orders.filter(o => {
|
|
if (statusFilter !== "all" && o.status !== statusFilter) return false;
|
|
if (dateFrom && (!o.anticipated_pickup_date || o.anticipated_pickup_date < dateFrom)) return false;
|
|
if (dateTo && (!o.anticipated_pickup_date || o.anticipated_pickup_date > dateTo)) return false;
|
|
if (searchQuery) {
|
|
const q = searchQuery.toLowerCase();
|
|
if (!(o.company_name?.toLowerCase().includes(q) || o.invoice_number?.toLowerCase().includes(q) || o.customer_email?.toLowerCase().includes(q))) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
function toggleSelect(id: string) {
|
|
setSelected(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
|
}
|
|
function toggleAll() {
|
|
if (selected.size === filtered.length) { setSelected(new Set()); }
|
|
else { setSelected(new Set(filtered.map(o => o.id))); }
|
|
}
|
|
|
|
async function handleBulkFulfill() {
|
|
if (selected.size === 0) return;
|
|
setBulkLoading(true);
|
|
const result = await bulkFulfillWholesaleOrders(Array.from(selected));
|
|
setBulkLoading(false);
|
|
if (result.success) {
|
|
onMsg("success", `${result.count} order(s) marked as fulfilled.`);
|
|
setSelected(new Set());
|
|
onRefresh();
|
|
} else {
|
|
onMsg("error", result.error ?? "Bulk fulfill failed.");
|
|
}
|
|
}
|
|
|
|
async function handleBulkDeposit() {
|
|
if (selected.size === 0 || !bulkDepAmount) return;
|
|
setBulkLoading(true);
|
|
const result = await bulkRecordWholesaleDeposit(Array.from(selected), Number(bulkDepAmount), bulkDepMethod);
|
|
setBulkLoading(false);
|
|
if (result.success) {
|
|
onMsg("success", `Deposit recorded on ${result.count} order(s).`);
|
|
setSelected(new Set());
|
|
setShowBulkDep(false);
|
|
setBulkDepAmount("");
|
|
onRefresh();
|
|
} else {
|
|
onMsg("error", result.error ?? "Bulk deposit failed.");
|
|
}
|
|
}
|
|
|
|
async function handleFulfill(orderId: string) {
|
|
setFulfilling(orderId);
|
|
const result = await markWholesaleOrderFulfilled(orderId);
|
|
setFulfilling(null);
|
|
if (result.success) {
|
|
onMsg("success", "Order marked as fulfilled.");
|
|
onRefresh();
|
|
|
|
// Enqueue order fulfilled notification
|
|
const order = orders.find(o => o.id === orderId);
|
|
const customer = order ? customers.find(c => c.email === order.customer_email) : null;
|
|
if (order && customer) {
|
|
enqueueWholesaleNotification({
|
|
brandId: brandId,
|
|
customerId: customer.id,
|
|
orderId,
|
|
type: "order_fulfilled",
|
|
emailTo: customer.email,
|
|
subject: `Order ${order.invoice_number ?? orderId.slice(0, 8)} Ready for Pickup`,
|
|
bodyHtml: `
|
|
<h2>Your Order is Ready!</h2>
|
|
<p>Hi ${customer.company_name},</p>
|
|
<p>Your wholesale order <strong>${order.invoice_number ?? orderId.slice(0, 8)}</strong> has been fulfilled and is ready for pickup${order.anticipated_pickup_date ? ` on ${order.anticipated_pickup_date}` : ""}.</p>
|
|
<p>Thank you for your business!</p>
|
|
`,
|
|
bodyText: `Order ${order.invoice_number ?? orderId.slice(0, 8)} has been fulfilled and is ready for pickup${order.anticipated_pickup_date ? ` on ${order.anticipated_pickup_date}` : ""}. Thank you!`,
|
|
});
|
|
|
|
// Also notify the admin
|
|
const settings = await getWholesaleSettings(brandId);
|
|
const adminEmail = settings?.notification_email ?? settings?.from_email ?? settings?.invoice_business_email ?? null;
|
|
if (adminEmail) {
|
|
enqueueWholesaleNotification({
|
|
brandId: brandId,
|
|
customerId: customer.id,
|
|
orderId,
|
|
type: "order_fulfilled",
|
|
emailTo: adminEmail,
|
|
subject: `[Admin] Order Fulfilled — ${order.invoice_number ?? orderId.slice(0, 8)}`,
|
|
bodyHtml: `
|
|
<h2>Order Fulfilled</h2>
|
|
<p>Order <strong>${order.invoice_number ?? orderId.slice(0, 8)}</strong> for <strong>${customer.company_name}</strong> has been marked as fulfilled.</p>
|
|
${order.anticipated_pickup_date ? `<p><strong>Pickup Date:</strong> ${order.anticipated_pickup_date}</p>` : ""}
|
|
`,
|
|
bodyText: `Order ${order.invoice_number ?? orderId.slice(0, 8)} for ${customer.company_name} has been fulfilled.`,
|
|
});
|
|
}
|
|
|
|
// Trigger notification send (fire-and-forget)
|
|
fetch(`/api/wholesale/notifications/send`, { method: "POST" }).catch(() => {});
|
|
}
|
|
} else {
|
|
onMsg("error", result.error ?? "Failed to fulfill order.");
|
|
}
|
|
}
|
|
|
|
// Close actions dropdown when clicking outside
|
|
useEffect(() => {
|
|
function handleClick(e: MouseEvent) {
|
|
if (!(e.target as Element).closest(".actions-cell")) {
|
|
setOpenActions(null);
|
|
}
|
|
}
|
|
document.addEventListener("click", handleClick);
|
|
return () => document.removeEventListener("click", handleClick);
|
|
}, []);
|
|
|
|
function toggleActions(orderId: string, e: React.MouseEvent) {
|
|
e.stopPropagation();
|
|
setOpenActions(prev => prev === orderId ? null : orderId);
|
|
}
|
|
|
|
async function handleRecordDeposit(orderId: string) {
|
|
const result = await recordWholesaleDeposit(orderId, Number(depAmount), depMethod);
|
|
setShowDepForm(null);
|
|
setDepAmount("");
|
|
if (result.success) {
|
|
onMsg("success", "Deposit recorded.");
|
|
onRefresh();
|
|
} else {
|
|
onMsg("error", result.error ?? "Failed to record deposit.");
|
|
}
|
|
}
|
|
|
|
async function handleCancelOrder(orderId: string) {
|
|
if (!confirm("Cancel this order? This cannot be undone.")) return;
|
|
const result = await updateWholesaleOrderStatus(orderId, "cancelled");
|
|
if (result.success) {
|
|
onMsg("success", "Order cancelled.");
|
|
onRefresh();
|
|
} else {
|
|
onMsg("error", result.error ?? "Failed to cancel order.");
|
|
}
|
|
}
|
|
|
|
async function handleDeleteOrder(orderId: string) {
|
|
if (!confirm("Delete this order? Fulfilled and paid orders cannot be deleted. This cannot be undone.")) return;
|
|
setDeleting(orderId);
|
|
const result = await deleteWholesaleOrder(orderId);
|
|
setDeleting(null);
|
|
if (result.success) {
|
|
onMsg("success", "Order deleted.");
|
|
onRefresh();
|
|
} else {
|
|
onMsg("error", result.error ?? "Failed to delete order.");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-semibold text-[var(--admin-text-primary)]">Wholesale Orders ({orders.length})</h2>
|
|
|
|
{/* Filter bar */}
|
|
<div className="flex flex-wrap items-center gap-3 rounded-2xl bg-white border border-[var(--admin-border)] p-4 shadow-sm">
|
|
<div>
|
|
<label htmlFor="fld-1-status" className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1">Status</label>
|
|
<select id="fld-1-status" value={statusFilter} onChange={e => setStatusFilter(e.target.value)}
|
|
className="rounded-lg border border-[var(--admin-border)] px-3 py-1.5 text-sm outline-none focus:border-[var(--admin-accent)]">
|
|
<option value="all">All Statuses</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="awaiting_deposit">Awaiting Deposit</option>
|
|
<option value="confirmed">Confirmed</option>
|
|
<option value="fulfilled">Fulfilled</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="fld-2-pickup-from" className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1">Pickup From</label>
|
|
<input id="fld-2-pickup-from" type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
|
className="rounded-lg border border-[var(--admin-border)] px-3 py-1.5 text-sm outline-none focus:border-[var(--admin-accent)]" />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="fld-3-pickup-to" className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1">Pickup To</label>
|
|
<input id="fld-3-pickup-to" type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
|
className="rounded-lg border border-[var(--admin-border)] px-3 py-1.5 text-sm outline-none focus:border-[var(--admin-accent)]" />
|
|
</div>
|
|
<div className="flex-1 min-w-[180px]">
|
|
<label className="block text-xs font-medium text-[var(--admin-text-muted)] mb-1" htmlFor="fld-search">Search</label>
|
|
<AdminSearchInput
|
|
value={searchQuery}
|
|
onChange={e => setSearchQuery(e.target.value)}
|
|
onClear={() => setSearchQuery("")}
|
|
placeholder="Invoice, customer, email..."
|
|
/>
|
|
</div>
|
|
{filtered.length !== orders.length && (
|
|
<div className="self-end pb-1">
|
|
<span className="text-xs text-[var(--admin-text-muted)]">{filtered.length} of {orders.length}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Bulk actions */}
|
|
{selected.size > 0 && (
|
|
<div className="flex items-center gap-3 rounded-2xl bg-[var(--admin-accent-light)] border border-[var(--admin-accent)] px-4 py-3">
|
|
<span className="text-sm font-medium text-[var(--admin-accent-text)]">{selected.size} selected</span>
|
|
<AdminButton variant="primary" size="sm" onClick={handleBulkFulfill} disabled={bulkLoading} isLoading={bulkLoading}>
|
|
{bulkLoading ? "..." : "Bulk Fulfill"}
|
|
</AdminButton>
|
|
<AdminButton variant="secondary" size="sm" onClick={() => setShowBulkDep(true)}>
|
|
Bulk Record Deposit
|
|
</AdminButton>
|
|
<button type="button" onClick={() => setSelected(new Set())} className="ml-auto text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]">Clear selection</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Manifest section */}
|
|
<div className="rounded-2xl bg-white border border-[var(--admin-border)] p-4 flex items-center gap-4 shadow-sm">
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--admin-text-secondary)]">Load Manifest</p>
|
|
<p className="text-xs text-[var(--admin-text-muted)]">Generate a printable manifest for pending/active orders.</p>
|
|
</div>
|
|
<AdminButton
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={async () => {
|
|
setManifestLoading(true);
|
|
const pending = filtered.filter(o => o.fulfillment_status !== "fulfilled");
|
|
if (pending.length === 0) { setManifestLoading(false); return; }
|
|
const html = await fetch("/api/wholesale/manifest", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ brandId, orders: pending }),
|
|
}).then(r => r.text());
|
|
openHtmlInPopup(html);
|
|
setManifestLoading(false);
|
|
}}
|
|
disabled={manifestLoading}
|
|
isLoading={manifestLoading}
|
|
>
|
|
{manifestLoading ? "Generating..." : "Generate Manifest"}
|
|
</AdminButton>
|
|
</div>
|
|
|
|
<div className="rounded-2xl bg-white border border-[var(--admin-border)] shadow-sm overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-[var(--admin-bg-subtle)] text-[var(--admin-text-muted)]">
|
|
<tr>
|
|
<th className="px-5 py-3 font-semibold text-left w-10">
|
|
<input id="fld-search" type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAll} className="rounded" />
|
|
</th>
|
|
<th className="px-5 py-3 font-semibold text-left">Invoice</th>
|
|
<th className="px-5 py-3 font-semibold text-left">Customer</th>
|
|
<th className="px-5 py-3 font-semibold text-left">Pickup Date</th>
|
|
<th className="px-5 py-3 font-semibold text-right">Subtotal</th>
|
|
<th className="px-5 py-3 font-semibold text-right">Deposit</th>
|
|
<th className="px-5 py-3 font-semibold text-right">Balance</th>
|
|
<th className="px-5 py-3 font-semibold text-left">Status</th>
|
|
<th className="px-5 py-3 font-semibold text-left">Payment</th>
|
|
<th className="px-5 py-3 font-semibold text-left">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[var(--admin-border-light)]">
|
|
{filtered.length === 0 ? (
|
|
<tr><td colSpan={10} className="py-8 text-center text-[var(--admin-text-muted)]">No orders match the current filters.</td></tr>
|
|
) : filtered.map(o => (
|
|
<tr key={o.id} className={`hover:bg-[var(--admin-bg-subtle)] ${selected.has(o.id) ? "bg-[var(--admin-accent-light)]" : ""}`}>
|
|
<td className="px-5 py-3">
|
|
<input type="checkbox" checked={selected.has(o.id)} onChange={() => toggleSelect(o.id)} className="rounded" />
|
|
</td>
|
|
<td className="px-5 py-3 font-mono text-xs text-[var(--admin-text-muted)]">{o.invoice_number ?? "—"}</td>
|
|
<td className="px-5 py-3 font-medium text-[var(--admin-text-primary)]">{o.company_name}</td>
|
|
<td className="px-5 py-3 text-[var(--admin-text-secondary)]">{o.anticipated_pickup_date ?? "—"}</td>
|
|
<td className="px-5 py-3 text-right font-semibold text-[var(--admin-text-primary)]">${Number(o.subtotal).toFixed(2)}</td>
|
|
<td className="px-5 py-3 text-right text-[var(--admin-text-secondary)]">${Number(o.deposit_paid).toFixed(2)} / ${Number(o.deposit_required).toFixed(2)}</td>
|
|
<td className="px-5 py-3 text-right">
|
|
<span className={Number(o.balance_due) > 0 ? "text-[var(--admin-warning)] font-medium" : "text-[var(--admin-accent)]"}>
|
|
${Number(o.balance_due).toFixed(2)}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-3"><StatusBadge status={o.status} /></td>
|
|
<td className="px-5 py-3">
|
|
<span className={`text-xs font-medium ${o.payment_status === "paid" ? "text-[var(--admin-accent)]" : "text-[var(--admin-warning)]"}`}>
|
|
{o.payment_status}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-4 actions-cell relative">
|
|
<div className="flex items-center gap-2">
|
|
{/* Primary: Mark Fulfilled — only when order is not yet fulfilled */}
|
|
{o.fulfillment_status !== "fulfilled" && (
|
|
<AdminButton
|
|
variant="primary"
|
|
size="sm"
|
|
onClick={() => !fulfilling && handleFulfill(o.id)}
|
|
disabled={fulfilling === o.id}
|
|
isLoading={fulfilling === o.id}
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
|
|
{fulfilling === o.id ? "..." : "Fulfill"}
|
|
</AdminButton>
|
|
)}
|
|
|
|
{/* Primary: Record Deposit — when there's a balance due */}
|
|
{Number(o.balance_due) > 0 && o.fulfillment_status !== "fulfilled" && (
|
|
<AdminButton
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={() => setShowDepForm(o.id)}
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m-9-9h18" /></svg>
|
|
Deposit
|
|
</AdminButton>
|
|
)}
|
|
|
|
{/* Invoice download — always visible as icon button */}
|
|
<a aria-label="Download Invoice"
|
|
href={`/api/wholesale/invoice/${o.id}/pdf`}
|
|
target="_blank"
|
|
rel="noopener noreferrer" title="Download Invoice"
|
|
className="inline-flex items-center justify-center rounded-xl w-9 h-9 text-[var(--admin-text-muted)] hover:text-[var(--admin-accent)] hover:bg-[var(--admin-bg-subtle)] transition-colors">
|
|
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12h9m-9-6h6m-3 12a9 9 0 110 12H15M6 2h9l4 4v14a2 2 0 01-2 2H6a2 2 0 01-2-2V4a2 2 0 012-2z"/></svg>
|
|
</a>
|
|
|
|
{/* ⋮ Actions dropdown — opens upward to avoid table cutoff */}
|
|
<div className="relative">
|
|
<button type="button"
|
|
onClick={(e) => toggleActions(o.id, e)}
|
|
title="More actions"
|
|
className="inline-flex items-center justify-center rounded-xl w-9 h-9 text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><circle cx="10" cy="4" r="1.5"/><circle cx="10" cy="10" r="1.5"/><circle cx="10" cy="16" r="1.5"/></svg>
|
|
</button>
|
|
|
|
{openActions === o.id && (
|
|
<div
|
|
className="absolute bottom-full right-0 mb-1 z-30 w-56 rounded-xl bg-white shadow-xl ring-1 ring-[var(--admin-border)] py-1 text-sm"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<button type="button"
|
|
onClick={() => { setOpenActions(null); setShowViewOrder(o); }}
|
|
className="w-full text-left px-4 py-3 text-[var(--admin-accent)] hover:bg-[var(--admin-accent-light)] flex items-center gap-3 font-medium"
|
|
>
|
|
<svg className="w-4 h-4 text-[var(--admin-accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
|
View / Edit Order
|
|
</button>
|
|
|
|
<button type="button"
|
|
onClick={() => {
|
|
setOpenActions(null);
|
|
fetch("/api/wholesale/manifest", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ brandId, orders: [o] }),
|
|
})
|
|
.then((r) => r.text())
|
|
.then((html) => openHtmlInPopup(html));
|
|
}}
|
|
className="w-full text-left px-4 py-3 text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] flex items-center gap-3"
|
|
>
|
|
<svg className="w-4 h-4 text-[var(--admin-text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12h9m-9-6h6m-3 12a9 9 0 01-18 0 9 9 0 0118 0z"/></svg>
|
|
Generate Manifest
|
|
</button>
|
|
|
|
<button type="button"
|
|
onClick={() => {
|
|
setOpenActions(null);
|
|
fetch("/api/wholesale/price-sheet", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ customerIds: [o.customer_id], brandId }),
|
|
}).then(r => r.json()).then(d => onMsg("success", `Price sheet sent to customer.`)).catch(() => onMsg("error", "Failed to send price sheet."));
|
|
}}
|
|
className="w-full text-left px-4 py-3 text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] flex items-center gap-3"
|
|
>
|
|
<svg className="w-4 h-4 text-[var(--admin-text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
Send Price Sheet
|
|
</button>
|
|
|
|
<div className="my-1 border-t border-[var(--admin-border)]" />
|
|
|
|
{o.status !== "cancelled" && o.fulfillment_status !== "fulfilled" && (
|
|
<button type="button"
|
|
onClick={() => { setOpenActions(null); handleCancelOrder(o.id); }}
|
|
className="w-full text-left px-4 py-3 text-[var(--admin-danger)] hover:bg-[var(--admin-danger-light)] flex items-center gap-3"
|
|
>
|
|
<svg className="w-4 h-4 text-[var(--admin-danger)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
|
|
Cancel Order
|
|
</button>
|
|
)}
|
|
|
|
{o.fulfillment_status !== "fulfilled" && (
|
|
<button type="button"
|
|
onClick={() => { setOpenActions(null); handleDeleteOrder(o.id); }}
|
|
className="w-full text-left px-4 py-3 text-[var(--admin-danger)] hover:bg-[var(--admin-danger-light)] flex items-center gap-3"
|
|
>
|
|
<svg className="w-4 h-4 text-[var(--admin-danger)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}><path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
Delete Order
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Deposit modal */}
|
|
{showDepForm && (
|
|
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-2xl p-6 w-96 shadow-xl border border-[var(--admin-border)]">
|
|
<h3 className="font-semibold text-[var(--admin-text-primary)] mb-4">Record Deposit</h3>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label htmlFor="fld-4-amount" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Amount ($)</label>
|
|
<input id="fld-4-amount" type="number" value={depAmount} onChange={e => setDepAmount(e.target.value)}
|
|
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" step="0.01" />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="fld-5-method" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Method</label>
|
|
<select id="fld-5-method" value={depMethod} onChange={e => setDepMethod(e.target.value)}
|
|
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]">
|
|
<option value="cash">Cash</option>
|
|
<option value="check">Check</option>
|
|
<option value="wire">Wire</option>
|
|
<option value="card">Card</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 flex gap-3">
|
|
<AdminButton onClick={() => handleRecordDeposit(showDepForm)} variant="primary">
|
|
Record Deposit
|
|
</AdminButton>
|
|
<AdminButton onClick={() => setShowDepForm(null)} variant="secondary">
|
|
Cancel
|
|
</AdminButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bulk Deposit modal */}
|
|
{showBulkDep && (
|
|
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-2xl p-6 w-96 shadow-xl border border-[var(--admin-border)]">
|
|
<h3 className="font-semibold text-[var(--admin-text-primary)] mb-4">Bulk Record Deposit ({selected.size} orders)</h3>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label htmlFor="fld-6-amount-per-order" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Amount per order ($)</label>
|
|
<input id="fld-6-amount-per-order" type="number" value={bulkDepAmount} onChange={e => setBulkDepAmount(e.target.value)}
|
|
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]" step="0.01" placeholder="0.00" />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="fld-7-method-2" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Method</label>
|
|
<select id="fld-7-method-2" value={bulkDepMethod} onChange={e => setBulkDepMethod(e.target.value)}
|
|
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)]">
|
|
<option value="cash">Cash</option>
|
|
<option value="check">Check</option>
|
|
<option value="wire">Wire</option>
|
|
<option value="card">Card</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 flex gap-3">
|
|
<AdminButton onClick={handleBulkDeposit} variant="primary" disabled={!bulkDepAmount || bulkLoading} isLoading={bulkLoading}>
|
|
{bulkLoading ? "Recording..." : "Record on All"}
|
|
</AdminButton>
|
|
<AdminButton onClick={() => setShowBulkDep(false)} variant="secondary">
|
|
Cancel
|
|
</AdminButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Order Details modal */}
|
|
{showViewOrder && (
|
|
<OrderDetailsModal
|
|
order={showViewOrder}
|
|
onClose={() => setShowViewOrder(null)}
|
|
onFulfill={handleFulfill}
|
|
onRecordDeposit={(id) => { setShowViewOrder(null); setShowDepForm(id); }}
|
|
fulfilling={fulfilling}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
} |