Compare commits

..

7 commits

Author SHA1 Message Date
HAM
3189ba90be fix: Dockerfile prisma module
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 14:40:58 +07:00
HAM
84137e031c fix: update Dockerfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 14:36:47 +07:00
HAM
8d2b8e192b update: entrypoint.sh
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 14:32:29 +07:00
HAM
c70c467e92 fix: no prisma module
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 14:30:02 +07:00
HAM
ad544bc544 fix: base for prod in Dockerfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 5s
2026-01-12 14:21:17 +07:00
HAM
e29ad6a64d chore: update Dockerfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 14:14:22 +07:00
HAM
134cc48de0 chore: update Dockerfile
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 4s
2026-01-12 14:10:47 +07:00
4 changed files with 2 additions and 777 deletions

View file

@ -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

View file

@ -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<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 ✅");
});

View file

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

View file

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