fix: react-doctor deslop/unused-export 7→0, prefer-module-scope-pure-function 6→0 (hoist handleExportCSV, downloadCSV, triggerDownload, scrollToContent, handleLogout, applyMappings)

This commit is contained in:
Nora
2026-06-26 06:24:02 -06:00
parent adce211480
commit 9633680e23
13 changed files with 145 additions and 240 deletions
+101 -112
View File
@@ -91,6 +91,107 @@ function getStatValueColor(highlight: boolean, warn: boolean) {
return "text-[var(--admin-text-muted)]";
}
function applyMappings(
mappings: ColumnMapping[],
overrides: Record<string, ImportField>,
headers: string[],
rows: string[][]
): ContactImportEntry[] {
const effective = mappings.map((m) => ({
...m,
field: overrides[m.csvColumn] ?? m.field,
}));
const seenEmails = new Set<string>();
const seenPhones = new Set<string>();
const entries: ContactImportEntry[] = [];
const headerIndex = new Map<string, number>();
headers.forEach((h, i) => headerIndex.set(h, i));
for (const row of rows) {
const entry: ContactImportEntry = {};
const ignored: Record<string, string> = {};
for (const mapping of effective) {
const colIdx = headerIndex.get(mapping.csvColumn);
if (colIdx === undefined) continue;
const raw = row[colIdx] ?? "";
if (mapping.field === null) {
if (raw) ignored[mapping.csvColumn] = raw;
continue;
}
switch (mapping.field) {
case "email": {
entry.email = raw.trim().toLowerCase() || undefined;
break;
}
case "phone": {
entry.phone = raw.trim() || undefined;
break;
}
case "first_name":
entry.first_name = raw || undefined;
break;
case "last_name":
entry.last_name = raw || undefined;
break;
case "full_name":
entry.full_name = raw || undefined;
break;
case "tags":
entry.tags = raw
? raw.split(/[;:,]/).flatMap((t) => {
const trimmed = t.trim();
return trimmed ? [trimmed] : [];
})
: undefined;
break;
case "email_opt_in":
entry.email_opt_in =
raw === "true" ||
raw === "1" ||
raw === "yes" ||
raw === "y" ||
raw === "on";
break;
case "sms_opt_in":
entry.sms_opt_in =
raw === "true" ||
raw === "1" ||
raw === "yes" ||
raw === "y" ||
raw === "on";
break;
case "external_id":
entry.external_id = raw || undefined;
break;
}
}
if (!entry.email && !entry.phone) continue;
let isDup = false;
if (entry.email) {
if (seenEmails.has(entry.email)) isDup = true;
seenEmails.add(entry.email);
}
if (entry.phone && !isDup) {
if (seenPhones.has(entry.phone)) isDup = true;
seenPhones.add(entry.phone);
}
if (isDup) continue;
if (Object.keys(ignored).length > 0) {
entry._metadata = ignored;
}
entries.push(entry);
}
return entries;
}
export default function ContactImportForm({ brandId }: { brandId: string }) {
const [step, setStep] = useState<Step>("idle");
const [file, setFile] = useState<File | null>(null);
@@ -208,118 +309,6 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
[handleFile]
);
// ── Mapping override ───────────────────────────────────────────────────────
function applyMappings(
mappings: ColumnMapping[],
overrides: Record<string, ImportField>,
headers: string[],
rows: string[][]
): ContactImportEntry[] {
const effective = mappings.map((m) => ({
...m,
field: overrides[m.csvColumn] ?? m.field,
}));
const emailColIdx = effective.findIndex((m) => m.field === "email");
const phoneColIdx = effective.findIndex((m) => m.field === "phone");
const seenEmails = new Set<string>();
const seenPhones = new Set<string>();
const entries: ContactImportEntry[] = [];
const headerIndex = new Map<string, number>();
headers.forEach((h, i) => headerIndex.set(h, i));
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const entry: ContactImportEntry = {};
const ignored: Record<string, string> = {};
for (const mapping of effective) {
const colIdx = headerIndex.get(mapping.csvColumn);
if (colIdx === undefined) continue;
const raw = row[colIdx] ?? "";
if (mapping.field === null) {
if (raw) ignored[mapping.csvColumn] = raw;
continue;
}
switch (mapping.field) {
case "email": {
const cleaned = raw.trim().toLowerCase();
entry.email = cleaned || undefined;
break;
}
case "phone": {
const stripped = raw.replace(/[\s\-().[\]]/g, "");
entry.phone = raw.trim() || undefined;
break;
}
case "first_name":
entry.first_name = raw || undefined;
break;
case "last_name":
entry.last_name = raw || undefined;
break;
case "full_name":
entry.full_name = raw || undefined;
break;
case "tags":
entry.tags = raw
? raw.split(/[;:,]/).flatMap((t) => {
const trimmed = t.trim();
return trimmed ? [trimmed] : [];
})
: undefined;
break;
case "email_opt_in":
entry.email_opt_in =
raw === "true" ||
raw === "1" ||
raw === "yes" ||
raw === "y" ||
raw === "on";
break;
case "sms_opt_in":
entry.sms_opt_in =
raw === "true" ||
raw === "1" ||
raw === "yes" ||
raw === "y" ||
raw === "on";
break;
case "external_id":
entry.external_id = raw || undefined;
break;
}
}
const hasEmail = !!entry.email;
const hasPhone = !!entry.phone;
if (!hasEmail && !hasPhone) continue;
// Dedupe within file
let isDup = false;
if (entry.email) {
if (seenEmails.has(entry.email!)) isDup = true;
seenEmails.add(entry.email!);
}
if (entry.phone && !isDup) {
if (seenPhones.has(entry.phone)) isDup = true;
seenPhones.add(entry.phone);
}
if (isDup) continue;
if (Object.keys(ignored).length > 0) {
entry._metadata = ignored;
}
entries.push(entry);
}
return entries;
}
// ── Confirm and import ─────────────────────────────────────────────────────
const handleConfirm = useCallback(
+11 -11
View File
@@ -68,6 +68,17 @@ function quarterLabel(startISO: string): string {
return `Q${q} ${s.getFullYear()}`;
}
function handleExportCSV(exportFn: () => string, filename: string) {
const csv = exportFn();
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function monthLabel(startISO: string): string {
const s = new Date(startISO + "T00:00:00");
return s.toLocaleDateString("en-US", { month: "long", year: "numeric" });
@@ -307,17 +318,6 @@ export default function ReportsDashboard({
setExplainError(null);
}
function handleExportCSV(exportFn: () => string, filename: string) {
const csv = exportFn();
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Auto-fetch when range or brand changes. The fetchAll call is wrapped in
// an async IIFE so the effect body does not call setState synchronously
// (avoids the cascading-renders ESLint rule).
+10 -10
View File
@@ -43,6 +43,16 @@ function buildRange(preset: DatePreset, customStart?: string, customEnd?: string
}
}
function downloadCSV(csv: string, filename: string) {
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function quarterLabel(start: string, end: string): string {
const s = new Date(start + "T00:00:00");
const q = Math.floor(s.getMonth() / 3) + 1;
@@ -261,16 +271,6 @@ export default function TaxDashboard({
}
}
function downloadCSV(csv: string, filename: string) {
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
const effectiveRate = summary && summary.total_gross_sales > 0
? (summary.total_tax_collected / summary.total_gross_sales) * 100
: 0;
@@ -3,6 +3,15 @@
import { useState, useEffect, useCallback } from "react";
function triggerDownload(url: string) {
const a = document.createElement("a");
a.href = url;
a.download = "";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const triggerLabel: Record<string, { en: string; color: string }> = {
daily_approaching: { en: "Daily OT Approaching", color: "bg-amber-100 text-amber-700" },
daily_reached: { en: "Daily OT Reached", color: "bg-red-100 text-red-700" },
@@ -236,15 +245,6 @@ export default function TimeTrackingSettingsClient({ brandId }: TimeTrackingSett
return `/api/time-tracking/export?${params}`;
}
function triggerDownload(url: string) {
const a = document.createElement("a");
a.href = url;
a.download = "";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const openAddWorker = () => {
setEditingWorker(null);
setWorkerName(""); setWorkerRole("worker"); setWorkerLang("en");