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); });