Compare commits
6 commits
3189ba90be
...
29c7954271
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29c7954271 | ||
|
|
1978b483e6 | ||
|
|
69601ce836 | ||
|
|
daa7bdb498 | ||
|
|
d2207389d9 | ||
|
|
d2c47ac15f |
5 changed files with 796 additions and 12 deletions
28
Dockerfile
28
Dockerfile
|
|
@ -8,27 +8,35 @@ RUN apt-get update && apt-get install -y openssl fontconfig
|
||||||
RUN fc-cache -f -v
|
RUN fc-cache -f -v
|
||||||
RUN pnpm i -g prisma prisma-kysely
|
RUN pnpm i -g prisma prisma-kysely
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y openssl fontconfig \
|
||||||
|
&& fc-cache -f -v \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
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
|
FROM base AS build
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
RUN pnpm prisma generate
|
RUN pnpm prisma generate
|
||||||
RUN pnpm run build
|
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
|
||||||
|
RUN pnpm prisma generate
|
||||||
|
|
||||||
ENV NODE_ENV="production"
|
FROM node:23-slim AS prod
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules /app/node_modules
|
COPY --from=deps /app/node_modules /app/node_modules
|
||||||
COPY --from=build /app/dist /app/dist
|
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
|
||||||
RUN chmod u+x ./entrypoint.sh
|
COPY entrypoint.sh .
|
||||||
|
|
||||||
|
RUN chmod +x ./entrypoint.sh
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
pnpm prisma migrate deploy
|
echo "Running prisma migrations..."
|
||||||
pnpm run start
|
npx prisma migrate deploy
|
||||||
|
|
||||||
|
echo "Starting server..."
|
||||||
|
node ./dist/app.js
|
||||||
|
|
|
||||||
218
importData.ts
Normal file
218
importData.ts
Normal file
|
|
@ -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<string, string>();
|
||||||
|
|
||||||
|
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 ✅");
|
||||||
|
});
|
||||||
319
jws-import-data.ts
Normal file
319
jws-import-data.ts
Normal file
|
|
@ -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<string, CsvRow>();
|
||||||
|
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<string, any>) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
235
jws-import-employee-data.ts
Normal file
235
jws-import-employee-data.ts
Normal file
|
|
@ -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<string, UniqueCompany>();
|
||||||
|
|
||||||
|
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<string, (typeof getCustomerBranches)[0]>();
|
||||||
|
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<string, string>();
|
||||||
|
provinces.forEach((p) => provinceMap.set(p.name, p.id));
|
||||||
|
|
||||||
|
const districtMap = new Map<string, string>();
|
||||||
|
districts.forEach((d) => districtMap.set(d.name, d.id));
|
||||||
|
|
||||||
|
const subDistrictMap = new Map<string, string>();
|
||||||
|
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 =
|
||||||
|
/^(?<houseNo>[\d\-\/]+)?\s*(?:หมู่\s*(?<moo>\d+))?\s*(?:ซอย\s*(?<soi>[^ ]+))?\s*(?<street>.*?)\s*(?:ตำบล|แขวง)?\s*(?<subDistrict>[^ ]+)?\s*(?:อำเภอ|เขต)?\s*(?<district>[^ ]+)?\s*จังหวัด\s*(?<province>[^ ]+)?\s*(?<postalCode>\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);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue