Files
route-commerce/src/components/wholesale/admin/SettingsTab.tsx
T
Nora f7ac9399b2 fix: react-doctor control-has-associated-label 137→? (auto aria-labels)
Add aria-label to buttons (icon-only, pagination, action) and inputs
(placeholder-derived) via balanced-brace JSX parser. Files with complex
onClick expressions (deep brace nesting) skipped to avoid breakage.
2026-06-26 04:08:56 -06:00

287 lines
17 KiB
TypeScript

import { useState } from "react";
import { AdminButton } from "@/components/admin/design-system";
import {
type WholesaleSettings,
type NotificationRecipient,
saveWholesaleSettings,
} from "@/actions/wholesale";
import AddRecipientForm from "./AddRecipientForm";
import WebhookSettingsSection from "./WebhookSettingsSection";
import type { MsgFn } from "./types";
interface SettingsTabProps {
settings: WholesaleSettings | null;
brandId: string;
onMsg: MsgFn;
onRefresh: () => void;
canManageSettings: boolean;
}
// Wholesale portal settings: approval flow, payments, pickup location, invoice
// details, Square sync toggle, team notification recipients, and outbound webhook.
export default function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }: SettingsTabProps) {
// Hooks must be called unconditionally - always declare them first
const [form, setForm] = useState({
requireApproval: settings?.require_approval ?? true,
minOrderAmount: settings?.min_order_amount ?? "",
onlinePaymentEnabled: settings?.online_payment_enabled ?? false,
wholesaleEnabled: settings?.wholesale_enabled ?? true,
squareSyncEnabled: settings?.square_sync_enabled ?? false,
pickupLocation: settings?.pickup_location ?? "",
fobLocation: settings?.fob_location ?? "",
fromEmail: settings?.from_email ?? "",
invoiceBusinessName: settings?.invoice_business_name ?? "",
invoiceBusinessAddress: settings?.invoice_business_address ?? "",
invoiceBusinessPhone: settings?.invoice_business_phone ?? "",
invoiceBusinessEmail: settings?.invoice_business_email ?? "",
invoiceBusinessWebsite: settings?.invoice_business_website ?? "",
notificationRecipients: settings?.notification_recipients ?? [],
});
const [saving, setSaving] = useState(false);
// Permission check after hooks
if (!canManageSettings) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-slate-400">You do not have permission to manage settings.</p>
</div>
);
}
async function handleSave() {
setSaving(true);
const result = await saveWholesaleSettings({
brandId,
requireApproval: form.requireApproval,
minOrderAmount: form.minOrderAmount ? Number(form.minOrderAmount) : undefined,
onlinePaymentEnabled: form.onlinePaymentEnabled,
wholesaleEnabled: form.wholesaleEnabled,
squareSyncEnabled: form.squareSyncEnabled,
pickupLocation: form.pickupLocation,
fobLocation: form.fobLocation,
fromEmail: form.fromEmail,
invoiceBusinessName: form.invoiceBusinessName,
invoiceBusinessAddress: form.invoiceBusinessAddress,
invoiceBusinessPhone: form.invoiceBusinessPhone,
invoiceBusinessWebsite: form.invoiceBusinessWebsite,
notificationRecipients: form.notificationRecipients,
});
setSaving(false);
if (result.success) {
onMsg("success", "Settings saved.");
onRefresh();
} else {
onMsg("error", result.error ?? "Failed to save settings.");
}
}
// ── Notification Recipients helpers ────────────────────────────────────────
function addRecipient(email: string, name: string = "") {
setForm(f => ({
...f,
notificationRecipients: [
...f.notificationRecipients,
{ email, name, active: true },
],
}));
}
function removeRecipient(idx: number) {
setForm(f => ({
...f,
notificationRecipients: f.notificationRecipients.filter((_: unknown, i: number) => i !== idx),
}));
}
function toggleRecipient(idx: number) {
setForm(f => ({
...f,
notificationRecipients: f.notificationRecipients.map((r: NotificationRecipient, i: number) =>
i === idx ? { ...r, active: !r.active } : r
),
}));
}
function updateRecipientName(idx: number, name: string) {
setForm(f => ({
...f,
notificationRecipients: f.notificationRecipients.map((r: NotificationRecipient, i: number) =>
i === idx ? { ...r, name } : r
),
}));
}
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-[var(--admin-text-primary)]">Wholesale Settings</h2>
<div className="rounded-2xl bg-white border border-[var(--admin-border)] p-6 shadow-sm">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-[var(--admin-text-primary)]">Require Approval</p>
<p className="text-sm text-[var(--admin-text-muted)]">New registrations must be manually approved.</p>
</div>
<button type="button"
onClick={() => setForm(f => ({ ...f, requireApproval: !f.requireApproval }))}
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors ${form.requireApproval ? "bg-[var(--admin-accent)]" : "bg-[var(--admin-border)]"}`}
>
<span className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${form.requireApproval ? "translate-x-6" : "translate-x-1"}`} />
</button>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-[var(--admin-text-primary)]">Wholesale Portal</p>
<p className="text-sm text-[var(--admin-text-muted)]">Show the Wholesale Portal card on this brand&apos;s storefront page.</p>
</div>
<button type="button"
onClick={() => setForm(f => ({ ...f, wholesaleEnabled: !f.wholesaleEnabled }))}
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors ${form.wholesaleEnabled ? "bg-[var(--admin-accent)]" : "bg-[var(--admin-border)]"}`}
>
<span className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${form.wholesaleEnabled ? "translate-x-6" : "translate-x-1"}`} />
</button>
</div>
<div className="flex items-center justify-between rounded-xl border border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] px-4 py-3">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--admin-accent-light)]">
<svg className="h-4 w-4 text-[var(--admin-accent)]" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
</div>
<div>
<p className="font-medium text-[var(--admin-text-primary)]">Square Sync for Wholesale</p>
<p className="text-xs text-[var(--admin-text-muted)]">
When enabled, wholesale product changes are automatically pushed to Square.
{form.squareSyncEnabled
? " Auto-sync is active."
: " Auto-sync is disabled — changes will not be pushed to Square."}
</p>
</div>
</div>
<button type="button"
onClick={() => setForm(f => ({ ...f, squareSyncEnabled: !f.squareSyncEnabled }))}
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors ${form.squareSyncEnabled ? "bg-[var(--admin-accent)]" : "bg-[var(--admin-border)]"}`}
>
<span className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${form.squareSyncEnabled ? "translate-x-6" : "translate-x-1"}`} />
</button>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="fld-1-minimum-order-amount" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Minimum Order Amount ($)</label>
<input id="fld-1-minimum-order-amount" type="number" value={form.minOrderAmount} onChange={e => setForm(f => ({ ...f, minOrderAmount: 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="No minimum" aria-label="No minimum"/>
</div>
<div>
<label htmlFor="fld-2-from-email" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">From Email</label>
<input id="fld-2-from-email" type="email" value={form.fromEmail} onChange={e => setForm(f => ({ ...f, fromEmail: 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)]" />
</div>
<div className="col-span-2">
<label htmlFor="fld-3-pickup-location" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Pickup Location</label>
<textarea id="fld-3-pickup-location" value={form.pickupLocation} onChange={e => setForm(f => ({ ...f, pickupLocation: 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)] font-mono" rows={3} />
</div>
<div className="col-span-2">
<label htmlFor="fld-4-fob-location" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">FOB Location</label>
<input id="fld-4-fob-location" value={form.fobLocation} onChange={e => setForm(f => ({ ...f, fobLocation: 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)]" />
</div>
<div className="col-span-2">
<label htmlFor="fld-5-invoice-business-name" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Invoice Business Name</label>
<input id="fld-5-invoice-business-name" value={form.invoiceBusinessName} onChange={e => setForm(f => ({ ...f, invoiceBusinessName: 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)]" />
</div>
<div className="col-span-2">
<label htmlFor="fld-6-invoice-business-address" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Invoice Business Address</label>
<textarea id="fld-6-invoice-business-address" value={form.invoiceBusinessAddress} onChange={e => setForm(f => ({ ...f, invoiceBusinessAddress: 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)]" rows={2} />
</div>
<div>
<label htmlFor="fld-7-invoice-business-phone" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Invoice Business Phone</label>
<input id="fld-7-invoice-business-phone" value={form.invoiceBusinessPhone} onChange={e => setForm(f => ({ ...f, invoiceBusinessPhone: 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)]" />
</div>
<div>
<label htmlFor="fld-8-invoice-business-email" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Invoice Business Email</label>
<input id="fld-8-invoice-business-email" type="email" value={form.invoiceBusinessEmail} onChange={e => setForm(f => ({ ...f, invoiceBusinessEmail: 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)]" />
</div>
<div className="col-span-2">
<label htmlFor="fld-9-invoice-business-website" className="block text-sm font-medium text-[var(--admin-text-secondary)] mb-1">Invoice Business Website</label>
<input id="fld-9-invoice-business-website" value={form.invoiceBusinessWebsite} onChange={e => setForm(f => ({ ...f, invoiceBusinessWebsite: 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)]" placeholder="https://" aria-label="https://"/>
</div>
{/* Team Notification Recipients */}
<div className="col-span-2 rounded-2xl bg-[var(--admin-bg-subtle)] p-5 ring-1 ring-[var(--admin-border)]">
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1" htmlFor="fld-team-notification-recipients">Team Notification Recipients</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-4 leading-relaxed">
These team members receive all wholesale notifications for this brand new orders,
deposits, fulfillments, price sheets, and pickup reminders. If no recipients are
active, the system falls back to any configured notification email on file.
</p>
{/* Empty state */}
{form.notificationRecipients.length === 0 && (
<div className="text-center py-6 mb-4 rounded-xl bg-white ring-1 ring-[var(--admin-border)]">
<p className="text-sm text-[var(--admin-text-muted)] mb-1">No recipients added yet.</p>
<p className="text-xs text-[var(--admin-text-muted)]">Add an email address below to get started.</p>
</div>
)}
{/* Recipients list */}
<div className="space-y-2 mb-4">
{form.notificationRecipients.map((r: NotificationRecipient, idx: number) => (
<div key={r.email} className={`flex items-center gap-2 rounded-xl px-3 py-2.5 bg-white ring-1 ${r.active ? "ring-[var(--admin-border)]" : "ring-[var(--admin-border-light)] opacity-70"}`}>
<input id="fld-team-notification-recipients"
type="checkbox" checked={r.active} onChange={() => toggleRecipient(idx)}
className="rounded border-[var(--admin-border)] mt-0.5" title={r.active ? "Active — receives notifications" : "Inactive"} />
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium ${r.active ? "text-[var(--admin-text-primary)]" : "text-[var(--admin-text-muted)]"}`}>{r.email}</p>
{r.name && <p className="text-xs text-[var(--admin-text-muted)] truncate">{r.name}</p>}
{r.notification_types && r.notification_types.length > 0 && (
<p className="text-xs text-[var(--admin-text-secondary)] mt-0.5" title="Per-type filtering coming soon">Types: {r.notification_types.join(", ")}</p>
)}
</div>
<input
type="text" placeholder="Name (optional)"
value={r.name ?? ""}
onChange={e => updateRecipientName(idx, e.target.value)}
className="rounded-xl border border-[var(--admin-border)] px-2.5 py-1.5 text-xs w-36 outline-none focus:border-[var(--admin-accent)]" aria-label="Name (optional)"/>
<button type="button" onClick={() => removeRecipient(idx)}
className="text-[var(--admin-danger)] hover:text-[var(--admin-danger)] text-xs font-medium px-2 py-1 rounded-lg hover:bg-[var(--admin-danger-light)] transition-colors"
title="Remove recipient" aria-label="Remove recipient">
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
{/* Add recipient */}
<AddRecipientForm onAdd={addRecipient} />
</div>
{/* Webhook Settings */}
<div className="col-span-2 rounded-2xl bg-[var(--admin-bg-subtle)] p-5 ring-1 ring-[var(--admin-border)]">
<label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1" htmlFor="fld-webhook-integration">Webhook Integration</label><label className="block text-sm font-semibold text-[var(--admin-text-primary)] mb-1">Webhook Integration</label>
<p className="text-xs text-[var(--admin-text-muted)] mb-4 leading-relaxed">
Send order events to external systems (Harvest Point, ERPs, etc.). Payload is signed
with HMAC-SHA256. Enable and configure the URL and secret below.
</p>
<WebhookSettingsSection brandId={brandId} onMsg={onMsg} />
</div>
</div>
<AdminButton onClick={handleSave} disabled={saving} variant="primary" isLoading={saving}>
{saving ? "Saving..." : "Save Settings"}
</AdminButton>
</div>
</div>
</div>
);
}