fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
@@ -171,10 +171,16 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
}
|
||||
|
||||
async function openPrintWindow(html: string) {
|
||||
const w = window.open("", "_blank", "width=700,height=700");
|
||||
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, "_blank", "width=700,height=700");
|
||||
if (w) {
|
||||
w.document.write(html);
|
||||
w.document.close();
|
||||
// Revoke the blob URL once the new window has had time to load the document.
|
||||
w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
|
||||
// Safety net: revoke after 60s even if the load event never fires.
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
} else {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +191,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<div className="mx-auto max-w-4xl flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm">← Back</button>
|
||||
<button type="button" onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm">← Back</button>
|
||||
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Headgates & QR Codes</h1>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">Manage headgates and generate QR codes for field access</p>
|
||||
@@ -201,7 +207,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
{/* Bulk actions bar */}
|
||||
{headgates.length > 0 && (
|
||||
<div className="flex items-center gap-3 rounded-xl border border-[var(--admin-border)] bg-white px-4 py-3">
|
||||
<button onClick={toggleAll} className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<button type="button" onClick={toggleAll} className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
{selected.size === headgates.length ? "Deselect all" : "Select all"}
|
||||
</button>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">|</span>
|
||||
@@ -223,7 +229,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<form onSubmit={handleAdd} className="flex gap-3 items-end">
|
||||
<div className="flex-1">
|
||||
<label htmlFor="headgate-add-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Headgate Name</label>
|
||||
<input
|
||||
<input aria-label=". North Field Gate 1"
|
||||
id="headgate-add-name"
|
||||
autoFocus
|
||||
type="text"
|
||||
@@ -237,7 +243,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="headgate-add-unit" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
|
||||
<select
|
||||
<select aria-label="Headgate Add Unit"
|
||||
id="headgate-add-unit"
|
||||
value={newUnit}
|
||||
onChange={(e) => setNewUnit(e.target.value)}
|
||||
@@ -269,7 +275,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<thead>
|
||||
<tr className="border-b border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] text-left">
|
||||
<th className="px-4 py-3 w-8">
|
||||
<input type="checkbox" className="rounded" onChange={toggleAll} checked={selected.size === headgates.length} />
|
||||
<input aria-label="Checkbox" type="checkbox" className="rounded" onChange={toggleAll} checked={selected.size === headgates.length} />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Name</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Token</th>
|
||||
@@ -283,7 +289,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
{headgates.map((hg) => (
|
||||
<tr key={hg.id} className={`border-b border-[var(--admin-border)] last:border-0 hover:bg-[var(--admin-bg-subtle)] ${selected.has(hg.id) ? "bg-blue-50" : ""}`}>
|
||||
<td className="px-4 py-3">
|
||||
<input type="checkbox" className="rounded" checked={selected.has(hg.id)} onChange={() => toggleSelect(hg.id)} />
|
||||
<input aria-label="Checkbox" type="checkbox" className="rounded" checked={selected.has(hg.id)} onChange={() => toggleSelect(hg.id)} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -324,7 +330,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
Edit
|
||||
</AdminButton>
|
||||
{/* QR preview thumbnail */}
|
||||
<button onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
|
||||
<button type="button" onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
|
||||
<img
|
||||
src={`/api/water-qr?token=${hg.headgate_token}&size=80`}
|
||||
alt={`QR for ${hg.name}`}
|
||||
@@ -366,7 +372,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
|
||||
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
|
||||
<button onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<button type="button" onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -375,7 +381,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<form onSubmit={handleEditSave} className="p-5 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="headgate-edit-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Name</label>
|
||||
<input
|
||||
<input aria-label="Headgate Edit Name"
|
||||
id="headgate-edit-name"
|
||||
type="text"
|
||||
value={editName}
|
||||
@@ -388,7 +394,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label htmlFor="headgate-edit-unit" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
|
||||
<select
|
||||
<select aria-label="Headgate Edit Unit"
|
||||
id="headgate-edit-unit"
|
||||
value={editUnit}
|
||||
onChange={(e) => setEditUnit(e.target.value)}
|
||||
@@ -409,7 +415,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label htmlFor="headgate-edit-high" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">High Alert Threshold</label>
|
||||
<input
|
||||
<input aria-label=". 15.0"
|
||||
id="headgate-edit-high"
|
||||
type="number"
|
||||
step="any"
|
||||
@@ -421,7 +427,7 @@ export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="headgate-edit-low" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Low Alert Threshold</label>
|
||||
<input
|
||||
<input aria-label=". 5.0"
|
||||
id="headgate-edit-low"
|
||||
type="number"
|
||||
step="any"
|
||||
@@ -471,8 +477,15 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
||||
body: JSON.stringify({ token: hg.headgate_token, name: hg.name }),
|
||||
});
|
||||
const html = await res.text();
|
||||
const w = window.open("", "_blank", "width=500,height=600");
|
||||
if (w) { w.document.write(html); w.document.close(); }
|
||||
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const w = window.open(url, "_blank", "width=500,height=600");
|
||||
if (w) {
|
||||
w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
} else {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -504,7 +517,7 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
||||
<p className="font-bold text-[var(--admin-text-primary)]">{hg.name}</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">QR Label Preview</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -531,7 +544,7 @@ function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => v
|
||||
{/* Tab toggle */}
|
||||
<div className="flex border-b border-[var(--admin-border)]">
|
||||
{(["preview", "print", "download"] as const).map((t) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
className={`flex-1 py-3 text-sm font-semibold transition-colors ${tab === t ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-accent)]" : "text-[var(--admin-text-muted)]"}`}
|
||||
|
||||
@@ -15,11 +15,12 @@ type HeadgatePageProps = {
|
||||
};
|
||||
|
||||
export default async function WaterLogHeadgatePage({ params, searchParams }: HeadgatePageProps) {
|
||||
const { id } = await params;
|
||||
const { from } = await searchParams;
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
const waterSession = await getWaterAdminSession();
|
||||
const [{ id }, { from }, adminUser, waterSession] = await Promise.all([
|
||||
params,
|
||||
searchParams,
|
||||
getAdminUser(),
|
||||
getWaterAdminSession(),
|
||||
]);
|
||||
|
||||
const isSiteAdmin =
|
||||
adminUser?.role === "platform_admin" ||
|
||||
|
||||
Reference in New Issue
Block a user