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:
@@ -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(
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user