diff --git a/Dockerfile b/Dockerfile index 099883e..2c8cd7b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,8 @@ FROM node:23-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable -RUN apt-get update && apt-get install -y openssl fontconfig -RUN fc-cache -f -v -RUN pnpm i -g prisma prisma-kysely +RUN corepack enable \ + && corepack prepare pnpm@9.15.0 --activate RUN apt-get update \ && apt-get install -y openssl fontconfig \ @@ -14,7 +12,6 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* WORKDIR /app - COPY . . FROM base AS build diff --git a/importData.ts b/importData.ts deleted file mode 100644 index 8085f6b..0000000 --- a/importData.ts +++ /dev/null @@ -1,218 +0,0 @@ -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 deleted file mode 100644 index 02cd10e..0000000 --- a/jws-import-data.ts +++ /dev/null @@ -1,319 +0,0 @@ -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 deleted file mode 100644 index 88d9218..0000000 --- a/jws-import-employee-data.ts +++ /dev/null @@ -1,235 +0,0 @@ -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); - });