import React, { useState, useRef, useEffect } from "react"; import { toPng } from "html-to-image"; import JSZip from "jszip"; import { saveAs } from "file-saver"; import digifoxlogo from "../assets/images/digifox_logo.webp"; import Papa from "papaparse"; import { Download, Upload, CheckCircle2, ChevronRight, HelpCircle, Users, ImagePlus, } from "lucide-react"; import { Login } from "./components/Login"; import { AdminDashboard } from "./components/AdminDashboard"; import { Employee, Region } from "./types"; import { jsPDF } from "jspdf"; import cavinKareLogo from "../assets/images/CavinKare-logo-z3dMoof1.png"; import defaultBgImage from "../assets/images/id-card-bg-1.png"; const DEFAULT_REGIONS: Region[] = [ { id: "assam", name: "Assam", address: "INDUSTRIES GROWTH CENTRE, MATIA (MORNAI)\nOPPOSITE TO SAINIK SCHOOL, GOALPARA, ASSAM - 783 101.", phone: "", }, { id: "bhiwandi", name: "Bhiwandi Plant", address: "S.NO. 31/B / 32 A, DHAMANGAON OLD AGRA ROAD,\nTAL. - BHIWANDI, DISTT. - THANE, MUMBAI, MAHARASHTRA - 421 302.", phone: "", }, { id: "erode", name: "Erode", address: "SF NO. 532, BHAVANI TO ANTHIYUR MAIN ROAD, MYLAMPADI,\nKANNADIPALAYAM POST, BHAVANI TK, ERODE DT - 638 314.", phone: "04256 - 238 202 / 302 / 402", }, { id: "haridwar", name: "Haridwar", address: "PLOT NO.16 & 17,SECTOR-4, INTEGRATED INDUSTRIAL ESTATE,\nSIDCUL,RANIPUR, HARIDWAR-249403", phone: "01334-239246", }, { id: "kanchipuram", name: "Kanchipuram", address: "NO. 18, ARYAPERUMBAKKAM VILLAGE, KANCHIPURAM - 631 502.", phone: "2729 4580/81", }, { id: "pondy", name: "Pondy", address: "R.S. NO. 81/4, KORKADU VILLAGE, NETTAPAKKAM COMMUNE,\nPONDICHERRY -605 110.", phone: "0413 - 2665146", }, { id: "ro-east", name: "RO East", address: "14TH FLOOR, P.S. SRIJAN TECH PARK, DN - 52, DN BLOCK, SECTOR - 5,\nSALT LAKE CITY, KOLKATA - 700091", phone: "033 - 40669540", }, { id: "ro-north", name: "RO North", address: "D-12/ 2ND FLOOR, KAUSHAMBI, GHAZIABAD - 201010\nUTTAR PRADESH", phone: "0120-4335396", }, { id: "ro-south", name: "RO South", address: "NO. 12, POONAMALLEE ROAD, EKKATTUTHANGAL,\nCHENNAI - 6OO 032.", phone: "044 - 2225 1011 / 12", }, { id: "ro-west", name: "RO West", address: "504-A, RAHEJA PLAZA PREMISES CO-OP SOCIETY LTD.,\nL.B.S. MARG, GHATKOPAR (WEST), MUMBAI – 400086", phone: "", }, ]; export default function App() { const [role, setRole] = useState<"login" | "user" | "admin">(() => { const savedRole = localStorage.getItem("userRole"); return (savedRole as "login" | "user" | "admin") || "login"; }); useEffect(() => { localStorage.setItem("userRole", role); }, [role]); const [regions, setRegions] = useState(DEFAULT_REGIONS); const [employees, setEmployees] = useState([]); const [currentEmployee, setCurrentEmployee] = useState({ id: crypto.randomUUID(), name: "NAGAMUTHU A", employeeId: "1016434", unitId: "erode", doj: "09/03/2026", dob: "06/12/1999", bloodGroup: "AB+", emergencyNo: "9791238462", photoUrl: null, }); const [bgImage, setBgImage] = useState(null); const [isBatchMode, setIsBatchMode] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const cardRef = useRef(null); useEffect(() => { // 1. Fetch regions from server fetch("/api/regions") .then((res) => res.json()) .then((data) => { if (data && data.length > 0) setRegions(data); }) .catch((err) => console.error("Error fetching regions:", err)); // 2. Fetch global background setting const localBg = localStorage.getItem("savedBgImage"); if (localBg) { setBgImage(localBg); } else { fetch("/api/settings/globalBgImage") .then((res) => res.json()) .then((data) => { if (data.value) setBgImage(data.value); }) .catch((err) => console.error("Error fetching global bg setting:", err)); } }, []); const currentRegion = regions.find((r) => r.id === currentEmployee.unitId) || regions[0]; const logCreation = async (unitId: string, count: number) => { try { await fetch("/api/stats", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ unitId, count }), }); } catch (err) { console.error("Error logging card creation stats:", err); } }; const handleInputChange = (field: keyof Employee, value: any) => { setCurrentEmployee((prev) => ({ ...prev, [field]: value })); if (isBatchMode) { setEmployees((emps) => emps.map((e) => e.id === currentEmployee.id ? { ...e, [field]: value } : e, ), ); } }; const handlePhotoUpload = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (event) => { handleInputChange("photoUrl", event.target?.result as string); }; reader.readAsDataURL(file); } }; const handleBgUpload = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (event) => { setBgImage(event.target?.result as string); }; reader.readAsDataURL(file); } }; const handlePhotoFolderUpload = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { if (employees.length === 0) { alert("Please import CSV first before uploading photos."); return; } const files = Array.from(e.target.files) as File[]; const updatedEmployees = [...employees]; let matchedCount = 0; files.forEach((file) => { const lastDot = file.name.lastIndexOf("."); const fileNameWithoutExt = ( lastDot !== -1 ? file.name.substring(0, lastDot) : file.name ).trim(); const normalizedFileName = fileNameWithoutExt .toLowerCase() .replace(/[^a-z0-9]/g, ""); const employeeIndex = updatedEmployees.findIndex((emp) => { const empId = String(emp.employeeId || "").trim(); const empIdNormalized = empId.toLowerCase().replace(/[^a-z0-9]/g, ""); const empNameNormalized = String(emp.name || "") .toLowerCase() .replace(/[^a-z0-9]/g, ""); return ( (empId !== "" && empId === fileNameWithoutExt) || (empIdNormalized !== "" && empIdNormalized === normalizedFileName) || (empNameNormalized !== "" && empNameNormalized === normalizedFileName) ); }); if (employeeIndex >= 0) { updatedEmployees[employeeIndex].photoUrl = URL.createObjectURL(file); matchedCount++; } }); setEmployees(updatedEmployees); // update current employee if matched const currIdx = updatedEmployees.findIndex( (e) => e.id === currentEmployee.id, ); if (currIdx >= 0) setCurrentEmployee(updatedEmployees[currIdx]); alert(`Successfully matched ${matchedCount} photos to employees!`); } }; const handleCSVUpload = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { Papa.parse(e.target.files[0], { header: true, complete: (results) => { const newEmployees = results.data .map((row: any) => { const csvUnit = String(row.unitId || row.unit || row.region || "") .trim() .toLowerCase(); let matchedUnit = regions.find( (r) => r.id.toLowerCase() === csvUnit || r.name.toLowerCase() === csvUnit, ); if (!matchedUnit && csvUnit) { matchedUnit = regions.find( (r) => r.name.toLowerCase().includes(csvUnit) || csvUnit.includes(r.name.toLowerCase()), ); } const finalUnitId = matchedUnit ? matchedUnit.id : regions[0]?.id || "erode"; return { id: crypto.randomUUID(), name: String(row.name || "").trim(), employeeId: String(row.employeeId || "").trim(), unitId: finalUnitId, doj: String(row.doj || "").trim(), dob: String(row.dob || "").trim(), bloodGroup: String(row.bloodGroup || "").trim(), emergencyNo: String(row.emergencyNo || "").trim(), photoUrl: null, }; }) .filter((e) => e.name); if (newEmployees.length > 0) { setEmployees(newEmployees); setCurrentEmployee(newEmployees[0]); setIsBatchMode(true); } }, }); } }; const handleSave = () => { localStorage.setItem( "savedEmployeeDetails", JSON.stringify(currentEmployee), ); if (bgImage) { localStorage.setItem("savedBgImage", bgImage); } if (isBatchMode) { setEmployees((prev) => prev.map((e) => (e.id === currentEmployee.id ? currentEmployee : e)), ); } alert("Details saved successfully!"); }; const handleDownloadSampleCSV = () => { const csvContent = "name,employeeId,unit,doj,dob,bloodGroup,emergencyNo\nJohn Doe,101234,Erode,01/01/2023,15/08/1990,O+,9876543210\nJane Smith,101235,Chennai,15/06/2022,22/11/1992,A-,8765432109"; const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); saveAs(blob, "sample_employees.csv"); }; const handleDownloadSingleBoth = async () => { if (!cardRef.current) return; setIsGenerating(true); try { const dataUrl = await toPng(cardRef.current, { pixelRatio: 3 }); // 1. Download PNG Image const link = document.createElement("a"); link.download = `${currentEmployee.name || "ID"}_${currentRegion.name}.png`; link.href = dataUrl; link.click(); // 2. Download PDF Document (Standard portrait card format 54mm x 85.6mm) setTimeout(() => { const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: [54, 85.6], }); pdf.addImage(dataUrl, "PNG", 0, 0, 54, 85.6); pdf.save(`${currentEmployee.name || "ID"}_${currentRegion.name}.pdf`); }, 300); logCreation(currentEmployee.unitId, 1); } catch (err) { console.error("Error generating card:", err); } finally { setIsGenerating(false); } }; const generateBulkZip = async (format: "png" | "pdf" | "both" = "both") => { if (employees.length === 0 || !cardRef.current) return; setIsGenerating(true); try { const zip = new JSZip(); const folderPNG = (format === "png" || format === "both") ? zip.folder("PNG_Cards") : null; const folderPDF = (format === "pdf" || format === "both") ? zip.folder("PDF_Cards") : null; const originalEmployee = currentEmployee; for (let i = 0; i < employees.length; i++) { const emp = employees[i]; setCurrentEmployee(emp); await new Promise((resolve) => setTimeout(resolve, 150)); const dataUrl = await toPng(cardRef.current, { pixelRatio: 3 }); const base64Data = dataUrl.split(",")[1]; const region = regions.find((r) => r.id === emp.unitId) || regions[0]; const fileNameBase = `${emp.name || "ID"}_${region.name}`; if (folderPNG) { folderPNG.file(`${fileNameBase}.png`, base64Data, { base64: true }); } if (folderPDF) { const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: [54, 85.6], }); pdf.addImage(dataUrl, "PNG", 0, 0, 54, 85.6); const pdfArrayBuffer = pdf.output("arraybuffer"); folderPDF.file(`${fileNameBase}.pdf`, pdfArrayBuffer); } } setCurrentEmployee(originalEmployee); const content = await zip.generateAsync({ type: "blob" }); saveAs(content, `Bulk_ID_Cards_${format.toUpperCase()}.zip`); const counts: Record = {}; employees.forEach((emp) => { counts[emp.unitId] = (counts[emp.unitId] || 0) + 1; }); Object.entries(counts).forEach(([unitId, count]) => { logCreation(unitId, count); }); } catch (err) { console.error("Error generating ZIP:", err); alert("Failed to generate ZIP file."); } finally { setIsGenerating(false); } }; if (role === "login") { return ; } if (role === "admin") { return ( setRole("login")} regions={regions} setRegions={setRegions} /> ); } return (
{/* Header Section */}
CavinKare Logo
ID CARD Maker
1 {" "} Entry
2 {" "} Photo
3 {" "} Export
{/* Main Content Body */}
{/* Sidebar Configuration */} {/* Preview Canvas */}
{/* Floating Badge Label */}
{" "} Live Real-time Preview
{/* The ID Card */}
{/* Top empty space for the logo in the background image */}
{/* Card Content - Fields aligned as in the template */}
NAME
:
{currentEmployee.name || " "}
EMP ID
:
{currentEmployee.employeeId || " "}
UNIT
:
{currentRegion.name.toUpperCase()}
D.O.J
:
{currentEmployee.doj || " "}
D.O.B
:
{currentEmployee.dob || " "}
BLOOD GROUP
:
{currentEmployee.bloodGroup || " "}
EMERGENCY NO
:
{currentEmployee.emergencyNo || " "}
{/* Bottom Section: Photo, Signature, Company Address */}
{/* Photo Square */}
{currentEmployee.photoUrl ? ( Employee ) : (
)}
{/* Company Address Section */}

CAVINKARE PRIVATE LIMITED

{currentRegion.address}

{currentRegion.phone && (

PHONE : {currentRegion.phone}

)}
{/* Controls */} {isBatchMode ? (
{employees.findIndex((e) => e.id === currentEmployee.id) + 1}{" "} of {employees.length}
) : (
)}
{/* Footer Instruction Area */}
); }