From d2c47ac15f0a6f032ad507283edffb362e826452 Mon Sep 17 00:00:00 2001 From: HAM Date: Mon, 12 Jan 2026 14:14:22 +0700 Subject: [PATCH] chore: update Dockerfile --- Dockerfile | 13 +- importData.ts | 218 ++++++++++++++++++++++++ jws-import-data.ts | 319 ++++++++++++++++++++++++++++++++++++ jws-import-employee-data.ts | 235 ++++++++++++++++++++++++++ 4 files changed, 779 insertions(+), 6 deletions(-) create mode 100644 importData.ts create mode 100644 jws-import-data.ts create mode 100644 jws-import-employee-data.ts diff --git a/Dockerfile b/Dockerfile index 7b7967d..5e45914 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,22 +12,23 @@ WORKDIR /app COPY . . -FROM base AS deps -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile -RUN pnpm prisma generate - FROM base AS build RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN pnpm prisma generate RUN pnpm run build -FROM base AS prod +FROM base AS deps +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile +FROM node:23-slim AS prod ENV NODE_ENV="production" +WORKDIR /app COPY --from=deps /app/node_modules /app/node_modules COPY --from=build /app/dist /app/dist -COPY --from=base /app/static /app/static +COPY --from=build /app/prisma /app/prisma +COPY --from=build /app/static /app/static +COPY entrypoint.sh . RUN chmod u+x ./entrypoint.sh diff --git a/importData.ts b/importData.ts new file mode 100644 index 0000000..8085f6b --- /dev/null +++ b/importData.ts @@ -0,0 +1,218 @@ +import { parse } from "csv-parse"; +import fs from "fs"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient({ + datasourceUrl: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL, +}); + +type CsvRow = { + customerType: string; + status: string; + statusOrder: string; + branchCode: string; + branchTaxNo: string; + branchName: string; + branchNameEN: string; + code: string; + legalPersonNo: string; + registerName: string; + registerNameEN: string; + registerDate: string; + authorizedCapital: string; + authorizedName: string; + authorizedNameEN: string; + email: string; + telephoneNo: string; + employmentOffice: string; + employmentOfficeEN: string; + officeTel: string; + jobPosition: string; + jobDescription: string; + payDate: string; + payDateEN: string; + wageRate: string; + wageRateText: string; + namePrefix: string; + firstName: string; + firstNameEN: string; + lastName: string; + lastNameEN: string; + contactName: string; + contactTel: string; + gender: string; + birthDate: string; + address: string; + addressEN: string; + moo: string; + mooEN: string; + soi: string; + soiEN: string; + street: string; + streetEN: string; + subDistrict: string; + district: string; + province: string; + zipcode: string; + homeCode: string; + lineId: string; +}; + +async function importCsv(filePath: string) { + const parser = fs.createReadStream(filePath).pipe(parse({ columns: true, trim: true })); + + const rows: CsvRow[] = []; + for await (const row of parser) { + rows.push(row as CsvRow); + } + + // ✅ 1. ดึง subDistrict ล่วงหน้า + const subDistricts = [...new Set(rows.map((r) => r.subDistrict).filter(Boolean))]; + + const searchAddr = await prisma.subDistrict.findMany({ + where: { + name: { in: subDistricts }, + }, + include: { + district: { + include: { + province: true, + }, + }, + }, + }); + + const sdtDtPv = new Map< + string, + { subDistrictId: string; districtId: string; provinceId: string } + >(); + + for (const s of searchAddr) { + if (!s.zipCode) continue; + sdtDtPv.set(s.zipCode, { + subDistrictId: s.id, + districtId: s.districtId, + provinceId: s.district.provinceId, + }); + } + + // ✅ cache branch ที่สร้างแล้วเพื่อลด query + const branchCache = new Map(); + + for (const row of rows) { + // หา province/district/subdistrict จาก zipcode + const addr = sdtDtPv.get(row.zipcode); + + let branchId = branchCache.get(row.branchCode); + + if (!branchId) { + let registeredBranch = await prisma.branch.findFirst({ + where: { + code: row.branchCode, + name: row.branchName, + }, + }); + + if (!registeredBranch) { + registeredBranch = await prisma.branch.create({ + data: { + code: row.branchCode, + taxNo: row.branchTaxNo || "-", + name: row.branchName, + nameEN: row.branchNameEN || row.branchName, + telephoneNo: row.telephoneNo || "-", + permitNo: "-", + address: row.address || "-", + addressEN: row.addressEN || row.address || "-", + email: row.email || "-", + latitude: "0", + longitude: "0", + headOfficeId: "DEFAULT_HEAD_OFFICE_ID", + status: "CREATED", + statusOrder: 0, + }, + }); + } + + branchId = registeredBranch.id; + branchCache.set(row.branchCode, branchId); + } + + // ✅ Create customer + const customer = await prisma.customer.create({ + data: { + customerType: row.customerType as any, + status: row.status as any, + statusOrder: Number(row.statusOrder) || 0, + registeredBranch: { + connect: { id: branchId }, + }, + }, + }); + + // ✅ Create CustomerBranch + await prisma.customerBranch.create({ + data: { + customerId: customer.id, + + code: row.code || "-", + codeCustomer: row.legalPersonNo || "-", + + telephoneNo: row.telephoneNo || "-", + + namePrefix: row.namePrefix || null, + firstName: row.firstName || null, + firstNameEN: row.firstNameEN || null, + lastName: row.lastName || null, + lastNameEN: row.lastNameEN || null, + gender: row.gender || null, + birthDate: row.birthDate ? new Date(row.birthDate) : null, + citizenId: row.legalPersonNo || null, + + legalPersonNo: row.legalPersonNo || null, + registerName: row.registerName || null, + registerNameEN: row.registerNameEN || null, + registerDate: row.registerDate ? new Date(row.registerDate) : null, + authorizedCapital: row.authorizedCapital || null, + authorizedName: row.authorizedName || null, + authorizedNameEN: row.authorizedNameEN || null, + + homeCode: row.homeCode || "-", + employmentOffice: row.employmentOffice || "-", + employmentOfficeEN: row.employmentOfficeEN || "-", + + // Address + address: row.address || "-", + addressEN: row.addressEN || "-", + soi: row.soi || null, + soiEN: row.soiEN || null, + moo: row.moo || null, + mooEN: row.mooEN || null, + street: row.street || null, + streetEN: row.streetEN || null, + provinceId: addr?.provinceId, + districtId: addr?.districtId, + subDistrictId: addr?.subDistrictId, + + // Contact + email: row.email || "-", + contactTel: row.contactTel || "-", + officeTel: row.officeTel || "-", + contactName: row.contactName || "-", + + jobPosition: row.jobPosition || "-", + jobDescription: row.jobDescription || "-", + payDate: row.payDate || "-", + payDateEN: row.payDateEN || "-", + wageRate: parseInt(row.wageRate || "0", 10), + wageRateText: row.wageRateText || "-", + }, + }); + + console.log(`✅ Inserted customer ${customer.id} with branch ${branchId}`); + } +} + +importCsv("/home/hamu/Documents/JWS import.csv").then(() => { + console.log("Import finished ✅"); +}); diff --git a/jws-import-data.ts b/jws-import-data.ts new file mode 100644 index 0000000..02cd10e --- /dev/null +++ b/jws-import-data.ts @@ -0,0 +1,319 @@ +import { parse } from "csv-parse"; +import fs from "fs"; +import HttpStatus from "./src/interfaces/http-status"; +import HttpError from "./src/interfaces/http-error"; +import { PrismaClient } from "@prisma/client"; +import { CustomerType, Status } from "@prisma/client"; // enum จาก Prisma + +const prisma = new PrismaClient({ + datasourceUrl: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL, +}); + +type CsvRow = { + jobNo: string; + class: string; + authorizedName: string; + customerNameEN: string; + customerName: string; + fullAddress: string; + fullAddressEN: string; + jobDescription: string; + businessTypeTH: string; + businessType: string; + citizenId: string; + registerDate: string; + legalPersonNo: string; + authorizedCapital: string; + jobPositionTH: string; + jobPosition: string; + homeCode: string; + address: string; + addressEN: string; + moo: string; + mooEN: string; + soi: string; + soiEN: string; + street: string; + streetEN: string; + subDistrict: string; + subDistrictEN: string; + district: string; + districtEN: string; + province: string; + provinceEN: string; + zipCode: string; + telephoneNo: string; + employmentOffice: string; + employmentOfficeEN: string; + payDate: string; + payDateEN: string; + wageRate: string; + contactName: string; + contactTel: string; +}; + +async function importCsv(filePath: string) { + const parser = fs.createReadStream(filePath).pipe(parse({ columns: true, trim: true })); + + const rows: CsvRow[] = []; + for await (const row of parser) { + rows.push(row as CsvRow); + } + + // 1) Group by (authorizedName + address) => key = authorizedName|subDistrict|district|province + const comboMap = new Map(); + for (const row of rows) { + const sub = (row.subDistrict || "").trim(); + const dist = (row.district || "").trim(); + const prov = (row.province || "").trim(); + const auth = (row.authorizedName || "").trim(); + + if (!auth || !sub || !dist || !prov) continue; // ข้ามบรรทัดถ้าขาดข้อมูลสำคัญ + + const comboKey = `${auth}|${sub}|${dist}|${prov}`; + if (!comboMap.has(comboKey)) { + comboMap.set(comboKey, row); // เก็บแค่รายการแรกของ combo นี้ + } + } + + const groupedRows = Array.from(comboMap.values()); // รายการ unique (authorizedName+address) + + // 2) สร้าง set ของ unique address keys (subDistrict|district|province) จาก groupedRows + const uniqueAddrKeys = [ + ...new Set( + groupedRows.map((r) => `${r.subDistrict.trim()}|${r.district.trim()}|${r.province.trim()}`), + ), + ]; + + // 3) ดึงชื่อ subDistrict เพื่อ query DB (query ทีเดียว) + const subDistrictNames = [...new Set(groupedRows.map((r) => r.subDistrict.trim()))]; + + // 4) Query DB หา subDistrict (รวม district + province) แล้ว filter ให้ตรงทั้ง 3 ชั้น + const searchAddr = await prisma.subDistrict.findMany({ + where: { name: { in: subDistrictNames } }, + include: { + district: { + include: { province: true }, + }, + }, + }); + + // 5) สร้าง map: key = "subDistrict|district|province" -> ids + const allAddrList = new Map< + string, + { subDistrictId: string; districtId: string; provinceId: string; zipCode?: string } + >(); + + for (const s of searchAddr) { + const key = `${s.name.trim()}|${s.district.name.trim()}|${s.district.province.name.trim()}`; + if (uniqueAddrKeys.includes(key)) { + allAddrList.set(key, { + subDistrictId: s.id, + districtId: s.districtId, + provinceId: s.district.provinceId, + zipCode: s.zipCode || undefined, + }); + } + } + + // 6) ตอนนี้ groupedRows คือ list ของ authorizedName+address ที่ไม่ซ้ำกัน (ตาม combo) + // และ allAddrList ให้ mapping จากชื่อ -> ids + // ตัวอย่าง: สรุปผล (หรือเอาไป process ต่อ เช่น สร้าง Customer ฯล.) + const resultList = groupedRows.map((r) => { + const addrKey = `${r.subDistrict.trim()}|${r.district.trim()}|${r.province.trim()}`; + const ids = allAddrList.get(addrKey) || null; + return { + authorizedName: r.authorizedName, + addressKey: addrKey, + addrIds: ids, + raw: r, + }; + }); + + console.log(`Grouped combos: ${resultList.length}`); + + return saveToDb(resultList, allAddrList); +} + +async function saveToDb(resultList: any[], allAddrList: Map) { + for (const item of resultList) { + const row = item.raw; + + // แปลง class -> customerType + let customerType: CustomerType; + if (row.class === "นายจ้าง (นิติบุคคล)") customerType = CustomerType.CORP; + else if (row.class === "นายจ้าง (บุคคลธรรมดา)") customerType = CustomerType.PERS; + else customerType = CustomerType.PERS; // default ถ้าไม่แมทช์ + + const registerBranch = await prisma.branch.findFirst({ + where: { id: "cmfywjxk0003hqm1xybqwe48t" }, + }); + + if (registerBranch) { + await prisma.branch.updateMany({ + where: { + id: registerBranch.id, + status: "CREATED", + }, + data: { + status: "INACTIVE", + statusOrder: 1, + }, + }); + + const branch = [ + { + legalPersonNo: "1234567890123", + namePrefix: "", + firstName: "", + lastName: "", + firstNameEN: "", + lastNameEN: "", + telephoneNo: "", + gender: "", + businessTypeId: "cmfp1dvrt0009ph1x6qk21l1s", + jobPosition: "domesticHelper", + jobDescription: "", + payDate: "", + payDateEN: "", + wageRate: 0, + wageRateText: "", + homeCode: "", + employmentOffice: "", + employmentOfficeEN: "", + address: "123", + addressEN: "123", + street: "", + streetEN: "", + moo: "", + mooEN: "", + soi: "", + soiEN: "", + provinceId: "40", + districtId: "4010", + subDistrictId: "401005", + contactName: "", + email: "", + contactTel: "", + officeTel: "", + status: "CREATED", + registerName: "ทดสอบ", + registerNameEN: "Opaspong", + authorizedCapital: "", + authorizedName: "", + authorizedNameEN: "", + }, + ]; + const company = registerBranch.code; + const headoffice = branch[0]; + + if (!headoffice) { + throw new HttpError( + HttpStatus.BAD_REQUEST, + "Require at least one branch as headoffice", + "requireOneMinBranch", + ); + } + + const runningKey = `CUSTOMER_BRANCH_${company}_${"citizenId" in headoffice ? headoffice.citizenId : headoffice.legalPersonNo}`; + + const last = await prisma.runningNo.upsert({ + where: { key: runningKey }, + create: { + key: runningKey, + value: branch.length, + }, + update: { value: { increment: branch.length } }, + }); + + // ✅ Customer + const customer = await prisma.customer.create({ + data: { + customerType, + status: Status.CREATED, + statusOrder: 0, + registeredBranch: { connect: { id: registerBranch.id } }, + }, + }); + + // ✅ Address mapping + const addrKey = `${row.subDistrict.trim()}|${row.district.trim()}|${row.province.trim()}`; + const addr = allAddrList.get(addrKey); + + // ✅ CustomerBranch + await prisma.customerBranch.create({ + data: { + customerId: customer.id, + code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - branch.length + 1}`.padStart(2, "0")}`, + codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""), + telephoneNo: row.telephoneNo || "-", + + // บุคคลธรรมดา + citizenId: row.citizenId || null, + namePrefix: row.namePrefix || null, + firstName: row.firstname || null, + firstNameEN: row.firstnameEN || null, + lastName: row.firstname || null, + lastNameEN: row.firstnameEN || null, + authorizedName: row.authorizedName || null, + authorizedNameEN: row.authorizedNameEN || null, + + // นิติบุคคล + legalPersonNo: row.legalPersonNo || null, + registerName: row.registerName || null, + registerNameEN: row.registerNameEN || null, + registerDate: parseBuddhistDate(row.registerDate), + authorizedCapital: row.authorizedCapital || null, + + // ที่อยู่ + homeCode: row.homeCode || "-", + employmentOffice: row.employmentOffice || "-", + employmentOfficeEN: row.employmentOfficeEN || "-", + address: row.address || "-", + addressEN: row.addressEN || "-", + soi: row.soi || null, + soiEN: row.soiEN || null, + moo: row.moo || null, + mooEN: row.mooEN || null, + street: row.street || null, + streetEN: row.streetEN || null, + provinceId: addr?.provinceId, + districtId: addr?.districtId, + subDistrictId: addr?.subDistrictId, + + // Contact + email: row.email || "-", + contactTel: row.contactTel || "-", + officeTel: row.officeTel || "-", + contactName: row.contactName || "-", + + // Job + jobPosition: row.jobPosition || "-", + jobDescription: row.jobDescription || "-", + payDate: row.payDate || "-", + payDateEN: row.payDateEN || "-", + wageRate: parseInt(row.wageRate || "0", 10), + wageRateText: "-", + }, + }); + + console.log(`✅ Saved customer ${customer.id}`); + } + } +} + +function parseBuddhistDate(csvDate: string): Date { + const [day, monthStr, yearStr] = csvDate.split("-"); + const dayNum = parseInt(day, 10); + const monthNum = new Date(`${monthStr} 1, 2000`).getMonth(); + const yearBE = parseInt(yearStr, 10); + const yearCE = yearBE + 2500 - 543; + return new Date(yearCE, monthNum, dayNum); +} + +importCsv("/home/hamu/Downloads/JWS - import customer template.csv") + .then(() => console.log("Import finished ✅")) + .catch((err) => { + console.error("Import failed:", err); + process.exit(1); + }); diff --git a/jws-import-employee-data.ts b/jws-import-employee-data.ts new file mode 100644 index 0000000..88d9218 --- /dev/null +++ b/jws-import-employee-data.ts @@ -0,0 +1,235 @@ +import { parse } from "csv-parse"; +import fs from "fs"; +import HttpStatus from "./src/interfaces/http-status"; +import HttpError from "./src/interfaces/http-error"; +import { PrismaClient } from "@prisma/client"; +import { CustomerType, Status } from "@prisma/client"; // enum จาก Prisma + +const prisma = new PrismaClient({ + datasourceUrl: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL, +}); + +type CsvRow = { + workerId: string; + fullName: string; + firstName: string; + gender: string; + dateOfBirth: string; + age: string; + ppBirthCountry: string; + nationality: string; + ppNumber: string; + ppIssuePlace: string; + ppIssueCountry: string; + ppIssueDate: string; + ppExpireDate: string; + visaNumber: string; + visaIssuePlace: string; + visaType: string; + visaIssueDate: string; + visaExpireDate: string; + visaArrivalTM: string; + visaArrivalAt: string; + visaArrivalTMNo: string; + visaReportDate: string; + identityNo: string; + workPermitNo: string; + nameListNo: string; + nrcNo: string; + nameOfId: string; + fatherFirstNameEN: string; + jobNo: string; + companyFullName: string; + companyFullNameEN: string; + address: string; + jobDescription: string; + contactName: string; + contactTel: string; +}; + +type UniqueCompany = { + companyFullName: string; + address: string; + jobDescription: string; +}; + +async function importEmployeeCsv(filePath: string) { + const parser = fs.createReadStream(filePath).pipe(parse({ columns: true, trim: true })); + + const rows: CsvRow[] = []; + for await (const row of parser) { + rows.push(row as CsvRow); + } + + const uniqueMap = new Map(); + + for (const row of rows) { + const key = `${row.companyFullName}|${row.address}|${row.jobDescription}`; + if (!uniqueMap.has(key)) { + uniqueMap.set(key, { + companyFullName: row.companyFullName, + address: row.address, + jobDescription: row.jobDescription, + }); + } + } + + const uniqueCompanies: UniqueCompany[] = Array.from(uniqueMap.values()); + console.log("Unique Companies:", uniqueCompanies); + + const getCustomerBranches = await prisma.customerBranch.findMany({ + where: { + OR: uniqueCompanies.map((c) => ({ + AND: [{ authorizedName: c.companyFullName }], + })), + }, + }); + + if (getCustomerBranches.length > 0) { + // map สำหรับ lookup customerBranchId + const branchMap = new Map(); + getCustomerBranches.forEach((branch) => { + branchMap.set(branch.authorizedName || "", branch); + }); + + await prisma.$transaction(async (tx) => { + for (const row of rows) { + const branch = branchMap.get(row.companyFullName); + if (!branch) continue; + + const gender = row.gender?.toLowerCase(); + + const addressParts = parseThaiAddress(row.address); + + // โหลดทั้งหมดจาก DB + const provinces = await prisma.province.findMany(); + const districts = await prisma.district.findMany(); + const subDistricts = await prisma.subDistrict.findMany(); + + // สร้าง map สำหรับ lookup + const provinceMap = new Map(); + provinces.forEach((p) => provinceMap.set(p.name, p.id)); + + const districtMap = new Map(); + districts.forEach((d) => districtMap.set(d.name, d.id)); + + const subDistrictMap = new Map(); + subDistricts.forEach((s) => subDistrictMap.set(s.name, s.id)); + + const provinceId = provinceMap.get(addressParts.province ?? "") ?? null; + const districtId = districtMap.get(addressParts.district ?? "") ?? null; + const subDistrictId = subDistrictMap.get(addressParts.subDistrict ?? "") ?? null; + + const last = await tx.runningNo.upsert({ + where: { + key: `EMPLOYEE_${branch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, + }, + create: { + key: `EMPLOYEE_${branch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, + value: 1, + }, + update: { value: { increment: 1 } }, + }); + + // 1. Employee + const employee = await tx.employee.create({ + data: { + code: `${branch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${last.value}`.padStart(7, "0")}`, + customerBranchId: branch.id, + firstName: row.firstName, + firstNameEN: "", + gender: gender, + dateOfBirth: row.dateOfBirth ? new Date(row.dateOfBirth) : null, + nationality: row.nationality, + nrcNo: row.nrcNo, + workerStatus: null, + status: Status.CREATED, + + address: addressParts.houseNo, + street: addressParts.street, + soi: addressParts.soi, + moo: addressParts.moo, + provinceId: provinceId, + districtId: districtId, + subDistrictId: subDistrictId, + }, + }); + + // 2. EmployeeOtherInfo + await tx.employeeOtherInfo.create({ + data: { + employeeId: employee.id, + citizenId: row.identityNo, + fatherFirstNameEN: row.fatherFirstNameEN, + }, + }); + + // 3. EmployeePassport + await tx.employeePassport.create({ + data: { + employeeId: employee.id, + number: row.ppNumber, + type: "PASSPORT", + issueDate: row.ppIssueDate ? new Date(row.ppIssueDate) : "", + expireDate: row.ppExpireDate ? new Date(row.ppExpireDate) : "", + issueCountry: row.ppIssueCountry, + issuePlace: row.ppIssuePlace, + firstName: row.firstName, + firstNameEN: "", + gender: gender, + birthCountry: row.ppBirthCountry, + birthDate: row.dateOfBirth, + nationality: row.nationality, + workerStatus: "normal", + }, + }); + + // 4. EmployeeVisa + await tx.employeeVisa.create({ + data: { + employeeId: employee.id, + number: row.visaNumber, + type: row.visaType, + entryCount: 1, + issuePlace: row.visaIssuePlace, + issueCountry: "", + issueDate: row.visaIssueDate ? new Date(row.visaIssueDate) : "", + expireDate: row.visaExpireDate ? new Date(row.visaExpireDate) : "", + reportDate: row.visaReportDate ? new Date(row.visaReportDate) : undefined, + arrivalTM: row.visaArrivalTM, + arrivalTMNo: row.visaArrivalTMNo, + arrivalAt: row.visaArrivalAt, + }, + }); + } + }); + + console.log("Import completed ✅"); + } else { + console.warn("No customer branches found for the CSV companies."); + } +} + +function parseThaiAddress(address: string) { + const regex = + /^(?[\d\-\/]+)?\s*(?:หมู่\s*(?\d+))?\s*(?:ซอย\s*(?[^ ]+))?\s*(?.*?)\s*(?:ตำบล|แขวง)?\s*(?[^ ]+)?\s*(?:อำเภอ|เขต)?\s*(?[^ ]+)?\s*จังหวัด\s*(?[^ ]+)?\s*(?\d{5})?$/; + const match = address.match(regex); + if (!match || !match.groups) return {}; + return { + houseNo: match.groups.houseNo?.trim(), + moo: match.groups.moo?.trim(), + soi: match.groups.soi?.trim(), + street: match.groups.street?.trim(), + subDistrict: match.groups.subDistrict?.trim(), + district: match.groups.district?.trim(), + province: match.groups.province?.trim(), + postalCode: match.groups.postalCode?.trim(), + }; +} + +importEmployeeCsv("/home/hamu/Downloads/JWS - import employee template example 3.csv") + .then(() => console.log("Import finished ✅")) + .catch((err) => { + console.error("Import failed:", err); + process.exit(1); + });