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:
@@ -1,6 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import {
|
||||
importContactsBatch,
|
||||
previewContactImport,
|
||||
@@ -355,152 +361,6 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
if (fileRef.current) fileRef.current.value = "";
|
||||
};
|
||||
|
||||
// ── Render helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function renderMappings() {
|
||||
if (!preview) return null;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border border-[var(--admin-border)] rounded-lg">
|
||||
<thead className="bg-[var(--admin-card)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-[var(--admin-text-muted)] font-semibold">
|
||||
CSV Column
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--admin-text-muted)] font-semibold">
|
||||
Maps To
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--admin-text-muted)] font-semibold">
|
||||
Sample Values
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{preview.mappings.map((m) => (
|
||||
<tr key={m.csvColumn} className="border-t border-[var(--admin-border)]">
|
||||
<td className="px-3 py-2 text-[var(--admin-text-primary)] font-mono text-xs">
|
||||
{m.csvColumn}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<select
|
||||
value={
|
||||
overridingMappings[m.csvColumn] ?? m.field ?? "ignore"
|
||||
}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setOverridingMappings((prev) => ({
|
||||
...prev,
|
||||
[m.csvColumn]:
|
||||
val === "ignore"
|
||||
? null
|
||||
: (val as ImportField),
|
||||
}));
|
||||
}}
|
||||
className="text-xs border border-[var(--admin-border)] rounded px-2 py-1 bg-white text-[var(--admin-text-primary)]"
|
||||
>
|
||||
<option value="ignore">— ignore —</option>
|
||||
{Object.entries(FIELD_LABELS).map(([k, label]) => (
|
||||
<option key={k} value={k}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[var(--admin-text-muted)] text-xs">
|
||||
{m.sampleValues.length > 0
|
||||
? m.sampleValues.join(", ")
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{preview.ignoredColumns.length > 0 && (
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
Extra columns (stored in metadata):
|
||||
<span className="font-mono">
|
||||
{" "}
|
||||
{preview.ignoredColumns.join(", ")}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
if (!preview) return null;
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Stat label="Total rows" value={preview.totalRows} />
|
||||
<Stat
|
||||
label="Importable"
|
||||
value={preview.validRows}
|
||||
highlight
|
||||
/>
|
||||
<Stat label="Skipped" value={preview.skippedRows} warn={preview.skippedRows > 0} />
|
||||
<Stat
|
||||
label="Duplicates"
|
||||
value={preview.duplicateRows}
|
||||
warn={preview.duplicateRows > 0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSampleRows() {
|
||||
if (!preview) return null;
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border border-[var(--admin-border)] rounded-lg">
|
||||
<thead className="bg-[var(--admin-card)]">
|
||||
<tr>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">#</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">email</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">phone</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">first_name</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">last_name</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">full_name</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">email_opt_in</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">sms_opt_in</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{preview.sampleRows.map((row, i) => (
|
||||
<tr key={i} className="border-t border-[var(--admin-border)] hover:bg-[var(--admin-card-hover)]">
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">{i + 1}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.email ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.phone ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.first_name ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.last_name ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.full_name ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">
|
||||
{row.email_opt_in === undefined
|
||||
? ""
|
||||
: row.email_opt_in
|
||||
? "true"
|
||||
: "false"}
|
||||
</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">
|
||||
{row.sms_opt_in === undefined
|
||||
? ""
|
||||
: row.sms_opt_in
|
||||
? "true"
|
||||
: "false"}
|
||||
</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">{row.tags?.join("; ") ?? ""}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main render ────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
@@ -514,7 +374,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Import Contacts</h3>
|
||||
</div>
|
||||
{step !== "idle" && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleReset}
|
||||
className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] transition-colors"
|
||||
>
|
||||
@@ -562,7 +422,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
<input aria-label="File upload"
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
@@ -594,7 +454,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
<>
|
||||
{preview.warnings.map((w, i) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`${w}-${i}`}
|
||||
className="rounded-lg bg-amber-50 border border-amber-200 px-4 py-3 text-sm text-amber-700"
|
||||
>
|
||||
{w}
|
||||
@@ -610,21 +470,27 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{renderMappings()}
|
||||
{preview ? (
|
||||
<MappingsView
|
||||
preview={preview}
|
||||
overridingMappings={overridingMappings}
|
||||
setOverridingMappings={setOverridingMappings}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-2 font-semibold uppercase tracking-wide">
|
||||
Import Summary
|
||||
</p>
|
||||
{renderStats()}
|
||||
{preview ? <StatsView preview={preview} /> : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-2 font-semibold uppercase tracking-wide">
|
||||
Sample rows (first {preview.sampleRows.length})
|
||||
</p>
|
||||
{renderSampleRows()}
|
||||
{preview ? <SampleRowsView preview={preview} /> : null}
|
||||
</div>
|
||||
|
||||
{preview.skippedReasons.length > 0 && (
|
||||
@@ -656,13 +522,13 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-sm text-[var(--admin-text-muted)] hover:bg-[var(--admin-card-hover)] transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => handleConfirm()}
|
||||
disabled={preview.validRows === 0}
|
||||
className="rounded-lg bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50 transition-colors"
|
||||
@@ -700,7 +566,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
</p>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1">
|
||||
{result.errors.slice(0, 10).map((e, i) => (
|
||||
<p key={i} className="text-red-600 text-xs">
|
||||
<p key={`${e.error}-${JSON.stringify(e.row)}-${i}`} className="text-red-600 text-xs">
|
||||
{e.error}: {JSON.stringify(e.row).slice(0, 80)}
|
||||
</p>
|
||||
))}
|
||||
@@ -713,7 +579,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleReset}
|
||||
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-sm text-[var(--admin-text-muted)] hover:bg-[var(--admin-card-hover)] transition-colors"
|
||||
>
|
||||
@@ -730,7 +596,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{importHistory.slice(0, 5).map((imp, i) => (
|
||||
<div key={i} className="flex items-center justify-between text-xs p-2 rounded-lg bg-[var(--admin-card)]">
|
||||
<div key={`${imp.filename}-${imp.createdAt}-${imp.size}`} className="flex items-center justify-between text-xs p-2 rounded-lg bg-[var(--admin-card)]">
|
||||
<div className="flex items-center gap-2">
|
||||
{Icons.file("h-4 w-4 text-[var(--admin-text-muted)]")}
|
||||
<span className="text-[var(--admin-text-primary)]">{imp.filename}</span>
|
||||
@@ -770,4 +636,146 @@ function Stat({
|
||||
<p className={`text-xs mt-0.5 ${labelColor}`}>{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
// ── Module-scope sub-views ───────────────────────────────────────────────────
|
||||
|
||||
function MappingsView({
|
||||
preview,
|
||||
overridingMappings,
|
||||
setOverridingMappings,
|
||||
}: {
|
||||
preview: ImportPreviewResult;
|
||||
overridingMappings: Record<string, ImportField>;
|
||||
setOverridingMappings: Dispatch<SetStateAction<Record<string, ImportField>>>;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border border-[var(--admin-border)] rounded-lg">
|
||||
<thead className="bg-[var(--admin-card)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-[var(--admin-text-muted)] font-semibold">
|
||||
CSV Column
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--admin-text-muted)] font-semibold">
|
||||
Maps To
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-[var(--admin-text-muted)] font-semibold">
|
||||
Sample Values
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{preview.mappings.map((m) => (
|
||||
<tr key={m.csvColumn} className="border-t border-[var(--admin-border)]">
|
||||
<td className="px-3 py-2 text-[var(--admin-text-primary)] font-mono text-xs">
|
||||
{m.csvColumn}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<select aria-label="Select"
|
||||
value={
|
||||
overridingMappings[m.csvColumn] ?? m.field ?? "ignore"
|
||||
}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setOverridingMappings((prev) => ({
|
||||
...prev,
|
||||
[m.csvColumn]:
|
||||
val === "ignore"
|
||||
? null
|
||||
: (val as ImportField),
|
||||
}));
|
||||
}}
|
||||
className="text-xs border border-[var(--admin-border)] rounded px-2 py-1 bg-white text-[var(--admin-text-primary)]"
|
||||
>
|
||||
<option value="ignore">— ignore —</option>
|
||||
{Object.entries(FIELD_LABELS).map(([k, label]) => (
|
||||
<option key={k} value={k}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[var(--admin-text-muted)] text-xs">
|
||||
{m.sampleValues.length > 0
|
||||
? m.sampleValues.join(", ")
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{preview.ignoredColumns.length > 0 && (
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
Extra columns (stored in metadata):
|
||||
<span className="font-mono">
|
||||
{" "}
|
||||
{preview.ignoredColumns.join(", ")}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatsView({ preview }: { preview: ImportPreviewResult }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Stat label="Total rows" value={preview.totalRows} />
|
||||
<Stat label="Importable" value={preview.validRows} highlight />
|
||||
<Stat label="Skipped" value={preview.skippedRows} warn={preview.skippedRows > 0} />
|
||||
<Stat label="Duplicates" value={preview.duplicateRows} warn={preview.duplicateRows > 0} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SampleRowsView({ preview }: { preview: ImportPreviewResult }) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border border-[var(--admin-border)] rounded-lg">
|
||||
<thead className="bg-[var(--admin-card)]">
|
||||
<tr>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">#</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">email</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">phone</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">first_name</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">last_name</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">full_name</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">email_opt_in</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">sms_opt_in</th>
|
||||
<th className="px-2 py-1 text-left text-[var(--admin-text-muted)]">tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{preview.sampleRows.map((row, i) => (
|
||||
<tr key={i} className="border-t border-[var(--admin-border)] hover:bg-[var(--admin-card-hover)]">
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">{i + 1}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.email ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.phone ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.first_name ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.last_name ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-primary)]">{row.full_name ?? ""}</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">
|
||||
{row.email_opt_in === undefined
|
||||
? ""
|
||||
: row.email_opt_in
|
||||
? "true"
|
||||
: "false"}
|
||||
</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">
|
||||
{row.sms_opt_in === undefined
|
||||
? ""
|
||||
: row.sms_opt_in
|
||||
? "true"
|
||||
: "false"}
|
||||
</td>
|
||||
<td className="px-2 py-1 text-[var(--admin-text-muted)]">{row.tags?.join("; ") ?? ""}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user