Compare commits

...

74 Commits

Author SHA1 Message Date
8df36bd3e2 *: added projectEdit and tweaking projectSearch
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m18s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
added useredit param into projectSearchController.js, also use switch case instead of if-else. projectEdit is still not working yet

Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-12-01 18:27:07 +07:00
d7c19bbc5b -comma
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m10s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-12-01 09:06:35 +07:00
x2Skyz
f2d988681a .
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m17s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-12-01 00:53:57 +07:00
x2Skyz
5ad8079465 .
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m15s
Build Docker Image / Restart Docker Compose (push) Successful in 1s
2025-11-30 23:47:47 +07:00
x2Skyz
fc8332f25b -login
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m20s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
-projectAdd and validate
2025-11-30 21:58:23 +07:00
x2Skyz
dd07f09243 uploads และ downloads
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m31s
Build Docker Image / Restart Docker Compose (push) Successful in 1s
2025-11-30 19:47:06 +07:00
x2Skyz
98e69ca5f0 ignore
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m11s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-11-30 19:30:26 +07:00
x2Skyz
20f0bb12fa ignor
Some checks failed
Build Docker Image / Build Docker Image (push) Has been cancelled
Build Docker Image / Restart Docker Compose (push) Has been cancelled
2025-11-30 19:30:10 +07:00
x2Skyz
351e348af1 ระบบ uploads
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m19s
Build Docker Image / Restart Docker Compose (push) Successful in 1s
2025-11-30 19:28:24 +07:00
x2Skyz
e881d7311b + pkg multer, archiver
+ ระบบ uploads ที่เือบสมบูร
2025-11-30 19:28:03 +07:00
x2Skyz
b32515779f -
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m7s
Build Docker Image / Restart Docker Compose (push) Successful in 1s
2025-11-29 12:13:17 +07:00
x2Skyz
16c3c1dc15 -token search
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m4s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-11-28 21:53:32 +07:00
x2Skyz
15e2cbae68 +packet
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m6s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-11-27 21:56:05 +07:00
x2Skyz
192451ecce Merge branch 'master' of http://10.9.0.0/ttc/micro-service-api 2025-11-27 21:55:08 +07:00
x2Skyz
8d112178d1 -socket 2025-11-27 21:55:02 +07:00
b5d4a8ccfc Workflow: change image to ubuntu-node for faster building
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 4s
Build Docker Image / Restart Docker Compose (push) Successful in 1s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-26 14:49:43 +07:00
62944baf24 Workflow: change image to ubuntu-node for faster building
Some checks failed
Build Docker Image / Build Docker Image (push) Successful in 1m25s
Build Docker Image / Restart Docker Compose (push) Has been cancelled
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-26 14:41:16 +07:00
x2Skyz
f416b065e3 -แก้ไฟล์
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m12s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-11-25 22:22:19 +07:00
x2Skyz
e0e7bd44e7 -.
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m37s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-11-25 22:06:05 +07:00
x2Skyz
1cb3a2bc2d -แก้ไข search พร้อม fonction ใหม่
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m10s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
2025-11-25 16:10:10 +07:00
0f176deb1b xecel: removed node_modules
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m12s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-25 16:03:29 +07:00
x2Skyz
b46197ca06 -expanse
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m14s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
-prjadd '0.00'
2025-11-25 15:59:21 +07:00
x2Skyz
5e22a0af02 -**Global** date.js
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m11s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
-แก้ไข  Expense สมบูรแบบ
2025-11-25 15:47:20 +07:00
d29744bcfb workflow: rebuild again
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m19s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-25 15:25:32 +07:00
d3decda0f7 Workflow: try again
Some checks failed
Build Docker Image / Restart Docker Compose (push) Has been cancelled
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-25 15:23:20 +07:00
x2Skyz
76d48f895f -Expense
Some checks failed
Build Docker Image / Build Docker Image (push) Successful in 3m43s
Build Docker Image / Restart Docker Compose (push) Has been cancelled
-Search
2025-11-25 15:12:38 +07:00
9771fa1360 listen-pipe.sh: added execpipe maker, for communicate with workflow and hostsys
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m38s
Build Docker Image / Restart Docker Compose (push) Successful in 0s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 15:32:35 +07:00
5a1ed0887e Workflow: figure out, finally
Some checks failed
Build Docker Image / Restart Docker Compose (push) Has been cancelled
Build Docker Image / Build Docker Image (push) Has been cancelled
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 15:31:10 +07:00
5bf68cfe40 Workflow: fix invalid workflow file
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m55s
Build Docker Image / Restart Docker Compose (push) Successful in 1s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 15:16:14 +07:00
7f405281c6 Workflow: try again
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 15:14:36 +07:00
8d8f7e278b Workflow: trying out linux:host runs-on
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 15:10:02 +07:00
32eae26d45 Workflow: double shutdown docker container
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m54s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 15:00:13 +07:00
be31d4a2b1 Workflow: introducing execpipe/hostpipe
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 4m4s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 14:54:02 +07:00
3f88f91281 Workflow: try verbose on curl
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 2m26s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 14:21:08 +07:00
fb52941dc6 Workflow: try hello world to watcherd
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 2m27s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 14:16:27 +07:00
ac3b028b7c Workflow: WOAH
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 11s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 14:14:36 +07:00
25f90e9c93 Workflow: again, once again, just use curl
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 15s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 14:12:39 +07:00
cb846dd346 Workflow: test pinging to ensure that can reach watcherd
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 17s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-24 14:10:25 +07:00
10b45d8439 workflow: try again
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m21s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 22:40:20 +07:00
d38a59a7c1 workflow: call watcherd when docker image is updated
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m7s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 21:35:55 +07:00
x2Skyz
b1fef4c600 แก้ env
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m0s
2025-11-23 19:26:36 +07:00
x2Skyz
74dead1f4b Merge branch 'master' of http://10.9.0.0/ttc/micro-service-api
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m10s
# Conflicts:
#	exthernal-accountingwep-api/.env
2025-11-23 15:55:40 +07:00
x2Skyz
18a8548596 เช็ค User ซ้ำ 2025-11-23 15:54:16 +07:00
aa0b17740a workflow: fix typo on docker
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 2m39s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 13:34:36 +07:00
8fd3ada2f3 Workflow: set +e-e to docker rm container
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 2m6s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 13:20:19 +07:00
4b29570757 .env: fix host
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 2m46s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 19:33:53 +07:00
712e17ece8 routes/route.js: case-sensitive
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 2m40s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 18:40:57 +07:00
40383733cd Dockerfile: just add the entire repo
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 2m42s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 17:45:37 +07:00
4a548e38b5 Dockerfile: forgot to add ttc, also add accountingwep api
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 2m59s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 17:33:25 +07:00
7584e0fb8e Docker: fix path for scripts
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 2m53s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 17:17:17 +07:00
2dee76e1c7 entrypoint: added start api services scripts
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m1s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 15:52:47 +07:00
x2Skyz
02b1a6f31b -แก้ไขระบบ Login Add
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m19s
2025-11-21 15:00:37 +07:00
x2Skyz
2237163847 Merge branch 'master' of http://10.9.0.0/ttc/micro-service-api
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m11s
2025-11-21 12:09:51 +07:00
x2Skyz
f8344e7afc -api เส้น register
-เครื่องมือต่างๆ (ไม่สมบูร)
2025-11-21 12:09:43 +07:00
75c40798c0 entrypoint: also start accountingwep-api
Some checks failed
Build Docker Image / Build Docker Image (push) Has been cancelled
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 12:08:39 +07:00
4cb135d251 workflow: fix path on Dockerfile
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m18s
need to repeat filename/foldername to ADD

Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 11:29:24 +07:00
9751b0ac6e workflow: exec /bin/sh entrypoint (debugging)
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m8s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 11:11:26 +07:00
c99ef8e64d workflow: fix path for executing api entrypoint
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m15s
2025-11-21 11:03:51 +07:00
2dcd432802 workflow: fix permission
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 3m20s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 10:36:41 +07:00
e296f41198 workflow: initial testing
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 2m49s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 10:24:38 +07:00
0d26b67165 -gennumber 2025-11-19 18:38:09 +07:00
e456af98d4 ttc-api: added transactionSearch* and projectSearch, more on reportController.
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-19 18:25:57 +07:00
f1339b22db budgetAddController.js: remove let id
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-19 17:14:14 +07:00
cfcd37be31 projectAdd*: initial working
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-19 17:13:13 +07:00
3ee63ebd7f loginservice: remove "delete user.rol"
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-19 15:17:04 +07:00
21bd8c64ff accountingweb-api and ttc-api: initial with modified .env
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-19 15:06:20 +07:00
a705281ad7 report*: initial (broken) 2025-11-19 08:41:07 +07:00
b48a241c26 -actmst interface
db change
2025-11-18 10:14:30 +07:00
66f2bffabb accounting*: renamed
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-18 10:13:40 +07:00
b662469176 accountingAdd*: initial (working maybe)
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-18 10:12:52 +07:00
f68c856340 budgetExpense*: initial (not working yet)
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-18 09:01:55 +07:00
1a3cf7d9ff projectSearchController.js: use translatebdg for string translation in prjbdgcod
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-18 09:00:24 +07:00
0fea9c08c9 ... 2025-11-17 18:35:05 +07:00
77c7d0574b - เพิ่ม interface prjmst
- เพิ่ม interface trnmst
2025-11-17 18:34:30 +07:00
318 changed files with 2965 additions and 111436 deletions

View File

@@ -0,0 +1,27 @@
name: Build Docker Image
run-name: Build Docker Image
on: [push]
jobs:
Build Docker Image:
runs-on: ubuntu-node
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build docker image
run: |
mv Dockerfile ../
mv entrypoint ../
cd ../
set +e
docker rm $(docker stop $(docker ps -a -q --filter ancestor=accounting-api:latest --format="{{.ID}}"))
set -e
docker image rm -f accounting-api:latest
docker build . -t accounting-api:latest
Restart Docker Compose:
runs-on: host
steps:
- name: Restart compose project
run: |
echo '(cd backend-development-kickstarter && ddd && ddd && ddud)' > /hostpipe

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
node_modules
package-lock.json
/exthernal-rentroom-api
uploads

View File

@@ -0,0 +1,33 @@
-- FUNCTION: dbo.translatebdg(text)
-- DROP FUNCTION IF EXISTS dbo.translatebdg(text);
CREATE OR REPLACE FUNCTION dbo.translatebdg(
p_bdgcod text)
RETURNS text
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
resultName TEXT;
BEGIN
-- ใช้ string_to_array เพื่อแยก text '24,33' เป็น array ['24','33']
-- ใช้ ANY เพื่อหาว่า bdgcod อยู่ใน array นั้นไหม
-- ใช้ string_agg เพื่อรวมชื่อที่ได้กลับมาเป็น text เดียว คั่นด้วย ', '
SELECT string_agg(bdgnam, ', ')
INTO resultName
FROM dbo.bdgmst
WHERE bdgcod = ANY(string_to_array(p_bdgcod, ','));
-- ถ้าหาไม่เจอเลย ให้คืนค่าเดิมกลับไป (Optional: หรือจะคืน NULL ก็ได้)
IF resultName IS NULL THEN
RETURN p_bdgcod;
END IF;
RETURN resultName;
END;
$BODY$;
ALTER FUNCTION dbo.translatebdg(text)
OWNER TO postgres;

View File

@@ -13,10 +13,6 @@ CREATE TABLE IF NOT EXISTS dbo.prjmst
)
-- Table: dbo.prjmst
-- DROP TABLE IF EXISTS dbo.prjmst;
CREATE TABLE IF NOT EXISTS dbo.trnmst
(
trnseq integer NOT NULL, -- เลขที่รายการ หรือ เลข บิล
@@ -33,3 +29,29 @@ TABLESPACE pg_default;
ALTER TABLE IF EXISTS dbo.trnmst
OWNER to postgres;
CREATE OR REPLACE FUNCTION dbo.translatebdg(
p_bdgcod text
)
RETURNS text
LANGUAGE plpgsql
AS $BODY$
DECLARE
resultName TEXT;
BEGIN
SELECT bdgnam
INTO resultName
FROM dbo.bdgmst
WHERE bdgcod = p_bdgcod
LIMIT 1;
RETURN resultName;
END;
$BODY$;
ALTER FUNCTION dbo.translatebdg(text)
OWNER TO postgres;

View File

@@ -0,0 +1,59 @@
-- ⚠️ PostgreSQL ไม่รองรับคำสั่ง AFTER ในการเพิ่ม Column
-- จำเป็นต้องสร้างตารางใหม่เพื่อจัดเรียงลำดับ Column
BEGIN; -- เริ่ม Transaction (ถ้า Error ข้อมูลจะไม่เสียหาย)
-- 1. เปลี่ยนชื่อตารางเดิมเป็น Backup
ALTER TABLE dbo.prjmst RENAME TO prjmst_backup;
-- 2. สร้างตารางใหม่โดยมี prjdoc อยู่ในตำแหน่งที่ต้องการ
CREATE TABLE dbo.prjmst
(
prjseq integer NOT NULL,
prjnam character varying(150) COLLATE pg_catalog."default" NOT NULL,
prjusrseq integer,
prjwntbdg numeric(14,2),
prjacpbdg numeric(14,2),
prjbdgcod character varying(3) COLLATE pg_catalog."default",
prjcomstt character varying(3) COLLATE pg_catalog."default",
-- ✅ แทรก Column ใหม่ตรงนี้
prjdoc character varying(255) COLLATE pg_catalog."default",
prjacpdtm character(12) COLLATE pg_catalog."default",
CONSTRAINT prjmst_pkey PRIMARY KEY (prjseq, prjnam)
)
TABLESPACE pg_default;
-- 3. กำหนด Owner (ถ้าจำเป็น)
ALTER TABLE dbo.prjmst OWNER to postgres;
-- 4. ย้ายข้อมูลจากตาราง Backup มาใส่ตารางใหม่ (Map ข้อมูลให้ตรง Column)
INSERT INTO dbo.prjmst (
prjseq,
prjnam,
prjusrseq,
prjwntbdg,
prjacpbdg,
prjbdgcod,
prjcomstt,
prjacpdtm
-- prjdoc จะเป็น NULL อัตโนมัติสำหรับข้อมูลเก่า
)
SELECT
prjseq,
prjnam,
prjusrseq,
prjwntbdg,
prjacpbdg,
prjbdgcod,
prjcomstt,
prjacpdtm
FROM prjmst_backup;
-- 5. ยืนยันการทำงาน
COMMIT;
-- หมายเหตุ: หลังจากตรวจสอบข้อมูลแล้ว สามารถลบตาราง Backup ได้ด้วยคำสั่ง:
-- DROP TABLE dbo.prjmst_backup;

View File

@@ -0,0 +1,63 @@
-- ⚠️ IMPORTANT: คำสั่ง ROLLBACK จะช่วยเคลียร์สถานะ "current transaction is aborted"
ROLLBACK;
BEGIN;
-- 1. ส่วนจัดการเปลี่ยนชื่อตารางและ Key (แบบปลอดภัย เช็คก่อนทำ)
DO $$
BEGIN
-- เช็ค: ถ้ามีตาราง 'prjmst' อยู่ และยังไม่มี 'prjmst_backup' ให้ทำการเปลี่ยนชื่อ (กรณีรันครั้งแรก)
IF EXISTS (SELECT FROM pg_tables WHERE schemaname = 'dbo' AND tablename = 'prjmst') THEN
IF NOT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'dbo' AND tablename = 'prjmst_backup') THEN
ALTER TABLE dbo.prjmst RENAME TO prjmst_backup;
END IF;
END IF;
-- เช็ค: แก้ชื่อ Primary Key ในตาราง Backup ถ้ามันยังชื่อเดิม (แก้ปัญหา duplicate key name)
IF EXISTS (
SELECT 1 FROM pg_constraint con
JOIN pg_class rel ON rel.oid = con.conrelid
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
WHERE nsp.nspname = 'dbo' AND rel.relname = 'prjmst_backup' AND con.conname = 'prjmst_pkey'
) THEN
ALTER TABLE dbo.prjmst_backup RENAME CONSTRAINT prjmst_pkey TO prjmst_backup_pkey;
END IF;
END $$;
-- 2. สร้างตารางใหม่ (New Structure)
-- ใช้ IF NOT EXISTS กัน Error ถ้ารันซ้ำ
CREATE TABLE IF NOT EXISTS dbo.prjmst
(
prjseq integer NOT NULL,
prjnam character varying(150) COLLATE pg_catalog."default" NOT NULL,
prjusrseq integer,
prjwntbdg numeric(14,2),
prjacpbdg numeric(14,2),
prjbdgcod character varying(3) COLLATE pg_catalog."default",
prjcomstt character varying(3) COLLATE pg_catalog."default",
-- ✅ แทรก Column ใหม่ตรงนี้
prjdoc character varying(255) COLLATE pg_catalog."default",
prjacpdtm character(12) COLLATE pg_catalog."default",
CONSTRAINT prjmst_pkey PRIMARY KEY (prjseq, prjnam)
)
TABLESPACE pg_default;
ALTER TABLE dbo.prjmst OWNER to postgres;
-- 3. ย้ายข้อมูลกลับมา (Data Migration)
-- เช็คก่อนว่าตารางใหม่ว่างอยู่ไหม ค่อย Insert
INSERT INTO dbo.prjmst (
prjseq, prjnam, prjusrseq, prjwntbdg, prjacpbdg, prjbdgcod, prjcomstt, prjacpdtm
)
SELECT
prjseq, prjnam, prjusrseq, prjwntbdg, prjacpbdg, prjbdgcod, prjcomstt, prjacpdtm
FROM dbo.prjmst_backup
WHERE NOT EXISTS (SELECT 1 FROM dbo.prjmst);
COMMIT;
-- หมายเหตุ: เมื่อตรวจสอบข้อมูลครบถ้วนแล้ว สามารถสั่งลบตาราง backup ได้:
-- DROP TABLE dbo.prjmst_backup;

View File

@@ -0,0 +1,23 @@
BEGIN;
-- 1. ย้ายตารางเก่าหลบไปก่อน (Backup ไว้ในตัว)
ALTER TABLE users RENAME TO users_old;
-- 2. สร้างตารางใหม่ (ด้วยโครงสร้างที่ต้องการแก้ไข)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
-- ใส่ Column ใหม่ หรือแก้ไข Type ตรงนี้
new_column_data JSONB
);
-- 3. ย้ายข้อมูลจากตารางเก่ามาใส่ตารางใหม่
INSERT INTO users (id, username, new_column_data)
SELECT id, username, CAST(old_data AS JSONB) -- แปลงข้อมูลระหว่างทางได้เลย
FROM users_old;
-- 4. สร้าง Index และ Constraint ให้ครบ (สำคัญมาก!)
CREATE INDEX idx_users_username ON users(username);
-- อย่าลืม Add Foreign Key หรือ Grant Permission ให้ User อื่นด้วย
COMMIT;

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM alpine:latest
ADD entrypoint /entrypoint
RUN chmod +x ./entrypoint
RUN apk update && apk add npm
ADD micro-service-api /server
RUN mv /server/start-accountingwep.sh /
RUN mv /server/start-login.sh /
RUN mv /server/start-ttc.sh /
RUN chmod +x /start-accountingwep.sh
RUN chmod +x /start-login.sh
RUN chmod +x /start-ttc.sh
RUN cd /server && npm install
ENTRYPOINT ["/entrypoint"]

4
entrypoint Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
./start-login.sh &
./start-ttc.sh &
exec ./start-accountingwep.sh

View File

@@ -2,9 +2,9 @@
PJ_NAME=exthernal-wepaccounting-api
# database
PG_HOST=localhost
PG_HOST=10.9.0.0
PG_USER=postgres
PG_PASS=123456
PG_PASS=ttc@2026
PG_DB=ttc
PG_PORT=5432
@@ -13,7 +13,7 @@ SMTP_USER=lalisakuty@gmail.com
SMTP_PASS=lurl pckw qugk tzob
# REDIS
REDIS_HOST=127.0.0.1
REDIS_HOST=10.9.0.0
REDIS_PORT=6379
OTP_TTL_SECONDS=300

View File

@@ -0,0 +1,66 @@
import { AccountingAddService } from '../services/accountingAddService.js'
import { sendError } from '../utils/response.js'
// import { OftenError } from '../utils/oftenError.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
import { Interface } from '../interfaces/Interface.js';
export class accountingAdd {
constructor() {
this.generalService = new GeneralService();
this.accountingAddService = new AccountingAddService();
this.Interface = new Interface();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'accountingAdd.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onAccountingAdd(req, res, organization);
return prommis;
}
async onAccountingAdd(req, res, database) {
let idx = -1
let aryResult = []
let latSeq = []
try {
let token = req.headers.authorization?.split(' ')[1];
var decoded = verifyToken(token);
let actnum = req.body.request.actnum;
database = decoded.organization;
aryResult = await this.accountingAddService.getAccountingAdd(database, actnum);
latSeq = await this.accountingAddService.genNum(database);
} catch (error) {
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
// if (!aryResult) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
if (aryResult == 0) {
let prommis = await this.makeArySave(req, latSeq, decoded.id);
return prommis
} else {
return sendError('คีย์หลักซ้ำในระบบ', 'Duplicate Primary Key');
}
}
}
async makeArySave(req, seq, actnum) {
let arysave = {
methods: 'post',
actseq: seq,
actnum: actnum,
actacpdtm: req.body.request.actacpdtm,
actcat: req.body.request.actcat,
actqty: req.body.request.actqty,
actcmt: req.body.request.actcmt,
acttyp: req.body.request.acttyp
}
return this.Interface.saveInterface('actmst', arysave, req);
}
}

View File

@@ -25,7 +25,7 @@ export class accountingSearch {
try {
// let username = req.body.request.username;
// let password = req.body.request.password;
let token = req.body.request.token;
let token = req.headers.authorization?.split(' ')[1];''
const decoded = verifyToken(token);
let id = decoded.id

View File

@@ -25,7 +25,7 @@ export class accountingSetup {
try {
// let username = req.body.request.username;
// let password = req.body.request.password;
let token = req.body.request.token;
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
database = decoded.organization

View File

@@ -2,7 +2,8 @@ import { AccountingSumService } from '../services/accountingSumService.js'
import { sendError } from '../utils/response.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
import { verifyToken,
generateToken } from '../utils/token.js'
export class accountingSum {
@@ -23,7 +24,7 @@ export class accountingSum {
let result = []
var aryResult
try {
let token = req.body.request.token;
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
let id = decoded.id
@@ -44,7 +45,7 @@ export class accountingSum {
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
try {
// 1) เตรียม data สำหรับใช้คำนวณ
// 1) เตรียม data สำหรับใช้คำนวณ
// ถ้า service คืนมาเป็น { code, message, data: [...] }
const data = Array.isArray(result)
? result
@@ -57,7 +58,7 @@ export class accountingSum {
return result;
}
// 2) แยก income / expense
// 2) แยก income / expense
const incomeList = data.filter(i => i.acttyp === 'i');
const expenseList = data.filter(e => e.acttyp === 'e');
@@ -68,7 +69,7 @@ export class accountingSum {
const profitRate = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0;
const adjustedProfitRate = profitRate + 1.9;
// 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า)
// 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า)
var summary = {
totalIncome: totalIncome.toFixed(2),
totalExpense: totalExpense.toFixed(2),
@@ -78,14 +79,14 @@ export class accountingSum {
period: '30 วัน'
};
// 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม)
// 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม)
// ตัวอย่างสมมติ: คุณไป query มาจาก service ก่อนหน้าแล้วได้เป็น object แบบนี้
// key = ชื่อหมวด (actcatnam หรือ code), value = color
const categoryColorMap = await this.accountingSumService.getCategoryColorMap(database);
// ตัวอย่างที่คาดหวังจาก service:
// { 'ค่าอาหาร': '#FF6384', 'ค่าเดินทาง': '#36A2EB', 'ขายสินค้า': '#4BC0C0', ... }
// 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense
// 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense
const expenseAgg = {};
expenseList.forEach(row => {
const key = row.actcatnam; // หรือใช้รหัส category ถ้ามี เช่น row.actcatcod
@@ -122,7 +123,7 @@ export class accountingSum {
};
});
// 6) แนบข้อมูล pie chart เข้า result
// 6) แนบข้อมูล pie chart เข้า result
var pie = {
expense: expensePie,
income: incomePie

View File

@@ -0,0 +1,146 @@
import { ReportService } from '../services/reportService.js'
import { sendError } from '../utils/response.js'
// import { OftenError } from '../utils/oftenError.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
import { Interface } from '../interfaces/Interface.js';
export class reportController {
constructor() {
this.generalService = new GeneralService();
this.reportService = new ReportService();
this.Interface = new Interface();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'reportController.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onReportController(req, res, organization);
return prommis;
}
async onReportController(req, res, database) {
let idx = -1
let aryResult = []
try {
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
let actnum = decoded.id;
database = decoded.organization;
aryResult = await this.reportService.getReportController(database, actnum);
} catch (error) {
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!aryResult) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
try {
// ✅ 1) เตรียม data สำหรับใช้คำนวณ
// ถ้า service คืนมาเป็น { code, message, data: [...] }
const data = Array.isArray(aryResult)
? aryResult
: Array.isArray(aryResult.data)
? aryResult.data
: [];
// ถ้าไม่มีข้อมูลก็ไม่ต้องคำนวณ
if (!data.length) {
return aryResult;
}
// 2) แยก income / expense
const incomeList = data.filter(i => i.acttyp === 'i');
const expenseList = data.filter(e => e.acttyp === 'e');
const totalIncome = incomeList.reduce((sum, i) => sum + parseFloat(i.actqty || 0), 0);
const totalExpense = expenseList.reduce((sum, e) => sum + parseFloat(e.actqty || 0), 0);
const netProfit = totalIncome - totalExpense;
const profitRate = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0;
const adjustedProfitRate = profitRate + 1.9;
// 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า)
var summary = {
totalIncome: totalIncome.toFixed(2),
totalExpense: totalExpense.toFixed(2),
netProfit: netProfit.toFixed(2),
profitRate: profitRate.toFixed(2) + ' %',
period: '30 วัน'
};
// ✅ 3.5) Create actdata table with required fields grouped by actnum
var actdata = data.map(row => ({
actnam: row.actnam,
actcat: row.actcat,
actqty: row.actqty,
actcmt: row.actcmt,
actacpdtm: row.actacpdtm
}));
// ✅ 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม)
// ตัวอย่างสมมติ: คุณไป query มาจาก service ก่อนหน้าแล้วได้เป็น object แบบนี้
// key = ชื่อหมวด (actcatnam หรือ code), value = color
const categoryColorMap = await this.reportService.getCategoryColorMap(database);
// ตัวอย่างที่คาดหวังจาก service:
// { 'ค่าอาหาร': '#FF6384', 'ค่าเดินทาง': '#36A2EB', 'ขายสินค้า': '#4BC0C0', ... }
// ✅ 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense
const expenseAgg = {};
expenseList.forEach(row => {
const key = row.actcat; // หรือใช้รหัส category ถ้ามี เช่น row.actcatcod
const amount = parseFloat(row.actqty || 0);
expenseAgg[key] = (expenseAgg[key] || 0) + amount;
});
const incomeAgg = {};
incomeList.forEach(row => {
const key = row.actcat;
const amount = parseFloat(row.actqty || 0);
incomeAgg[key] = (incomeAgg[key] || 0) + amount;
});
const expensePie = Object.entries(expenseAgg).map(([cat, value]) => {
const percent = totalExpense > 0 ? (value / totalExpense) * 100 : 0;
const color = categoryColorMap[cat] || '#CCCCCC'; // fallback สี default
return {
label: cat,
value: value.toFixed(2),
percent: percent.toFixed(2),
color
};
});
const incomePie = Object.entries(incomeAgg).map(([cat, value]) => {
const percent = totalIncome > 0 ? (value / totalIncome) * 100 : 0;
const color = categoryColorMap[cat] || '#CCCCCC';
return {
label: cat,
value: value.toFixed(2),
percent: percent.toFixed(2),
color
};
});
// ✅ 6) แนบข้อมูล pie chart เข้า aryResult
var pie = {
expense: expensePie,
income: incomePie
};
} catch (err) {
console.error('calculate summary/pie error:', err);
}
let arydiy = {
actdata,
summary,
pie,
}
return arydiy;
}
}
}

View File

@@ -1,27 +1,34 @@
import jwt from 'jsonwebtoken'
import { BdgmstInterface } from './table/bdgmstInterface.js'
import { ActmstInterface } from './table/actmstInterface.js'
import { sendError } from '../utils/response.js'
// import { ActmstInterface } from './actmstInterface.js'
// -------------------------------
// GLOBAL FILE
// -----------------------------
export class Interface {
constructor() {
this.map = {
bdgmst: new BdgmstInterface(),
// actmst: new ActmstInterface(),
actmst: new ActmstInterface(),
}
}
// ===============================================================
// 📌 saveInterface → แกะ token เอง และ route ไปยัง interface เฉพาะ table
// saveInterface → แกะ token เอง และ route ไปยัง interface เฉพาะ table
// ===============================================================
async saveInterface(tableName, req, data) {
async saveInterface(tableName, data, req) {
// ------------------------------
// ✔ 1) จับ Interface ที่ตรงกับ table
// ------------------------------
const handler = this.map[tableName.toLowerCase()]
if (!handler) {
throw new Error(`Interface not found for table: ${tableName}`)
return new sendError(`Interface not found for table: ${tableName}`)
}
// ------------------------------
@@ -29,18 +36,18 @@ export class Interface {
// ------------------------------
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
throw new Error('Missing token in request header')
return new sendError('ไม่พบการยืนยันตัวตน' ,'Missing token in request header')
}
let decoded
try {
decoded = jwt.verify(token, process.env.JWT_SECRET)
} catch (err) {
throw new Error('Invalid token: ' + err.message)
return new sendError('Invalid token: ' + err.message)
}
const schema = decoded.organization // ⭐ ได้ schema ที่ต้องการ
if (!schema) throw new Error("Token missing 'organization' field")
const schema = decoded.organization
if (!schema) return new sendError("Token missing 'organization' field")
// ------------------------------
// ✔ 3) ส่งงานไปยัง interface ของ table นั้น ๆ

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class ActmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'actmst'
this.pk = ['actseq', 'actnum'] // ⭐ PK ตาม CREATE TABLE
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -1,7 +1,9 @@
import express from 'express'
import { accountingSetup } from '../controllers/accountingSetup.js'
import { accountingSearch } from '../controllers/accountingSearch.js'
import { accountingSum } from '../controllers/accountingSum.js'
import { accountingSetup } from '../controllers/accountingSetupController.js'
import { accountingSearch } from '../controllers/accountingSearchController.js'
import { accountingSum } from '../controllers/accountingSumController.js'
import { accountingAdd } from '../controllers/accountingAddController.js'
import { reportController } from '../controllers/reportController.js'
// import { authMiddleware } from '../middlewares/auth.js'
// import { sendResponse } from '../utils/response.js'
@@ -10,24 +12,34 @@ const router = express.Router()
const controller_accountingSetup_post = new accountingSetup()
const controller_accountingSearch_post = new accountingSearch()
const controller_accountingSum_post = new accountingSum()
const controller_accountingAdd_post = new accountingAdd()
const controller_report_post = new reportController()
router.post('/accountingSetup', async (req, res) => {
router.post('/accountingsetup', async (req, res) => {
const result = await controller_accountingSetup_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/accountingSearch', async (req, res) => {
router.post('/accountingsearch', async (req, res) => {
const result = await controller_accountingSearch_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/accountingSum', async (req, res) => {
router.post('/accountingsum', async (req, res) => {
const result = await controller_accountingSum_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/accountingadd', async (req, res) => {
const result = await controller_accountingAdd_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/report', async (req, res) => {
const result = await controller_report_post.onNavigate(req, res)
if (result) return res.json(result)
})
// // ===================================================
// // 🔹 BIOMETRIC LOGIN

View File

@@ -0,0 +1,49 @@
import { GeneralService } from '../share/generalservice.js'
export class AccountingAddService {
constructor() {
this.generalService = new GeneralService()
}
async getAccountingAdd(database, number) {
const sql = `
SELECT
actseq,
actnum
FROM ${database}.actmst
WHERE actnum = $1
`
const params = [number]
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
// async getLatestAccountingSeq(database) {
// const sql = `
// SELECT
// actseq
// FROM ${database}.actmst
// WHERE actseq=(SELECT max(actseq) FROM ${database}.actmst)
// `
// const params = []
// const result = await this.generalService.executeQueryParam(database, sql, params);
// return result
// }
async genNum(database) {
const sql = `
SELECT
MAX(actseq) as max_seq
FROM ${database}.actmst
`
const params = []
const aryResult = await this.generalService.executeQueryParam(database, sql, params);
const lastSeq = aryResult[0]?.max_seq || 0;
return lastSeq + 1;
}
}

View File

@@ -23,7 +23,6 @@ export class AccountingSumService {
return result
}
async getCategoryColorMap(database) {
const sql = `
SELECT dtlcod, dtlnam, dtlmsc as dtlclr

View File

@@ -0,0 +1,43 @@
import { GeneralService } from '../share/generalservice.js'
export class ReportService {
constructor() {
this.generalService = new GeneralService()
}
async getReportController(database, number) {
const sql = `
SELECT
${database}.translatedtl('ACTTYP', acttyp) AS actnam,
acttyp,
${database}.translatedtl_multi(ARRAY['ACTCAT_INC','ACTCAT_EXP'], actcat) AS actcat,
actqty,
actcmt,
actacpdtm
FROM ${database}.actmst
WHERE actnum = $1
`
const params = [number]
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
async getCategoryColorMap(database) {
const sql = `
SELECT dtlcod, dtlnam, dtlmsc as dtlclr
FROM ${database}.dtlmst
WHERE dtltblcod IN ('ACTCAT_INC', 'ACTCAT_EXP')
`;
const params = []
const rows = await this.generalService.executeQueryParam(database, sql, params);
const map = {};
rows.forEach(r => {
map[r.dtlnam] = r.dtlclr;
});
return map;
}
}

View File

@@ -54,7 +54,9 @@ export class GeneralService {
for (const [key, value] of Object.entries(conditions)) {
if (value === undefined || value === null || value === '') continue
const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i)
if (match) {
const operator = match[1].toUpperCase()
const pattern = match[2].trim()
@@ -69,6 +71,7 @@ export class GeneralService {
let finalQuery = baseQuery
if (whereClauses.length > 0) finalQuery += ' AND ' + whereClauses.join(' AND ')
const formattedSQL = finalQuery.replace(/\${database}/g, database)
try {
@@ -77,15 +80,20 @@ export class GeneralService {
sql: formattedSQL,
params
})
const result = await connection.query(formattedSQL, params)
this.devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
} catch (err) {
this.devhint(1, 'executeQueryConditions', `❌ SQL Error`, err.message)
console.error('🧨 SQL Error:', err.message)
throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) // ✅ “เจ๊งจริง” ส่งถึง controller แน่นอน
throw new Error(`SQL_EXECUTION_FAILED::${err.message}`)
}
}
}
// ===================================================
// Export สำหรับ controller หรืออื่นๆ เรียกใช้ได้ด้วย

View File

@@ -1,10 +1,10 @@
#project
PJ_NAME=exthernal-mobile-api
PJ_NAME=exthernal-login-api
# database
PG_HOST=localhost
PG_HOST=10.9.0.0
PG_USER=postgres
PG_PASS=123456
PG_PASS=ttc@2026
PG_DB=ttc
PG_PORT=5432
@@ -13,7 +13,7 @@ SMTP_USER=lalisakuty@gmail.com
SMTP_PASS=lurl pckw qugk tzob
# REDIS
REDIS_HOST=127.0.0.1
REDIS_HOST=10.9.0.0
REDIS_PORT=6379
OTP_TTL_SECONDS=300

View File

@@ -25,7 +25,7 @@ app.use((err, req, res, next) => {
next()
})
app.use('/api', router)
app.use('/api/login', router)
app.listen(process.env.PORT, () => {
console.log(`${process.env.PJ_NAME} running on port ${process.env.PORT}`)

View File

@@ -33,35 +33,7 @@ export class loginController {
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
if(result) { return result }
return null
return result
}
}
// async onBiometricLogin(req, res) {
// try {
// const result = await this.loginService.loginWithBiometric(organization, biometric_id)
// } catch (error) {
// idx = 1
// } finally { // สำคัญมากต้อง จดจำไม่มีดัดแปลง อัปเดทเลย เรื่อง idx
// if(idx == 1){ return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error') }
// }
// return { result, timestamp: new Date().toISOString() }
// }
// async onBiometricRegister(req, res) {
// const { organization, request } = req.body || {}
// const { biometric_id } = request || {}
// const userId = req.user.id
// const result = await this.loginService.registerBiometric(organization, userId, biometric_id)
// return { result, timestamp: new Date().toISOString() }
// }
// async onRenewToken(req, res) {
// const user = req.user
// const newToken = generateToken({ id: user.id, name: user.name })
// return { token: newToken, renewed_at: new Date().toISOString() }
// }
}

View File

@@ -1,26 +1,76 @@
import { RegisterService } from '../services/registerservice.js';
import { sendError } from '../utils/response.js';
import { RegisterService } from '../services/registerservice.js'
import { sendError } from '../utils/response.js'
import { GeneralService } from '../share/generalservice.js';
import bcrypt from 'bcrypt'
import { Interface } from '../interfaces/Interface.js';
import { validateSave } from '../utils/validate.js'; // import เข้ามา
export class RegisterController {
constructor() {
this.registerService = new RegisterService();
this.generalService = new GeneralService();
this.registerService = new RegisterService();
this.Interface = new Interface();
}
async onNavigate(req, res) {
let idx = -1, result = [];
this.generalService.devhint(1, 'registercontroller.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onRegisterController(req, res, organization);
return prommis;
}
async onRegisterController(req, res, database) {
let idx = -1
let result = []
let aryUserDuplicate = [];
try {
const { organization, request } = req.body;
const { email, fname, lname, password } = request;
result = await this.registerService.requestRegistration(organization, email, fname, lname, password);
// 1. ดึง Sequence ล่าสุดจาก Service (เพื่อเอามา +1)
var Seq = await this.registerService.genNum(database);
// 2. Hash Password
let passwordRaw = req.body.request.password;
const saltRounds = 10;
var hashedPassword = await bcrypt.hash(passwordRaw, saltRounds);
// 3. เรียก makeArySave เพื่อเตรียมข้อมูลและบันทึกผ่าน saveInterface
// ส่ง nextSeq และ hashedPassword เข้าไป
aryUserDuplicate = await this.registerService.checkUserDuplicate(database, req.body.request.email);
this.generalService.devhint(1, 'registercontroller.js', 'Register success');
} catch (error) {
idx = 1;
this.generalService.devhint(1, 'registercontroller.js', 'Jumpout', error.message);
result = error; // จะถูก Global Handler จัด format
} finally {
if (idx === 1) return result;
return result;
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (aryUserDuplicate.length > 0) return sendError('มีชื่อผู้ใช้หรืออีเมลนี้ในระบบแล้ว', 'Username or Email already exists');
result = await this.makeArySave(req, Seq, hashedPassword);
if (!result) return sendError('ไม่สามารถลงทะเบียนได้', 'Registration failed');
return result
}
}
async makeArySave(req, seq, hashedPassword) {
// Map ข้อมูลเข้า Field ตามตาราง usrmst
let arysave = {
methods: 'post', // สั่งให้ saveInterface ทำการ INSERT
usrseq: seq, // PK: integer
usrnam: validateSave(req.body.request.username, 'username'),
usrpwd: hashedPassword, // character varying(255)
usreml: req.body.request.email, // character varying(50)
usrthinam: validateSave(req.body.request.firstname, 'firstname'), // character varying(100)
usrthilstnam: validateSave(req.body.request.lastname, 'lastname'), // character varying(100)
// Field อื่นๆ ที่อาจต้อง Default ค่าไว้ก่อน (ตาม Schema)
usrrol: 'U', // Default User Role
adpdte: '', // Approved Date (รออนุมัติ)
expdte: '', // Expire Date
lstlgn: '', // Last Login
usrorg: req.body.request.organization, // ถ้ามี field นี้
}
// เรียก saveInterface ผ่าน generalService (ระบุชื่อตาราง 'usrmst')
return this.Interface.saveInterface('usrmst', arysave, req);
}
}

View File

@@ -0,0 +1,70 @@
import jwt from 'jsonwebtoken'
import { BdgmstInterface } from './table/bdgmstInterface.js'
import { ActmstInterface } from './table/actmstInterface.js'
import { UsrmstInterface } from './table/usrmstInterface.js'
import { sendError } from '../utils/response.js'
// -------------------------------
// GLOBAL FILE
// -----------------------------
export class Interface {
constructor() {
this.map = {
bdgmst: new BdgmstInterface(),
actmst: new ActmstInterface(),
usrmst: new UsrmstInterface(),
}
}
// ===============================================================
// saveInterface → แกะ token เอง และ route ไปยัง interface เฉพาะ table
// ===============================================================
async saveInterface(tableName, data, req) {
// ------------------------------
// 1) จับ Interface ที่ตรงกับ table
// ------------------------------
const handler = this.map[tableName.toLowerCase()]
if (!handler) {
return new sendError(`Interface not found for table: ${tableName}`)
}
let schema;
// ------------------------------
// 2) ตรวจสอบเงื่อนไข (Exception for usrmst)
// ------------------------------
if (tableName.toLowerCase() === 'usrmst') {
// กรณี usrmst (เช่น Register/Login) ไม่บังคับ Token
// เราต้องกำหนด schema เอง เพราะไม่มี token ให้แกะ
schema = 'nuttakit'
} else {
// ------------------------------
// 3) ตารางอื่น ๆ บังคับ Token ตามปกติ
// ------------------------------
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return new sendError('ไม่พบการยืนยันตัวตน' ,'Missing token in request header')
}
let decoded
try {
decoded = jwt.verify(token, process.env.JWT_SECRET)
} catch (err) {
return new sendError('Invalid token: ' + err.message)
}
schema = decoded.organization
}
if (!schema) return new sendError("Token missing 'organization' field or Schema undefined")
// ------------------------------
// ✔ 4) ส่งงานไปยัง interface ของ table นั้น ๆ
// ------------------------------
return await handler.saveInterface(schema, data)
}
}

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class ActmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'actmst'
this.pk = ['actseq', 'actnum'] // ⭐ PK ตาม CREATE TABLE
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class BdgmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'bdgmst'
this.pk = ['bdgseq']
}
async saveInterface(database, data) {
const method = data.method.toLowerCase()
const payload = { ...data }
delete payload.method
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class UsrmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'usrmst'
this.pk = ['usrseq'] // ⭐ PK ตาม CREATE TABLE
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -18,18 +18,18 @@ const controller_login_post = new loginController()
const otpController = new OtpController()
const otpVerifyController = new OtpVerifyController()
router.post('/login/login', async (req, res) => {const result = await controller_login_post.onNavigate(req, res); return res.json(result)})
router.post('/login', async (req, res) => {const result = await controller_login_post.onNavigate(req, res); return res.json(result)})
router.post('/login/otp/send', async (req, res) => {
router.post('/otp/send', async (req, res) => {
const result = await otpController.onSendOtp(req, res)
if (result) return res.json(result)
})
router.post('/login/register', async (req, res) => {
router.post('/register', async (req, res) => {
const result = await registerController.onNavigate(req, res);
if (result) return res.json(result);
});
router.get('/login/verify-email', async (req, res) => {
router.get('/verify-email', async (req, res) => {
const result = await verifyEmailController.onVerifyEmail(req, res);
if (result?.code && result.code !== '200') return res.status(400).send(result.message_th);
// ถ้า controller ส่ง HTML string กลับมา → render ตรง ๆ
@@ -38,13 +38,13 @@ router.get('/login/verify-email', async (req, res) => {
});
router.post('/login/reset-password', async (req, res) => {
router.post('/reset-password', async (req, res) => {
const result = await resetPasswordController.onNavigate(req, res);
if (result) return res.json(result);
});
router.post('/login/otp/verify', async (req, res) => {
router.post('/otp/verify', async (req, res) => {
const result = await otpVerifyController.onNavigate(req, res)
if (result) return res.json(result)
})

View File

@@ -15,7 +15,7 @@ export class LoginService {
let sql = `
SELECT usrseq, usrnam, usrorg, usrrol, usrpwd, usrthinam, usrthilstnam
FROM nuttakit.usrmst
WHERE usrnam = $1
WHERE usrnam = $1 OR (usreml = $1)
`
let params = [username]
const rows = await this.generalService.executeQueryParam(database, sql, params)
@@ -44,9 +44,8 @@ export class LoginService {
this.generalService.devhint(2, 'loginservice.js', 'token generated successfully')
delete user.usrseq
// delete user.usrseq
delete user.usrnam
delete user.usrrol
delete user.usrpwd
delete user.usrorg
return {

View File

@@ -0,0 +1,88 @@
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import nodemailer from 'nodemailer';
import { GeneralService } from '../share/generalservice.js';
import { sendError } from '../utils/response.js';
import redis from '../utils/redis.js';
export class RegisterService {
constructor() {
// this.redis = new Redis();
this.generalService = new GeneralService();
}
async requestRegistration(database, email, fname, lname, password) {
let result = [];
try {
let sql = `
SELECT usrseq FROM ${database}.usrmst WHERE usrnam = $1
`;
let param = [email];
const userCheck = await this.generalService.executeQueryParam(database, sql, param);
if (userCheck.length > 0) {
this.generalService.devhint(1, 'registerservice.js', `❌ Duplicate email (${email})`);
throw sendError('อีเมลนี้ถูกใช้แล้ว', 'Email already registered');
}
const hashedPwd = await bcrypt.hash(password, 10);
const token = crypto.randomBytes(32).toString('hex');
const payload = JSON.stringify({ fname, lname, hashedPwd, token, database });
await redis.set(`verify:${email}`, payload, 'EX', 86400); // 24h
const verifyUrl = `http://localhost:1012/login/verify-email?token=${token}&email=${encodeURIComponent(email)}&organization=${database}`;
await this.sendVerifyEmail(email, verifyUrl);
this.generalService.devhint(2, 'registerservice.js', `✅ Verify link sent to ${email}`);
result = {
code: '200',
message_th: 'ส่งลิงก์ยืนยันอีเมลแล้ว',
data: {}
};
} catch (error) {
this.generalService.devhint(1, 'registerservice.js', '❌ Registration Error', error.message);
throw error;
}
return result;
}
async sendVerifyEmail(email, verifyUrl) {
try {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const html = `
<div style="font-family: sans-serif;">
<h2>ยืนยันการสมัครสมาชิก</h2>
<p>กรุณากดยืนยันภายใน 24 ชั่วโมง เพื่อเปิดใช้งานบัญชีของคุณ</p>
<a href="${verifyUrl}"
style="display:inline-block;background:#0078d4;color:white;
padding:10px 20px;text-decoration:none;border-radius:5px;">ยืนยันอีเมล</a>
<p style="margin-top:16px;font-size:13px;color:#555;">หากคุณไม่ได้สมัคร โปรดละเว้นอีเมลนี้</p>
</div>
`;
await transporter.sendMail({
from: `"System" <${process.env.SMTP_USER}>`,
to: email,
subject: '📩 ยืนยันอีเมลสำหรับสมัครสมาชิก',
html,
});
this.generalService.devhint(2, 'registerservice.js', `📤 Verification email sent (${email})`);
} catch (error) {
this.generalService.devhint(1, 'registerservice.js', '❌ Email Send Failed', error.message);
throw sendError('ไม่สามารถส่งอีเมลได้', 'Email send failed');
}
}
}

View File

@@ -1,88 +1,66 @@
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import nodemailer from 'nodemailer';
import { GeneralService } from '../share/generalservice.js';
import { sendError } from '../utils/response.js';
import redis from '../utils/redis.js';
import { GeneralService } from '../share/generalservice.js'
import bcrypt from 'bcrypt'
export class RegisterService {
constructor() {
// this.redis = new Redis();
this.generalService = new GeneralService();
this.generalService = new GeneralService()
}
async requestRegistration(database, email, fname, lname, password) {
let result = [];
async createUser(database, userData) {
// 1. ทำการ Hash Password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userData.password, saltRounds);
// 2. เตรียม SQL
const sql = `
INSERT INTO ${database}.usrmst
(username, password, email, firstname, lastname, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
`
// 3. ใช้ hashedPassword แทน password เดิม
const params = [
userData.username,
hashedPassword, // ส่งตัวที่ Hash แล้วเข้า DB
userData.email,
userData.firstname,
userData.lastname
]
try {
let sql = `
SELECT usrseq FROM ${database}.usrmst WHERE usrnam = $1
`;
let param = [email];
const userCheck = await this.generalService.executeQueryParam(database, sql, param);
const result = await this.generalService.executeQueryParam(database, sql, params);
if (userCheck.length > 0) {
this.generalService.devhint(1, 'registerservice.js', `❌ Duplicate email (${email})`);
throw sendError('อีเมลนี้ถูกใช้แล้ว', 'Email already registered');
}
const hashedPwd = await bcrypt.hash(password, 10);
const token = crypto.randomBytes(32).toString('hex');
const payload = JSON.stringify({ fname, lname, hashedPwd, token, database });
await redis.set(`verify:${email}`, payload, 'EX', 86400); // 24h
const verifyUrl = `http://localhost:1012/login/verify-email?token=${token}&email=${encodeURIComponent(email)}&organization=${database}`;
await this.sendVerifyEmail(email, verifyUrl);
this.generalService.devhint(2, 'registerservice.js', `✅ Verify link sent to ${email}`);
result = {
code: '200',
message_th: 'ส่งลิงก์ยืนยันอีเมลแล้ว',
data: {}
};
// เช็คผลลัพธ์ตาม Structure ของ GeneralService
// สมมติว่าถ้า Error ตัว executeQueryParam อาจจะ throw หรือ return null
return { status: true, message: 'Registration successful' };
} catch (error) {
this.generalService.devhint(1, 'registerservice.js', '❌ Registration Error', error.message);
throw error;
console.error('Register Service Error:', error);
return null;
}
return result;
}
async sendVerifyEmail(email, verifyUrl) {
try {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
async checkUserDuplicate(database, email) {
const sql = `
SELECT * FROM nuttakit.usrmst
WHERE usreml = $1
`
const params = [email]
const aryResult = await this.generalService.executeQueryParam(database, sql, params);
return aryResult;
}
const html = `
<div style="font-family: sans-serif;">
<h2>ยืนยันการสมัครสมาชิก</h2>
<p>กรุณากดยืนยันภายใน 24 ชั่วโมง เพื่อเปิดใช้งานบัญชีของคุณ</p>
<a href="${verifyUrl}"
style="display:inline-block;background:#0078d4;color:white;
padding:10px 20px;text-decoration:none;border-radius:5px;">ยืนยันอีเมล</a>
<p style="margin-top:16px;font-size:13px;color:#555;">หากคุณไม่ได้สมัคร โปรดละเว้นอีเมลนี้</p>
</div>
`;
async genNum(database) {
const sql = `
SELECT
MAX(usrseq) as max_seq
FROM nuttakit.usrmst
`
const params = []
const aryResult = await this.generalService.executeQueryParam(database, sql, params);
await transporter.sendMail({
from: `"System" <${process.env.SMTP_USER}>`,
to: email,
subject: '📩 ยืนยันอีเมลสำหรับสมัครสมาชิก',
html,
});
const lastSeq = aryResult[0]?.max_seq || 0;
this.generalService.devhint(2, 'registerservice.js', `📤 Verification email sent (${email})`);
} catch (error) {
this.generalService.devhint(1, 'registerservice.js', '❌ Email Send Failed', error.message);
throw sendError('ไม่สามารถส่งอีเมลได้', 'Email send failed');
}
return lastSeq + 1;
}
}

View File

@@ -1,19 +1,13 @@
// ===================================================
// ⚙️ Nuttakit Response Layer vFinal++++++
// ===================================================
export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400) {
export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400, data = []) {
return {
code: String(code),
message: enMsg,
message_th: thMsg,
data: []
data: data
}
}
// ===================================================
// 🔹 Auto Success Response (ใช้โดย Global Handler เท่านั้น)
// ===================================================
export function formatSuccessResponse(data) {
return {
code: "200",

View File

@@ -0,0 +1,25 @@
import { sendError } from './response.js';
export const validateSave = (value, columnName) => {
// เช็คว่าค่าเป็น null, undefined หรือ empty string
if (value === undefined || value === null || value === '') {
// สร้างก้อน data ที่จะบอกว่า column ไหนหายไป
// ตามโจทย์: data: { "email": "ไม่พบข้อมูล" }
const errorDetail = {};
errorDetail[columnName] = "ไม่พบข้อมูล";
// เรียก sendError ใส่ message และ errorDetail ลงไปใน parameter ตัวที่ 4
sendError(
'ข้อมูลพารามิเตอร์ ไม่ถูกต้อง', // thMsg
'Invalid Parameter', // enMsg
400, // code
errorDetail // data
);
// ปาลูกระเบิดออกไปให้ Controller รับ
// throw errorObj;
}
return value;
}

View File

@@ -13,7 +13,7 @@ SMTP_USER=lalisakuty@gmail.com
SMTP_PASS=lurl pckw qugk tzob
# REDIS
REDIS_HOST=127.0.0.1
REDIS_HOST=10.9.0.0
REDIS_PORT=6379
OTP_TTL_SECONDS=300

View File

@@ -1,7 +1,7 @@
{
"name": "exthernal-mobile-api",
"name": "exthernal-ttc-api",
"version": "1.0.0",
"description": "External Mobile API following Nuttakit Controller Pattern vFinal",
"description": "External TTC API following Nuttakit Controller Pattern vFinal",
"type": "module",
"main": "src/app.js",
"scripts": {

View File

@@ -1,8 +1,11 @@
import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import { createServer } from 'http' // ✅ เพิ่ม
import { Server } from 'socket.io' // ✅ เพิ่ม
import router from './routes/route.js'
import { globalResponseHandler } from './middlewares/responseHandler.js'
import { SocketManager } from './socket/socketManager.js' // ✅ เพิ่ม Class ที่เราจะสร้าง
dotenv.config()
@@ -27,6 +30,20 @@ app.use((err, req, res, next) => {
app.use('/api/ttc', router)
app.listen(process.env.PORT, () => {
console.log(`${process.env.PJ_NAME} running on port ${process.env.PORT}`)
// ✅ เปลี่ยนการ Listen เป็น HTTP Server + Socket
const httpServer = createServer(app)
const io = new Server(httpServer, {
cors: {
origin: "*", // ปรับตามความเหมาะสม
methods: ["GET", "POST"]
}
})
// ✅ เรียกใช้ Socket Manager ตาม Pattern
const socketManager = new SocketManager(io)
socketManager.initialize()
const PORT = process.env.PORT || 3000
httpServer.listen(PORT, () => {
console.log(`${process.env.PJ_NAME} running on port ${PORT} with WebSocket`)
})

View File

@@ -30,7 +30,6 @@ export class budgetAdd {
const decoded = verifyToken(token);
let name = req.body.request.bdgnam;
let id = req.body.request.bdgseq;
database = decoded.organization
aryResult = await this.budgetAddService.getBudgetAdd(database, name); // เช็คกับ db กลาง ส่ง jwttoken ออกมา

View File

@@ -0,0 +1,85 @@
import { BudgetExpenseService } from '../services/budgetExpenseService.js';
import { ProjectSearchService } from '../services/projectSearchService.js';
import { sendError, formatSuccessResponse } from '../utils/response.js';
import { GeneralService } from '../share/generalservice.js';
import { verifyToken } from '../utils/token.js';
export class BudgetExpenseController {
constructor() {
this.generalService = new GeneralService();
this.budgetExpenseService = new BudgetExpenseService();
this.projectSearchService = new ProjectSearchService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'budgetexpensecontroller.js', 'onNavigate() start');
// Logic เดิม: รับ organization จาก body แต่เดี๋ยวจะถูก override ด้วย token ใน onBudgetExpense
let organization = req.body.organization || 'dbo';
const result = await this.onBudgetExpense(req, res, organization);
return result;
}
async onBudgetExpense(req, res, database) {
let idx = -1;
let aryResult = [];
let condition = {};
try {
// 1. แกะ Token เพื่อหา Organization
let token = req.headers.authorization?.split(' ')[1];
if(!token) return sendError('ไม่พบ Token', 'Missing Token');
const decoded = verifyToken(token);
if(!decoded) return sendError('Token ไม่ถูกต้อง', 'Invalid Token');
database = decoded.organization || database;
// 2. เตรียมเงื่อนไขค้นหา Project
// column = `prjseq` (กำหนดเงื่อนไขการค้นหา)
condition['prjseq'] = req.body.request.prjseq;
// 3. ตรวจสอบว่ามี Project นี้จริงหรือไม่
aryResult = await this.projectSearchService.getProjectSearch(database, 'prjseq, prjcomstt', condition);
} catch (error) {
console.error(error);
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
// ถ้าไม่เจอข้อมูล Project
if (!aryResult || aryResult.length === 0) {
return sendError('ไม่พบข้อมูลโครงการ', 'Cannot Find Project Data');
}
// ถ้าเจอ Project (aryResult.length >= 1)
if (aryResult.length > 0) {
// // [Check 1] เช็คว่าโครงการนี้อนุมัติไปแล้วหรือยัง?
// const project = aryResult[0];
// if (project.prjcomstt === 'BAP') {
// return sendError('โครงการนี้ได้รับการอนุมัติงบประมาณไปแล้ว ไม่สามารถทำรายการซ้ำได้', 'Project Already Approved');
// }
// เรียก makeArySave เพื่อทำการตัดงบ
const promise = await this.makeArySave(req, database);
return promise;
}
}
}
async makeArySave(req, database) {
// เตรียมข้อมูลสำหรับตัดงบ
const { prjseq, expenseList } = req.body.request;
// เรียก Service ที่ทำ Transaction (ตัดงบ, บันทึกรายการ, อัปเดตโปรเจกต์)
try {
const result = await this.budgetExpenseService.approveBudgetExpense(database, prjseq, expenseList);
return result;
} catch (error) {
return sendError(error.message);
}
}
}

View File

@@ -36,7 +36,7 @@ export class budgetSearch {
column = `bdgseq, bdgnam, bdgcod, bdgttl`
condition['bdgseq'] = req.body.request.bdgseq
} else if(columnParams == 'result' || columnParams == undefined || columnParams == ''){
column = `bdgnam, bdgttl`
column = `bdgnam, bdgcod`
}
aryResult = await this.budgetSearchService.getBudgetSearch(database, column, condition);

View File

@@ -0,0 +1,128 @@
import { ProjectAddService } from '../services/projectAddService.js'
import { sendError } from '../utils/response.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
import { Interface } from '../interfaces/Interface.js';
import fs from 'fs';
import path from 'path';
import { getDTM } from '../utils/date.js';
export class projectAdd {
constructor() {
this.generalService = new GeneralService();
this.Interface = new Interface();
this.projectAddService = new ProjectAddService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'projectAdd.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onProjectAdd(req, res, organization);
return prommis;
}
async onProjectAdd(req, res, database) {
let idx = -1
let aryResult = []
let latSeq = []
try {
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
const requestData = req.body;
let name = requestData.prjnam;
database = decoded.organization || 'dbo';
aryResult = await this.projectAddService.getProjectAdd(database, name);
latSeq = await this.projectAddService.getLatestProjectSeq(database);
} catch (error) {
idx = 1;
console.error(error);
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (aryResult == 0) {
const currentSeq = (latSeq && latSeq[0] && latSeq[0].prjseq) ? latSeq[0].prjseq : 0;
let prommis = await this.makeArySave(req, currentSeq);
return prommis
} else {
if (req.files) {
req.files.forEach(f => {
if (fs.existsSync(f.path)) fs.unlinkSync(f.path);
});
}
return sendError('คีย์หลักซ้ำในระบบ', 'Duplicate Primary Key');
}
}
}
async makeArySave(req, latseq) {
const requestData = req.body;
const nextSeq = latseq + 1;
let prjwntbdg = requestData.prjwntbdg;
if (!prjwntbdg || prjwntbdg === '' || prjwntbdg === 'undefined' || prjwntbdg === 'null') {
prjwntbdg = '0.00';
}
let prjusrseq = requestData.prjusrseq;
if (!prjusrseq || prjusrseq === '' || prjusrseq === 'undefined' || prjusrseq === 'null') {
prjusrseq = null;
}
const typ = requestData.typ;
let arysave = {
methods: 'post',
prjseq: nextSeq,
prjnam: requestData.prjnam,
prjusrseq: prjusrseq,
prjwntbdg: prjwntbdg,
prjacpbdg: '0.00',
prjbdgcod: '',
prjcomstt: requestData.prjcomstt || 'UAC',
prjacpdtm: getDTM(),
}
let savedFileNames = [];
if (req.files && req.files.length > 0) {
if (typ === 'prj') {
const targetDir = `uploads/projects/${nextSeq}`;
try {
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
req.files.forEach(file => {
const targetPath = path.join(targetDir, file.filename);
fs.renameSync(file.path, targetPath);
savedFileNames.push(file.filename);
});
arysave.prjdoc = savedFileNames.join(',');
} catch (err) {
console.error('Error moving files:', err);
return sendError('ไม่สามารถบันทึกไฟล์ลงโฟลเดอร์โครงการได้');
}
} else {
arysave.prjdoc = req.files.map(f => f.filename).join(',');
}
}
if (!arysave.prjusrseq) {
const token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
if (decoded) arysave.prjusrseq = decoded.id;
}
return this.Interface.saveInterface('prjmst', arysave, req);
}
}

View File

@@ -0,0 +1,114 @@
import { GeneralService } from '../share/generalservice.js';
import { sendError } from '../utils/response.js';
import { verifyToken } from '../utils/token.js'; // ✅ เพิ่ม verifyToken
import fs from 'fs';
import path from 'path';
import archiver from 'archiver';
export class projectDownload {
constructor() {
this.generalService = new GeneralService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'projectDownload.js', 'onNavigate() start');
const prjseq = req.query.prjseq;
const docType = req.query.docType;
// ✅ 1. แกะ Token เพื่อหา Database Schema (เพราะต้อง Query DB)
let token = req.headers.authorization?.split(' ')[1];
let database = 'dbo'; // Default
if (token) {
const decoded = verifyToken(token);
if (decoded && decoded.organization) {
database = decoded.organization;
}
}
if (!prjseq) {
return res.status(400).json(sendError('กรุณาระบุ prjseq', 'Missing prjseq parameter'));
}
return await this.onProjectDownload(req, res, prjseq, docType, database);
}
async onProjectDownload(req, res, prjseq, docType, database) {
try {
// ✅ 2. Query เช็คค่า prjdoc ใน Database ก่อน (Source of Truth)
const sql = `SELECT prjdoc FROM ${database}.prjmst WHERE prjseq = $1`;
const result = await this.generalService.executeQueryParam(database, sql, [prjseq]);
// ถ้าไม่เจอโครงการเลย
if (result.length === 0) {
return res.json(sendError('ไม่พบข้อมูลโครงการนี้ในระบบ', 'Project not found in DB', 404));
}
const prjdoc = result[0].prjdoc;
// ✅ 3. เช็คว่า prjdoc ว่างหรือไม่? (null, undefined, หรือ string ว่าง)
if (!prjdoc || prjdoc.trim() === '') {
return res.json(sendError('ไม่พบเอกสารแนบในระบบ (prjdoc ว่าง)', 'No documents recorded in database', 404));
}
// ✅ 4. แปลงรายชื่อไฟล์จาก DB (Comma Separated) เป็น Array
// ตัวอย่าง: "file1.jpg,file2.pdf" -> ["file1.jpg", "file2.pdf"]
let dbFiles = prjdoc.split(',').map(f => f.trim()).filter(f => f !== '');
const folderPath = `uploads/projects/${prjseq}`;
// เช็คว่ามีโฟลเดอร์จริงไหม
if (!fs.existsSync(folderPath)) {
return res.json(sendError('ไม่พบไฟล์ใน Server (โฟลเดอร์สูญหาย)', 'Project folder missing on server', 404));
}
// ✅ 5. กรองไฟล์: ต้องมีชื่อใน DB **และ** มีไฟล์อยู่จริงบน Disk
// (และผ่าน filter docType ถ้ามี)
let validFiles = dbFiles.filter(filename => {
// Filter docType (ถ้าส่งมา)
if (docType && !filename.toLowerCase().endsWith(`.${docType.toLowerCase()}`)) {
return false;
}
// เช็คว่าไฟล์มีอยู่จริงไหม
const fullPath = path.join(folderPath, filename);
return fs.existsSync(fullPath);
});
// ถ้ากรองแล้วไม่เหลือไฟล์เลย
if (validFiles.length === 0) {
return res.json(sendError('ไม่พบไฟล์เอกสารที่สามารถดาวน์โหลดได้', 'No valid files found', 404));
}
// --- เริ่มกระบวนการ Zip ---
const archive = archiver('zip', { zlib: { level: 9 } });
const zipFilename = `project_${prjseq}_documents.zip`;
res.attachment(zipFilename);
archive.on('error', function(err) {
console.error('Archiver Error:', err);
if (!res.headersSent) {
res.status(500).send({error: err.message});
}
});
archive.pipe(res);
validFiles.forEach(filename => {
const filePath = path.join(folderPath, filename);
archive.file(filePath, { name: filename });
});
await archive.finalize();
} catch (error) {
console.error('Download Controller Error:', error);
if (!res.headersSent) {
return res.json(sendError('เกิดข้อผิดพลาดในการดาวน์โหลด', 'Download Error', 500));
}
}
}
}

View File

@@ -0,0 +1,128 @@
import { ProjectEditService } from '../services/projectEditService.js'
import { sendError } from '../utils/response.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
import { Interface } from '../interfaces/Interface.js';
import fs from 'fs';
import path from 'path';
import { getDTM } from '../utils/date.js';
export class projectEdit {
constructor() {
this.generalService = new GeneralService();
this.Interface = new Interface();
this.projectEditService = new ProjectEditService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'projectAdd.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onProjectEdit(req, res, organization);
return prommis;
}
async onProjectEdit(req, res, database) {
let idx = -1
let aryResult = []
let latSeq = []
try {
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
const requestData = req.body;
let name = requestData.prjnam;
database = decoded.organization || 'dbo';
aryResult = await this.projectEditService.getProjectEdit(database, name);
latSeq = await this.projectEditService.getLatestProjectSeq(database);
} catch (error) {
idx = 1;
console.error(error);
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (aryResult == 0) {
const currentSeq = (latSeq && latSeq[0] && latSeq[0].prjseq) ? latSeq[0].prjseq : 0;
let prommis = await this.makeArySave(req, currentSeq);
return prommis
} else {
if (req.files) {
req.files.forEach(f => {
if (fs.existsSync(f.path)) fs.unlinkSync(f.path);
});
}
return sendError('คีย์หลักซ้ำในระบบ', 'Duplicate Primary Key');
}
}
}
async makeArySave(req, latseq) {
const requestData = req.body;
const nextSeq = latseq + 1;
let prjwntbdg = requestData.prjwntbdg;
if (!prjwntbdg || prjwntbdg === '' || prjwntbdg === 'undefined' || prjwntbdg === 'null') {
prjwntbdg = '0.00';
}
let prjusrseq = requestData.prjusrseq;
if (!prjusrseq || prjusrseq === '' || prjusrseq === 'undefined' || prjusrseq === 'null') {
prjusrseq = null;
}
const typ = requestData.typ;
let arysave = {
methods: 'post',
prjseq: nextSeq,
prjnam: requestData.prjnam,
prjusrseq: prjusrseq,
prjwntbdg: prjwntbdg,
prjacpbdg: '0.00',
prjbdgcod: '',
prjcomstt: requestData.prjcomstt || 'UAC',
prjacpdtm: getDTM(),
}
let savedFileNames = [];
if (req.files && req.files.length > 0) {
if (typ === 'prj') {
const targetDir = `uploads/projects/${nextSeq}`;
try {
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
req.files.forEach(file => {
const targetPath = path.join(targetDir, file.filename);
fs.renameSync(file.path, targetPath);
savedFileNames.push(file.filename);
});
arysave.prjdoc = savedFileNames.join(',');
} catch (err) {
console.error('Error moving files:', err);
return sendError('ไม่สามารถบันทึกไฟล์ลงโฟลเดอร์โครงการได้');
}
} else {
arysave.prjdoc = req.files.map(f => f.filename).join(',');
}
}
if (!arysave.prjusrseq) {
const token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
if (decoded) arysave.prjusrseq = decoded.id;
}
return this.Interface.saveInterface('prjmst', arysave, req);
}
}

View File

@@ -14,7 +14,7 @@ export class projectSearch {
async onNavigate(req, res) {
this.generalService.devhint(1, 'projectSearch.js', 'onNavigate() start');
let organization = req.body.organization;
let organization = req.body.organization || 'dbo'; // Default Schema
const prommis = await this.onProjectSearch(req, res, organization);
return prommis;
}
@@ -23,29 +23,75 @@ export class projectSearch {
let idx = -1
let aryResult = []
let condition = {}
let column = ""
try {
// let username = req.body.request.username;
// let password = req.body.request.password;
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
database = decoded.organization
// ใช้ Organization จาก Token ถ้ามี
database = decoded.organization || database
let columnParams = req.query.column
var column = ""
if(columnParams == 'edit'){
column = `prjnam, prjwntbdg`
condition['prjseq'] = req.body.request.prjseq
} else if(columnParams == 'result' || columnParams == undefined || columnParams == ''){
column = `prjseq, prjnam, prjwntbdg, prjacpbdg, prjbdgcod, prjcomstt, prjacpdtm`
condition['prjseq'] = req.body.request.prjseq;
switch (columnParams) {
case "user":
column = `prjseq, prjnam, prjwntbdg, prjcomstt, prjacpdtm,
(
SELECT trnacpdtm
FROM ${database}.trnmst t
WHERE trnprjseq = prjseq
ORDER BY trnacpdtm DESC
LIMIT 1
) as trnacpdtm`
aryResult = await this.projectSearchService.getProjectSearch(database, column, condition);
break;
case "edit":
column = `prjseq, prjnam, prjwntbdg, prjcomstt`
aryResult = await this.projectSearchService.getProjectSearch(database, column, condition);
break;
case "useredit":
column = `prjseq, prjnam, prjwntbdg, prjdoc`
aryResult = await this.projectSearchService.getProjectSearch(database, column, condition);
break;
default:
column = `
prjseq,
prjnam,
usrthinam as prjusrnam,
prjwntbdg,
${database}.translatebdg(
(
SELECT string_agg(DISTINCT trnbdgcod, ',')
FROM ${database}.trnmst
WHERE trnprjseq = prjseq
)
) as prjbdgnam,
(
SELECT string_agg(DISTINCT trnbdgcod, ',')
FROM ${database}.trnmst
WHERE trnprjseq = prjseq
) as prjbdgcod,
prjacpbdg,
${database}.translatedtl('COMSTT', prjcomstt) as prjcomsttnam,
prjcomstt,
prjacpdtm
`;
aryResult = await this.projectSearchService.getProjectDetailSearch(database, column, condition);
break;
}
aryResult = await this.projectSearchService.getProjectSearch(database, column, condition);
} catch (error) {
console.error(error);
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (aryResult == 0) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
if (!aryResult || aryResult.length === 0) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
return aryResult
}
}

View File

@@ -0,0 +1,165 @@
import { ReportService } from '../services/reportService.js'
import { sendError } from '../utils/response.js'
// import { OftenError } from '../utils/oftenError.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
import { Interface } from '../interfaces/Interface.js';
export class reportController {
constructor() {
this.generalService = new GeneralService();
this.reportService = new ReportService();
this.Interface = new Interface();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'reportController.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onReportController(req, res, organization);
return prommis;
}
async onReportController(req, res, database) {
let idx = -1
let aryResult = []
try {
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
let acpTime = req.body.request.acpTime;
let expTime = req.body.request.expTime;
database = decoded.organization;
aryResult = await this.reportService.getReportController(database, acpTime, expTime);
} catch (error) {
idx = 1;b
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!aryResult) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
try {
// ✅ 1) เตรียม data สำหรับใช้คำนวณ
// ถ้า service คืนมาเป็น { code, message, data: [...] }
const data = Array.isArray(aryResult)
? aryResult
: Array.isArray(aryResult.data)
? aryResult.data
: [];
// ถ้าไม่มีข้อมูลก็ไม่ต้องคำนวณ
if (!data.length) {
return aryResult;
}
// 2) แยก Budget Typechar
const omitFields = (obj, ...keys) => {
const copy = { ...obj };
keys.forEach(k => delete copy[k]);
return copy;
};
const developStudentList = data.filter(i => i.trnbdgcod === '29')
.map(i => omitFields(i, 'trnbdgcod', 'trncomstt'));
const incomeList = data.filter(i => i.trnbdgcod === '33')
.map(i => omitFields(i, 'trnbdgcod', 'trncomstt'));
const incomeBachelorList = data.filter(i => i.trnbdgcod === '38')
.map(i => omitFields(i, 'trnbdgcod', 'trncomstt'));
const budgetCollegeList = data.filter(i => i.trnbdgcod === '24')
.map(i => omitFields(i, 'trnbdgcod', 'trncomstt'));
const budgetTeachingList = data.filter(i => i.trnbdgcod === '30')
.map(i => omitFields(i, 'trnbdgcod', 'trncomstt'));
const shortBudgetList = data.filter(i => i.trnbdgcod === '25')
.map(i => omitFields(i, 'trnbdgcod', 'trncomstt'));
// ✅ 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า)
var summary = {
// DEBUG
developStudentList: developStudentList,
incomeList: incomeList,
incomeBachelorList: incomeBachelorList,
budgetCollegeList: budgetCollegeList,
budgetTeachingList: budgetTeachingList,
shortBudgetList: shortBudgetList,
// totalIncome: totalIncome.toFixed(2),
// totalExpense: otalExpense.toFixed(2),
// netProfit: netProfit.toFixed(2),
};
// ✅ 3.5) Create actdata table with required fields grouped by actnum
var trndata = data.map(row => ({
trnprjnam: row.trnprjnam,
trnexpbdg: row.trnexpbdg,
trnbdgnam: row.trnbdgnam,
// trnbdgcod: row.trnbdgcod, // DEBUG
trncomsttnam: row.trncomsttnam,
// trncomstt: row.trncomstt, // DEBUG
trnacpdtm: row.trnacpdtm
}));
// // ✅ 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม)
// // ตัวอย่างสมมติ: คุณไป query มาจาก service ก่อนหน้าแล้วได้เป็น object แบบนี้
// // key = ชื่อหมวด (actcatnam หรือ code), value = color
// const categoryColorMap = await this.reportService.getCategoryColorMap(database);
// // ตัวอย่างที่คาดหวังจาก service:
// // { 'ค่าอาหาร': '#FF6384', 'ค่าเดินทาง': '#36A2EB', 'ขายสินค้า': '#4BC0C0', ... }
// // ✅ 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense
// const expenseAgg = {};
// expenseList.forEach(row => {
// const key = row.actcat; // หรือใช้รหัส category ถ้ามี เช่น row.actcatcod
// const amount = parseFloat(row.actqty || 0);
// expenseAgg[key] = (expenseAgg[key] || 0) + amount;
// });
// const incomeAgg = {};
// incomeList.forEach(row => {
// const key = row.actcat;
// const amount = parseFloat(row.actqty || 0);
// incomeAgg[key] = (incomeAgg[key] || 0) + amount;
// });
// const expensePie = Object.entries(expenseAgg).map(([cat, value]) => {
// const percent = totalExpense > 0 ? (value / totalExpense) * 100 : 0;
// const color = categoryColorMap[cat] || '#CCCCCC'; // fallback สี default
// return {
// label: cat,
// value: value.toFixed(2),
// percent: percent.toFixed(2),
// color
// };
// });
// const incomePie = Object.entries(incomeAgg).map(([cat, value]) => {
// const percent = totalIncome > 0 ? (value / totalIncome) * 100 : 0;
// const color = categoryColorMap[cat] || '#CCCCCC';
// return {
// label: cat,
// value: value.toFixed(2),
// percent: percent.toFixed(2),
// color
// };
// });
// // ✅ 6) แนบข้อมูล pie chart เข้า aryResult
// var pie = {
// expense: expensePie,
// income: incomePie
// };
} catch (err) {
console.error('calculate summary/pie error:', err);
}
let arydiy = {
trndata,
summary,
// pie,
}
return arydiy;
}
}
}

View File

@@ -0,0 +1,118 @@
import { SocketService } from '../services/socketService.js'
import { GeneralService } from '../share/generalservice.js'
import { Interface } from '../interfaces/Interface.js'
// import { sendError } from '../utils/response.js' // Socket ส่ง error กลับคนละแบบ แต่ import ไว้ได้
export class SocketController {
constructor() {
this.generalService = new GeneralService()
this.socketService = new SocketService()
this.Interface = new Interface()
}
// =========================================================
// FEATURE: NOTIFICATION
// =========================================================
async onSendNotification(io, socket, data) {
this.generalService.devhint(1, 'socketController.js', 'onSendNotification() start')
let idx = -1
let database = socket.organization
try {
// Data: { targetUserId, title, message, type }
const { targetUserId, title, message, type } = data
// 1. บันทึกลง Database (ใช้ Interface Pattern ถ้ามี Table รองรับ เช่น 'notimst')
// สมมติว่ามีตาราง notimst
/*
let arysave = {
methods: 'post',
notusrseq: targetUserId,
nottitle: title,
notmsg: message,
notread: 'N',
notdtm: this.socketService.getCurrentDTM() // function ใน service
}
// await this.Interface.saveInterface('notimst', arysave, { headers: { authorization: ... } })
// *หมายเหตุ: Interface.js ต้องการ req.headers ซึ่ง socket ไม่มี ต้อง Mock หรือแก้ Interface
*/
// หรือเรียก Service ตรงๆ เพื่อบันทึก
await this.socketService.saveNotificationLog(database, socket.user.id, targetUserId, title, message)
// 2. ส่ง Realtime หา Target
io.to(targetUserId.toString()).emit('receive_notification', {
from: socket.user.usrnam,
title,
message,
type,
timestamp: new Date()
})
this.generalService.devhint(2, 'socketController.js', `Sent notify to ${targetUserId}`)
} catch (error) {
idx = 1
console.error(error)
} finally {
if (idx === 1) {
socket.emit('error', { message: 'Failed to send notification' })
}
}
}
// =========================================================
// FEATURE: VOIP (WebRTC Signaling)
// =========================================================
// A โทรหา B
async onCallUser(io, socket, data) {
this.generalService.devhint(1, 'socketController.js', 'onCallUser() start')
let idx = -1
try {
const { userToCall, signalData } = data
// ส่ง Event 'call_incoming' ไปหาห้องของ userToCall
io.to(userToCall.toString()).emit('call_incoming', {
signal: signalData,
from: socket.user.id,
fromName: socket.user.usrnam
})
} catch (error) {
idx = 1
} finally {
if (idx === 1) socket.emit('error', { message: 'Call failed' })
}
}
// B รับสาย A
async onAnswerCall(io, socket, data) {
this.generalService.devhint(1, 'socketController.js', 'onAnswerCall() start')
try {
const { to, signal } = data
io.to(to.toString()).emit('call_accepted', { signal })
} catch (error) {
console.error('VoIP Error:', error)
}
}
// แลกเปลี่ยน Network Info (ICE Candidate)
async onIceCandidate(io, socket, data) {
try {
const { targetUserId, candidate } = data
io.to(targetUserId.toString()).emit('receive_ice_candidate', { candidate })
} catch (error) {
// silent fail for ICE
}
}
// วางสาย
async onEndCall(io, socket, data) {
const { targetUserId } = data
if(targetUserId) {
io.to(targetUserId.toString()).emit('call_ended', { from: socket.user.id })
}
}
}

View File

@@ -0,0 +1,53 @@
import { TransactionSearchService } from '../services/transactionSearchService.js'
import { sendError } from '../utils/response.js'
// import { OftenError } from '../utils/oftenError.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
export class transactionSearch {
constructor() {
this.generalService = new GeneralService();
this.transactionSearchService = new TransactionSearchService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'transactionSearch.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onTransactionSearch(req, res, organization);
return prommis;
}
async onTransactionSearch(req, res, database) {
let idx = -1
let aryResult = []
let condition = {}
try {
// let username = req.body.request.username;
// let password = req.body.request.password;
let token = req.headers.authorization?.split(' ')[1];
const decoded = verifyToken(token);
database = decoded.organization
let columnParams = req.query.column
var column = ""
if(columnParams == 'edit'){
column = `trnseq, trnprjnam, trnprjseq, trnbdgcod, trncomstt`
condition['trnseq'] = req.body.request.trnseq
} else if(columnParams == 'result' || columnParams == undefined || columnParams == ''){
condition['trnprjseq'] = req.body.request.trnprjseq
column = `trnseq, trnprjnam, trnbdgcod, ${database}.translatebdg(trnbdgcod) AS trnbdgnam, ${database}.translatedtl('COMSTT', trncomstt) AS trncomstt, trnexpbdg, trnacpdtm`
}
aryResult = await this.transactionSearchService.getTransactionSearch(database, column, condition);
} catch (error) {
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
// if (aryResult == 0) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
return aryResult
}
}
}

View File

@@ -1,5 +1,7 @@
import jwt from 'jsonwebtoken'
import { BdgmstInterface } from './table/bdgmstInterface.js'
import { PrjmstInterface } from './table/prjmstInterface.js'
import { TrnmstInterface } from './table/trnmstInterface.js'
import { sendError } from '../utils/response.js'
// import { ActmstInterface } from './actmstInterface.js'
@@ -13,6 +15,8 @@ export class Interface {
constructor() {
this.map = {
bdgmst: new BdgmstInterface(),
prjmst: new PrjmstInterface(),
trnmst: new TrnmstInterface(),
// actmst: new ActmstInterface(),
}
}

View File

@@ -0,0 +1,84 @@
import { GeneralService } from '../../share/generalservice.js'
export class PrjmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'prjmst'
this.pk = ['prjseq', 'prjnam']
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -0,0 +1,84 @@
import { GeneralService } from '../../share/generalservice.js'
export class TrnmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'trnmst'
this.pk = ['trnseq', 'trnprjnam', 'trnbdgcod'] // ⭐ PK จาก table
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -0,0 +1,94 @@
import multer from 'multer'
import path from 'path'
import fs from 'fs'
import { sendError } from '../utils/response.js'
import { getDTM } from '../utils/date.js'
const tempDir = 'uploads/temp'
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, tempDir)
},
filename: function (req, file, cb) {
// ดึงนามสกุลไฟล์
const ext = path.extname(file.originalname);
// ดึงชื่อไฟล์เดิม (ตัดนามสกุลออก)
const originalName = path.basename(file.originalname, ext);
// Clean ชื่อไฟล์: เปลี่ยน space เป็น _, ลบอักขระพิเศษ, เหลือแค่ภาษาอังกฤษ ตัวเลข และ - _
const cleanName = originalName.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 100);
// Format: YYYYMMDDHHmm-Random-CleanName.ext
// ตัวอย่าง: 202511300930-1234-System_Req.docx
const dtm = getDTM();
const random = Math.round(Math.random() * 1E4);
cb(null, `${dtm}-${random}-${cleanName}${ext}`);
}
})
const fileFilter = (req, file, cb) => {
const allowedMimes = [
'image/jpeg', 'image/png', 'image/jpg',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
]
if (allowedMimes.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('รองรับเฉพาะไฟล์รูปภาพ, PDF หรือเอกสาร Word เท่านั้น'), false)
}
}
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: { fileSize: 10 * 1024 * 1024 }
})
function verifyFileSignature(filePath) {
try {
const buffer = Buffer.alloc(8)
const fd = fs.openSync(filePath, 'r')
fs.readSync(fd, buffer, 0, 8, 0)
fs.closeSync(fd)
const hex = buffer.toString('hex').toUpperCase()
if (hex.startsWith('FFD8FF')) return true // JPG
if (hex.startsWith('89504E47')) return true // PNG
if (hex.startsWith('25504446')) return true // PDF
if (hex.startsWith('D0CF11E0')) return true // DOC
if (hex.startsWith('504B0304')) return true // DOCX
return false
} catch (err) {
return false
}
}
export const uploadMiddleware = (req, res, next) => {
const uploadHandler = upload.array('prjdoc', 10)
uploadHandler(req, res, (err) => {
if (err) return res.status(400).json(sendError(err.message))
if (!req.files || req.files.length === 0) return next()
for (const file of req.files) {
const isSafe = verifyFileSignature(file.path)
if (!isSafe) {
req.files.forEach(f => {
if (fs.existsSync(f.path)) fs.unlinkSync(f.path)
})
return res.status(400).json(sendError(`ไฟล์ ${file.originalname} ไม่ปลอดภัย (Invalid Signature)`))
}
}
next()
})
}

View File

@@ -1,4 +1,4 @@
import Redis from 'ioredis';
import Redis from '../utils/redis.js';
import { GeneralService } from '../share/generalservice.js';
// import { sendError } from './response.js';

View File

@@ -1,21 +1,23 @@
import express from 'express'
// import { budgetSetup } from '../controllers/budgetSetupController.js'
import { budgetSearch } from '../controllers/budgetSearchController.js'
import { budgetAdd } from '../controllers/budgetAddController.js'
import { projectSearch } from '../controllers/projectSearchController.js'
// import { authMiddleware } from '../middlewares/auth.js'
// import { sendResponse } from '../utils/response.js'
import { projectAdd } from '../controllers/projectAddController.js'
import { BudgetExpenseController } from '../controllers/budgetExpenseController.js'
import { reportController } from '../controllers/reportController.js'
import { transactionSearch } from '../controllers/transactionSearchController.js'
import { uploadMiddleware } from '../middlewares/uploadMiddleware.js'
import { projectDownload } from '../controllers/projectDownloadController.js' // ✅ Import
const router = express.Router()
const controller_projectSearch_post = new projectSearch()
const controller_budgetSearch_post = new budgetSearch()
const controller_budgetAdd_post = new budgetAdd()
// router.post('/budgetSetup', async (req, res) => {
// const result = await controller_budgetSetup_post.onNavigate(req, res)
// if (result) return res.json(result)
// })
const controller_budgetSetup_post = new BudgetExpenseController()
const controller_report_post = new reportController()
const controller_projectAdd_post = new projectAdd()
const controller_transactionSearch_post = new transactionSearch()
const controller_projectDownload_get = new projectDownload()
router.post('/budgetadd', async (req, res) => {
const result = await controller_budgetAdd_post.onNavigate(req, res)
@@ -32,10 +34,34 @@ router.post('/projectsearch', async (req, res) => {
if (result) return res.json(result)
})
// router.put('/budgetexpense', async (req, res) => {
// const result = await controller_budgetSetup_post.onNavigate(req, res)
// if (result) return res.json(result)
// })
router.post('/projectadd', uploadMiddleware, async (req, res) => {
const result = await controller_projectAdd_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/projectedit', uploadMiddleware, async (req, res) => {
const result = await controller_projectAdd_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.get('/projectdownload', async (req, res) => {
// ไม่ต้อง return res.json() เพราะ Controller จัดการ Stream แล้ว
await controller_projectDownload_get.onNavigate(req, res)
})
router.post('/transactionsearch', async (req, res) => {
const result = await controller_transactionSearch_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/budgetexpense', async (req, res) => {
const result = await controller_budgetSetup_post.onNavigate(req, res)
if (result) return res.json(result)
})
router.post('/report', async (req, res) => {
const result = await controller_report_post.onNavigate(req, res)
if (result) return res.json(result)
})
export default router

View File

@@ -18,4 +18,18 @@ export class BudgetAddService {
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
async genNum(database) {
const sql = `
SELECT
MAX(bdgseq) as max_seq
FROM ${database}.bdgmst
`
const params = []
const aryResult = await this.generalService.executeQueryParam(database, sql, params);
const lastSeq = aryResult[0]?.max_seq || 0;
return lastSeq + 1;
}
}

View File

@@ -0,0 +1,144 @@
import { GeneralService } from '../share/generalservice.js';
import { connection } from '../config/db.js';
import { sendError } from '../utils/response.js';
import { getDTM } from '../utils/date.js'; // ✅ 1. Import ฟังก์ชันกลางเข้ามา
export class BudgetExpenseService {
constructor() {
this.generalService = new GeneralService();
}
async getBudgetExpense(database, name) {
const sql = `
SELECT trnseq, trnprjnam
FROM ${database}.trnmst
WHERE trnprjnam = $1
`;
const params = [name];
const result = await this.generalService.executeQueryParam(database, sql, params);
return result;
}
// ฟังก์ชันอนุมัติและตัดงบ (Transaction)
async approveBudgetExpense(database, projectSeq, expenseList, user) {
const client = await connection.connect();
try {
await client.query('BEGIN');
// 2. เรียกใช้ getDTM() แทนโค้ดเดิม
const currentDTM = getDTM();
// =========================================================
// STEP 1: คืนเงินงบประมาณเดิมก่อน (กรณีแก้ไข/ลบรายการ)
// =========================================================
const oldExpenses = await client.query(
`SELECT trnbdgcod, trnexpbdg FROM ${database}.trnmst WHERE trnprjseq = $1`,
[projectSeq]
);
// วนลูปคืนเงินกลับเข้าตาราง bdgmst (bdgttl + ยอดเดิม)
for (const oldItem of oldExpenses.rows) {
await client.query(`
UPDATE ${database}.bdgmst
SET bdgttl = bdgttl + $1, bdgedtdtm = $2
WHERE bdgcod = $3
`, [oldItem.trnexpbdg, currentDTM, oldItem.trnbdgcod]); // update เวลาแก้ไขล่าสุด
}
// =========================================================
// STEP 2: ลบรายการเดิมทิ้ง (เพื่อเตรียมลงใหม่)
// =========================================================
await client.query(`DELETE FROM ${database}.trnmst WHERE trnprjseq = $1`, [projectSeq]);
// =========================================================
// STEP 3: บันทึกรายการใหม่ และตัดเงินใหม่
// =========================================================
let totalApprovedAmount = 0;
const prjRes = await client.query(`SELECT prjnam FROM ${database}.prjmst WHERE prjseq = $1`, [projectSeq]);
if (prjRes.rows.length === 0) throw new Error(`Project ${projectSeq} not found`);
const projectName = prjRes.rows[0].prjnam;
for (const expense of expenseList) {
// 🛑 [Validation] ห้ามยอดเงินเป็น 0 หรือติดลบ
if (!expense.amount || Number(expense.amount) <= 0) {
return sendError(`ยอดเงินต้องมากกว่า 0 (รายการรหัสงบ: ${expense.bdgcod})`);
}
// [Check 2] ตรวจสอบว่ามีรหัสงบประมาณนี้จริงหรือไม่
const checkBdgSql = `SELECT bdgseq FROM ${database}.bdgmst WHERE bdgcod = $1`;
const checkBdgRes = await client.query(checkBdgSql, [expense.bdgcod]);
if (checkBdgRes.rows.length === 0) {
return sendError(`ไม่พบรหัสงบประมาณ: ${expense.bdgcod} ในระบบ`);
}
// Get Next Seq
const seqRes = await client.query(`SELECT COALESCE(MAX(trnseq), 0) + 1 as nextseq FROM ${database}.trnmst`);
const nextTrnSeq = seqRes.rows[0].nextseq;
const expenseAmount = Number(expense.amount).toFixed(2);
// Insert รายการใหม่ (บันทึก currentDTM ลง trnacpdtm)
const sqlTrn = `
INSERT INTO ${database}.trnmst
(trnseq, trnprjnam, trnprjseq, trnexpbdg, trnbdgcod, trncomstt, trnacpdtm)
VALUES ($1, $2, $3, $4, $5, 'BAP', $6)
`;
await client.query(sqlTrn, [
nextTrnSeq,
projectName,
projectSeq,
expenseAmount,
expense.bdgcod,
currentDTM
]);
// ตัดเงินงบประมาณ (อัปเดตเวลาแก้ไข)
const sqlUpdateBdg = `
UPDATE ${database}.bdgmst
SET bdgttl = bdgttl - $1,
bdgedtdtm = $2
WHERE bdgcod = $3
`;
await client.query(sqlUpdateBdg, [expenseAmount, currentDTM, expense.bdgcod]);
totalApprovedAmount += Number(expense.amount);
}
// ---------------------------------------------------------
// STEP 4: อัปเดต Project Master (เปลี่ยนสถานะตามยอดเงิน)
// ---------------------------------------------------------
const formattedTotal = totalApprovedAmount.toFixed(2);
// [UPDATED] กำหนดสถานะ: ถ้ายอดรวมเป็น 0 ให้เป็น UAC (รออนุมัติ), ถ้ามีเงินให้เป็น BAP (อนุมัติแล้ว)
const projectStatus = totalApprovedAmount > 0 ? 'BAP' : 'UAC';
const sqlUpdatePrj = `
UPDATE ${database}.prjmst
SET prjacpbdg = $1,
prjcomstt = $2
WHERE prjseq = $3
`;
await client.query(sqlUpdatePrj, [formattedTotal, projectStatus, projectSeq]);
await client.query('COMMIT');
return {
status: true,
msg: 'Budget updated successfully',
total: formattedTotal,
projectStatus: projectStatus
};
} catch (error) {
await client.query('ROLLBACK');
console.error('Transaction Error:', error);
throw error;
} finally {
client.release();
}
}
}

View File

@@ -0,0 +1,34 @@
import { GeneralService } from '../share/generalservice.js'
export class ProjectAddService {
constructor() {
this.generalService = new GeneralService()
}
async getProjectAdd(database, name) {
const sql = `
SELECT
prjseq,
prjnam
FROM ${database}.prjmst
WHERE prjnam = $1
`
const params = [name]
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
async getLatestProjectSeq(database) {
const sql = `
SELECT
prjseq
FROM ${database}.prjmst
WHERE prjseq=(SELECT max(prjseq) FROM ${database}.prjmst)
`
const params = []
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
}

View File

@@ -0,0 +1,34 @@
import { GeneralService } from '../share/generalservice.js'
export class ProjectEditService {
constructor() {
this.generalService = new GeneralService()
}
async getProjectEdit(database, name) {
const sql = `
SELECT
prjseq,
prjnam
FROM ${database}.prjmst
WHERE prjnam = $1
`
const params = [name]
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
async getLatestProjectSeq(database) {
const sql = `
SELECT
prjseq
FROM ${database}.prjmst
WHERE prjseq=(SELECT max(prjseq) FROM ${database}.prjmst)
`
const params = []
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
}

View File

@@ -1,26 +1,47 @@
import { GeneralService } from '../share/generalservice.js'
import { GeneralService } from '../share/generalservice.js';
export class ProjectSearchService {
constructor() {
this.generalService = new GeneralService()
this.generalService = new GeneralService();
}
// 🟢 ฟังก์ชันเดิม (Simple Search) - คืนสภาพเดิมเพื่อไม่ให้กระทบ Service อื่น
// ใช้สำหรับค้นหาข้อมูลในตาราง prjmst อย่างเดียว
async getProjectSearch(database, column, condition) {
const selectCol = column || '*';
const sql = `
SELECT
${column}
SELECT ${selectCol}
FROM ${database}.prjmst
WHERE 1=1
`
const params = []
const result = await this.generalService.executeQueryConditions(database, sql, condition);
return result
}
`;
return await this.generalService.executeQueryConditions(database, sql, condition);
}
// bdgseq,
// bdgnam,
// bdgcod,
// bdgttl,
// bdgedtdtm
// ดึงข้อมูล: ลำดับ, รหัส, ชื่อโครงการ, ผู้รับผิดชอบ, งบขอ, หมวดงบ, งบอนุมัติ, สถานะ
async getProjectDetailSearch(database, column, condition) {
const selectCol = column || `
prjseq,
prjnam,
usrnam,
prjwntbdg,
bdgnam,
prjacpbdg,
prjcomstt,
prjacpdtm
`;
const sql = `
SELECT ${selectCol}
FROM ${database}.prjmst p
LEFT JOIN nuttakit.usrmst u ON prjusrseq = usrseq
LEFT JOIN ${database}.bdgmst b ON prjbdgcod = bdgcod
WHERE 1=1
ORDER BY prjseq ASC
`;
const result = await this.generalService.executeQueryConditions(database, sql, condition);
return result;
}
}

View File

@@ -0,0 +1,45 @@
import { GeneralService } from '../share/generalservice.js'
export class ReportService {
constructor() {
this.generalService = new GeneralService()
}
async getReportController(database, acpTime, expTime) {
// trnprjseq,
const sql = `
SELECT
trnprjnam,
trnexpbdg,
${database}.translatebdg(trnbdgcod) AS trnbdgnam,
trnbdgcod,
${database}.translatedtl('COMSTT', trncomstt) AS trncomsttnam,
trncomstt,
trnacpdtm
FROM ${database}.trnmst
WHERE trnacpdtm BETWEEN $1 AND $2;
`;
const params = [acpTime, expTime];
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
async getCategoryColorMap(database) {
const sql = `
SELECT dtlcod, dtlnam, dtlmsc as dtlclr
FROM ${database}.dtlmst
WHERE dtltblcod IN ('ACTCAT_INC', 'ACTCAT_EXP')
`;
const params = [];
const rows = await this.generalService.executeQueryParam(database, sql, params);
const map = {};
rows.forEach(r => {
map[r.dtlnam] = r.dtlclr;
});
return map;
}
}

View File

@@ -0,0 +1,32 @@
import { GeneralService } from '../share/generalservice.js'
import { getDTM } from '../utils/date.js'
export class SocketService {
constructor() {
this.generalService = new GeneralService()
}
getCurrentDTM() {
return getDTM()
}
// ตัวอย่างฟังก์ชันบันทึก Notification
async saveNotificationLog(database, fromUserSeq, toUserSeq, title, msg) {
// สมมติว่ามีตาราง comhtr
// ตรวจสอบก่อนว่ามีตารางไหม หรือข้ามไปถ้ายังไม่ได้สร้าง
/*
const sql = `
INSERT INTO ${database}.comhtr
(from_seq, to_seq, title, message, created_dtm)
VALUES ($1, $2, $3, $4, $5)
`
const params = [fromUserSeq, toUserSeq, title, msg, getDTM()]
await this.generalService.executeQueryParam(database, sql, params)
*/
// Demo: แค่ Log ไว้ก่อน
this.generalService.devhint(2, 'SocketService', `Saving Log DB: [${database}] From ${fromUserSeq} to ${toUserSeq}`)
return true
}
}

View File

@@ -0,0 +1,26 @@
import { GeneralService } from '../share/generalservice.js'
export class TransactionSearchService {
constructor() {
this.generalService = new GeneralService()
}
async getTransactionSearch(database, column, condition) {
const sql = `
SELECT
${column}
FROM ${database}.trnmst
WHERE 1=1
`
const params = []
const result = await this.generalService.executeQueryConditions(database, sql, condition);
return result
}
}
// bdgseq,
// bdgnam,
// bdgcod,
// bdgttl,
// bdgedtdtm

View File

@@ -0,0 +1,80 @@
import { verifyToken } from '../utils/token.js'
import { SocketController } from '../controllers/socketController.js'
import { GeneralService } from '../share/generalservice.js'
import redis from '../utils/redis.js' // ใช้ Redis ที่มีเก็บ Session
export class SocketManager {
constructor(io) {
this.io = io
this.generalService = new GeneralService()
this.socketController = new SocketController()
}
initialize() {
this.generalService.devhint(1, 'SocketManager.js', 'Initializing Socket.io')
// Middleware: Authentication (เช็ค Token ก่อน Connect)
this.io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.token
if (!token) return next(new Error('Authentication error'))
const decoded = verifyToken(token)
if (!decoded) return next(new Error('Invalid Token'))
// เก็บข้อมูล User เข้า Socket Session
socket.user = decoded
socket.organization = decoded.organization // ใช้สำหรับ Schema
next()
} catch (err) {
next(new Error('Authentication failed'))
}
})
this.io.on('connection', async (socket) => {
this.generalService.devhint(1, 'SocketManager.js', `User Connected: ${socket.user.usrnam}`)
// 1. Save User Session to Redis (Pattern การเก็บ state)
// key: "online:user_id", value: socket_id
await redis.set(`online:${socket.user.id}`, socket.id)
// Join Room ส่วนตัว (ตาม User ID)
socket.join(socket.user.id.toString())
// ==========================================
// Event Handlers (เรียก Controller Pattern)
// ==========================================
// 1. Send Notification (User ส่งหา User)
socket.on('send_notification', async (data) => {
await this.socketController.onSendNotification(this.io, socket, data)
})
// 2. VoIP: Call Request
socket.on('call_user', async (data) => {
await this.socketController.onCallUser(this.io, socket, data)
})
// 3. VoIP: Answer Call
socket.on('answer_call', async (data) => {
await this.socketController.onAnswerCall(this.io, socket, data)
})
// 4. VoIP: ICE Candidate (Network info)
socket.on('ice_candidate', async (data) => {
await this.socketController.onIceCandidate(this.io, socket, data)
})
// 5. VoIP: End Call
socket.on('end_call', async (data) => {
await this.socketController.onEndCall(this.io, socket, data)
})
// Disconnect
socket.on('disconnect', async () => {
this.generalService.devhint(1, 'SocketManager.js', `User Disconnected: ${socket.user.usrnam}`)
await redis.del(`online:${socket.user.id}`)
})
})
}
}

View File

@@ -0,0 +1,16 @@
/**
* getDTM (Get Date Time)
* คืนค่าเวลาปัจจุบันในรูปแบบ: YYYYMMDDHHmm (12 หลัก)
* ตัวอย่าง: 202511211445
*/
export function getDTM() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // เดือนเริ่มที่ 0 ต้อง +1
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}`;
}

View File

@@ -4,7 +4,19 @@ dotenv.config()
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
port: process.env.REDIS_PORT,
connectTimeout: 10000,
maxRetriesPerRequest: null
})
redis.on('error', (err) => {
// Log the error so you know it's happening, but don't crash
console.error('Redis connection error:', err.code);
})
// Optional: Log when connected successfully
redis.on('connect', () => {
console.log('Connected to Redis successfully');
})
export async function saveOtp(email, otp) {

6
listen-pipe.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
trap "rm -f $HOME/execpipe" EXIT
trap "rm -f $HOME/execpipe" ERR
mkfifo $HOME/execpipe
while true; do eval "$(cat $HOME/execpipe)"; done

View File

@@ -20,6 +20,7 @@
"author": "Nuttakit Pothong",
"license": "MIT",
"dependencies": {
"archiver": "^7.0.1",
"bcrypt": "^6.0.0",
"connect-redis": "^9.0.0",
"cors": "^2.8.5",
@@ -28,8 +29,10 @@
"express-session": "^1.18.2",
"ioredis": "^5.8.2",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"nodemailer": "^7.0.10",
"pg": "^8.16.3",
"socket.io": "^4.8.1",
"xlsx": "^0.18.5"
},
"devDependencies": {

3
start-accountingwep.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd /server/exthernal-accountingwep-api
npm start

3
start-login.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd /server/exthernal-login-api
npm start

3
start-ttc.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd /server/exthernal-ttc-api
npm start

1
test.sh Normal file
View File

@@ -0,0 +1 @@
#!/bin/bash

16
xecel/node_modules/.bin/crc32 generated vendored
View File

@@ -1,16 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../crc-32/bin/crc32.njs" "$@"
else
exec node "$basedir/../crc-32/bin/crc32.njs" "$@"
fi

17
xecel/node_modules/.bin/crc32.cmd generated vendored
View File

@@ -1,17 +0,0 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\crc-32\bin\crc32.njs" %*

28
xecel/node_modules/.bin/crc32.ps1 generated vendored
View File

@@ -1,28 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
} else {
& "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
} else {
& "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
xecel/node_modules/.bin/xlsx generated vendored
View File

@@ -1,16 +0,0 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../xlsx/bin/xlsx.njs" "$@"
else
exec node "$basedir/../xlsx/bin/xlsx.njs" "$@"
fi

17
xecel/node_modules/.bin/xlsx.cmd generated vendored
View File

@@ -1,17 +0,0 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\xlsx\bin\xlsx.njs" %*

28
xecel/node_modules/.bin/xlsx.ps1 generated vendored
View File

@@ -1,28 +0,0 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../xlsx/bin/xlsx.njs" $args
} else {
& "$basedir/node$exe" "$basedir/../xlsx/bin/xlsx.njs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../xlsx/bin/xlsx.njs" $args
} else {
& "node$exe" "$basedir/../xlsx/bin/xlsx.njs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

111
xecel/node_modules/.package-lock.json generated vendored
View File

@@ -1,111 +0,0 @@
{
"name": "xecel",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
}
}
}

201
xecel/node_modules/adler-32/LICENSE generated vendored
View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2014-present SheetJS LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

140
xecel/node_modules/adler-32/README.md generated vendored
View File

@@ -1,140 +0,0 @@
# adler32
Signed ADLER-32 algorithm implementation in JS (for the browser and nodejs).
Emphasis on correctness, performance, and IE6+ support.
## Installation
With [npm](https://www.npmjs.org/package/adler-32):
```bash
$ npm install adler-32
```
In the browser:
```html
<script src="adler32.js"></script>
```
The browser exposes a variable `ADLER32`.
When installed globally, npm installs a script `adler32` that computes the
checksum for a specified file or standard input.
The script will manipulate `module.exports` if available . This is not always
desirable. To prevent the behavior, define `DO_NOT_EXPORT_ADLER`.
## Usage
In all cases, the relevant function takes an argument representing data and an
optional second argument representing the starting "seed" (for running hash).
The return value is a signed 32-bit integer.
- `ADLER32.buf(byte array or buffer[, seed])` assumes the argument is a sequence
of 8-bit unsigned integers (nodejs `Buffer`, `Uint8Array` or array of bytes).
- `ADLER32.bstr(binary string[, seed])` assumes the argument is a binary string
where byte `i` is the low byte of the UCS-2 char: `str.charCodeAt(i) & 0xFF`
- `ADLER32.str(string)` assumes the argument is a standard JS string and
calculates the hash of the UTF-8 encoding.
For example:
```js
// var ADLER32 = require('adler-32'); // uncomment if in node
ADLER32.str("SheetJS") // 176947863
ADLER32.bstr("SheetJS") // 176947863
ADLER32.buf([ 83, 104, 101, 101, 116, 74, 83 ]) // 176947863
adler32 = ADLER32.buf([83, 104]) // 17825980 "Sh"
adler32 = ADLER32.str("eet", adler32) // 95486458 "Sheet"
ADLER32.bstr("JS", adler32) // 176947863 "SheetJS"
[ADLER32.str("\u2603"), ADLER32.str("\u0003")] // [ 73138686, 262148 ]
[ADLER32.bstr("\u2603"), ADLER32.bstr("\u0003")] // [ 262148, 262148 ]
[ADLER32.buf([0x2603]), ADLER32.buf([0x0003])] // [ 262148, 262148 ]
```
## Testing
`make test` will run the nodejs-based test.
To run the in-browser tests, run a local server and go to the `ctest` directory.
`make ctestserv` will start a python `SimpleHTTPServer` server on port 8000.
To update the browser artifacts, run `make ctest`.
To generate the bits file, use the `adler32` function from python `zlib`:
```python
>>> from zlib import adler32
>>> x="foo bar baz٪☃🍣"
>>> adler32(x)
1543572022
>>> adler32(x+x)
-2076896149
>>> adler32(x+x+x)
2023497376
```
The [`adler32-cli`](https://www.npmjs.com/package/adler32-cli) package includes
scripts for processing files or text on standard input:
```bash
$ echo "this is a test" > t.txt
$ adler32-cli t.txt
726861088
```
For comparison, the `adler32.py` script in the subdirectory uses python `zlib`:
```bash
$ packages/adler32-cli/bin/adler32.py t.txt
726861088
```
## Performance
`make perf` will run algorithmic performance tests (which should justify certain
decisions in the code).
Bit twiddling is much faster than taking the mod in Safari and Firefox browsers.
Instead of taking the literal mod 65521, it is faster to keep it in the integers
by bit-shifting: `65536 ~ 15 mod 65521` so for nonnegative integer `a`:
```
a = (a >>> 16) * 65536 + (a & 65535) [equality]
a ~ (a >>> 16) * 15 + (a & 65535) mod 65521
```
The mod is taken at the very end, since the intermediate result may exceed 65521
## Magic Number
The magic numbers were chosen so as to not overflow a 31-bit integer:
```mathematica
F[n_] := Reduce[x*(x + 1)*n/2 + (x + 1)*(65521) < (2^31 - 1) && x > 0, x, Integers]
F[255] (* bstr: x \[Element] Integers && 1 <= x <= 3854 *)
F[127] (* ascii: x \[Element] Integers && 1 <= x <= 5321 *)
```
Subtract up to 4 elements for the Unicode case.
## License
Please consult the attached LICENSE file for details. All rights not explicitly
granted by the Apache 2.0 license are reserved by the Original Author.
## Badges
[![Sauce Test Status](https://saucelabs.com/browser-matrix/adler32.svg)](https://saucelabs.com/u/adler32)
[![Build Status](https://img.shields.io/github/workflow/status/sheetjs/js-adler32/Tests:%20node.js)](https://github.com/SheetJS/js-adler32/actions)
[![Coverage Status](http://img.shields.io/coveralls/SheetJS/js-adler32/master.svg)](https://coveralls.io/r/SheetJS/js-adler32?branch=master)
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/js-adler32?pixel)](https://github.com/SheetJS/js-adler32)

View File

@@ -1,92 +0,0 @@
/* adler32.js (C) 2014-present SheetJS -- http://sheetjs.com */
/* vim: set ts=2: */
/*exported ADLER32 */
var ADLER32;
(function (factory) {
/*jshint ignore:start */
/*eslint-disable */
if(typeof DO_NOT_EXPORT_ADLER === 'undefined') {
if('object' === typeof exports) {
factory(exports);
} else if ('function' === typeof define && define.amd) {
define(function () {
var module = {};
factory(module);
return module;
});
} else {
factory(ADLER32 = {});
}
} else {
factory(ADLER32 = {});
}
/*eslint-enable */
/*jshint ignore:end */
}(function(ADLER32) {
ADLER32.version = '1.3.1';
function adler32_bstr(bstr, seed) {
var a = 1, b = 0, L = bstr.length, M = 0;
if(typeof seed === 'number') { a = seed & 0xFFFF; b = seed >>> 16; }
for(var i = 0; i < L;) {
M = Math.min(L-i, 2654)+i;
for(;i<M;i++) {
a += bstr.charCodeAt(i)&0xFF;
b += a;
}
a = (15*(a>>>16)+(a&65535));
b = (15*(b>>>16)+(b&65535));
}
return ((b%65521) << 16) | (a%65521);
}
function adler32_buf(buf, seed) {
var a = 1, b = 0, L = buf.length, M = 0;
if(typeof seed === 'number') { a = seed & 0xFFFF; b = (seed >>> 16) & 0xFFFF; }
for(var i = 0; i < L;) {
M = Math.min(L-i, 2654)+i;
for(;i<M;i++) {
a += buf[i]&0xFF;
b += a;
}
a = (15*(a>>>16)+(a&65535));
b = (15*(b>>>16)+(b&65535));
}
return ((b%65521) << 16) | (a%65521);
}
function adler32_str(str, seed) {
var a = 1, b = 0, L = str.length, M = 0, c = 0, d = 0;
if(typeof seed === 'number') { a = seed & 0xFFFF; b = seed >>> 16; }
for(var i = 0; i < L;) {
M = Math.min(L-i, 2918);
while(M>0) {
c = str.charCodeAt(i++);
if(c < 0x80) { a += c; }
else if(c < 0x800) {
a += 192|((c>>6)&31); b += a; --M;
a += 128|(c&63);
} else if(c >= 0xD800 && c < 0xE000) {
c = (c&1023)+64; d = str.charCodeAt(i++) & 1023;
a += 240|((c>>8)&7); b += a; --M;
a += 128|((c>>2)&63); b += a; --M;
a += 128|((d>>6)&15)|((c&3)<<4); b += a; --M;
a += 128|(d&63);
} else {
a += 224|((c>>12)&15); b += a; --M;
a += 128|((c>>6)&63); b += a; --M;
a += 128|(c&63);
}
b += a; --M;
}
a = (15*(a>>>16)+(a&65535));
b = (15*(b>>>16)+(b&65535));
}
return ((b%65521) << 16) | (a%65521);
}
// $FlowIgnore
ADLER32.bstr = adler32_bstr;
// $FlowIgnore
ADLER32.buf = adler32_buf;
// $FlowIgnore
ADLER32.str = adler32_str;
}));

View File

@@ -1,35 +0,0 @@
{
"name": "adler-32",
"version": "1.3.1",
"author": "sheetjs",
"description": "Pure-JS ADLER-32",
"keywords": [ "adler32", "checksum" ],
"main": "./adler32",
"types": "types/index.d.ts",
"devDependencies": {
"mocha": "~2.5.3",
"blanket": "~1.2.3",
"codepage": "~1.10.0",
"@sheetjs/uglify-js": "~2.7.3",
"@types/node": "^8.0.7",
"dtslint": "^0.1.2",
"typescript": "2.2.0"
},
"repository": { "type": "git", "url": "git://github.com/SheetJS/js-adler32.git" },
"scripts": {
"test": "make test",
"build": "make",
"lint": "make fullint",
"dtslint": "dtslint types"
},
"config": {
"blanket": {
"pattern": "adler32.js"
}
},
"homepage": "http://sheetjs.com/opensource",
"files": ["adler32.js", "LICENSE", "README.md", "types/index.d.ts", "types/*.json"],
"bugs": { "url": "https://github.com/SheetJS/js-adler32/issues" },
"license": "Apache-2.0",
"engines": { "node": ">=0.8" }
}

View File

@@ -1,14 +0,0 @@
/* adler32.js (C) 2014-present SheetJS -- http://sheetjs.com */
// TypeScript Version: 2.2
/** Version string */
export const version: string;
/** Process a node buffer or byte array */
export function buf(data: number[] | Uint8Array, seed?: number): number;
/** Process a binary string */
export function bstr(data: string, seed?: number): number;
/** Process a JS string based on the UTF8 encoding */
export function str(data: string, seed?: number): number;

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": [ "es5" ],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": false,
"baseUrl": ".",
"paths": { "adler-32": ["."] },
"types": [],
"noEmit": true,
"strictFunctionTypes": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@@ -1,14 +0,0 @@
{
"extends": "dtslint/dtslint.json",
"rules": {
"no-implicit-dependencies": false,
"whitespace": false,
"no-sparse-arrays": false,
"only-arrow-functions": false,
"no-consecutive-blank-lines": false,
"prefer-conditional-expression": false,
"one-variable-per-declaration": false,
"strict-export-declare-modifiers": false,
"prefer-template": false
}
}

201
xecel/node_modules/cfb/LICENSE generated vendored
View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2013-present SheetJS LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

161
xecel/node_modules/cfb/README.md generated vendored
View File

@@ -1,161 +0,0 @@
# Container File Blobs
Pure JS implementation of various container file formats, including ZIP and CFB.
[![Build Status](https://travis-ci.org/SheetJS/js-cfb.svg?branch=master)](https://travis-ci.org/SheetJS/js-cfb)
[![Coverage Status](http://img.shields.io/coveralls/SheetJS/js-cfb/master.svg)](https://coveralls.io/r/SheetJS/js-cfb?branch=master)
[![Dependencies Status](https://david-dm.org/sheetjs/js-cfb/status.svg)](https://david-dm.org/sheetjs/js-cfb)
[![NPM Downloads](https://img.shields.io/npm/dt/cfb.svg)](https://npmjs.org/package/cfb)
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/js-cfb?pixel)](https://github.com/SheetJS/js-cfb)
## Installation
In the browser:
```html
<script src="dist/cfb.min.js" type="text/javascript"></script>
```
With [npm](https://www.npmjs.org/package/cfb):
```bash
$ npm install cfb
```
The `xlscfb.js` file is designed to be embedded in [js-xlsx](http://git.io/xlsx)
## Library Usage
In node:
```js
var CFB = require('cfb');
```
For example, to get the Workbook content from an Excel 2003 XLS file:
```js
var cfb = CFB.read(filename, {type: 'file'});
var workbook = CFB.find(cfb, 'Workbook');
var data = workbook.content;
```
## Command-Line Utility Usage
The [`cfb-cli`](https://www.npmjs.com/package/cfb-cli) module ships with a CLI
tool for manipulating and inspecting supported files.
## JS API
TypeScript definitions are maintained in `types/index.d.ts`.
The CFB object exposes the following methods and properties:
`CFB.parse(blob)` takes a nodejs Buffer or an array of bytes and returns an
parsed representation of the data.
`CFB.read(blob, opts)` wraps `parse`.
`CFB.find(cfb, path)` performs a case-insensitive match for the path (or file
name, if there are no slashes) and returns an entry object or null if not found.
`CFB.write(cfb, opts)` generates a file based on the container.
`CFB.writeFile(cfb, filename, opts)` creates a file with the specified name.
### Parse Options
`CFB.read` takes an options argument. `opts.type` controls the behavior:
| `type` | expected input |
|------------|:----------------------------------------------------------------|
| `"base64"` | string: Base64 encoding of the file |
| `"binary"` | string: binary string (byte `n` is `data.charCodeAt(n)`) |
| `"buffer"` | nodejs Buffer |
| `"file"` | string: path of file that will be read (nodejs only) |
| (default) | buffer or array of 8-bit unsigned int (byte `n` is `data[n]`) |
### Write Options
`CFB.write` and `CFB.writeFile` take options argument.
`opts.type` controls the behavior:
| `type` | output |
|------------|:----------------------------------------------------------------|
| `"base64"` | string: Base64 encoding of the file |
| `"binary"` | string: binary string (byte `n` is `data.charCodeAt(n)`) |
| `"buffer"` | nodejs Buffer |
| `"file"` | string: path of file that will be created (nodejs only) |
| (default) | buffer if available, array of 8-bit unsigned int otherwise |
`opts.fileType` controls the output file type:
| `fileType` | output |
|:-------------------|:------------------------|
| `'cfb'` (default) | CFB container |
| `'zip'` | ZIP file |
| `'mad'` | MIME aggregate document |
`opts.compression` enables DEFLATE compression for ZIP file type.
## Utility Functions
The utility functions are available in the `CFB.utils` object. Functions that
accept a `name` argument strictly deal with absolute file names:
- `.cfb_new(?opts)` creates a new container object.
- `.cfb_add(cfb, name, ?content, ?opts)` adds a new file to the `cfb`.
Set the option `{unsafe:true}` to skip existence checks (for bulk additions)
- `.cfb_del(cfb, name)` deletes the specified file
- `.cfb_mov(cfb, old_name, new_name)` moves the old file to new path and name
- `.use_zlib(require("zlib"))` loads a nodejs `zlib` instance.
By default, the library uses a pure JS inflate/deflate implementation. NodeJS
`zlib.InflateRaw` exposes the number of bytes read in versions after `8.11.0`.
If a supplied `zlib` does not support the required features, a warning will be
displayed in the console and the pure JS fallback will be used.
## Container Object Description
The objects returned by `parse` and `read` have the following properties:
- `.FullPaths` is an array of the names of all of the streams (files) and
storages (directories) in the container. The paths are properly prefixed from
the root entry (so the entries are unique)
- `.FileIndex` is an array, in the same order as `.FullPaths`, whose values are
objects following the schema:
```typescript
interface CFBEntry {
name: string; /** Case-sensitive internal name */
type: number; /** 1 = dir, 2 = file, 5 = root ; see [MS-CFB] 2.6.1 */
content: Buffer | number[] | Uint8Array; /** Raw Content */
ct?: Date; /** Creation Time */
mt?: Date; /** Modification Time */
ctype?: String; /** Content-Type (for MAD) */
}
```
## License
Please consult the attached LICENSE file for details. All rights not explicitly
granted by the Apache 2.0 License are reserved by the Original Author.
## References
- `MS-CFB`: Compound File Binary File Format
- ZIP `APPNOTE.TXT`: .ZIP File Format Specification
- RFC1951: https://www.ietf.org/rfc/rfc1951.txt
- RFC2045: https://www.ietf.org/rfc/rfc2045.txt
- RFC2557: https://www.ietf.org/rfc/rfc2557.txt

1979
xecel/node_modules/cfb/cfb.js generated vendored

File diff suppressed because it is too large Load Diff

201
xecel/node_modules/cfb/dist/LICENSE generated vendored
View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2013-present SheetJS LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

1979
xecel/node_modules/cfb/dist/cfb.js generated vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1856
xecel/node_modules/cfb/dist/xlscfb.js generated vendored

File diff suppressed because it is too large Load Diff

68
xecel/node_modules/cfb/package.json generated vendored
View File

@@ -1,68 +0,0 @@
{
"name": "cfb",
"version": "1.2.2",
"author": "sheetjs",
"description": "Compound File Binary File Format extractor",
"keywords": [
"cfb",
"compression",
"office"
],
"main": "./cfb",
"types": "types",
"browser": {
"node": false,
"process": false,
"fs": false
},
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"devDependencies": {
"@sheetjs/uglify-js": "~2.7.3",
"@types/node": "^8.10.25",
"acorn": "7.4.1",
"alex": "8.1.1",
"blanket": "~1.2.3",
"dtslint": "~0.1.2",
"eslint": "7.23.0",
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-json": "^2.1.2",
"jscs": "3.0.7",
"jshint": "2.13.4",
"mocha": "~2.5.3",
"typescript": "2.2.0"
},
"repository": {
"type": "git",
"url": "git://github.com/SheetJS/js-cfb.git"
},
"scripts": {
"pretest": "make init",
"test": "make test",
"dtslint": "dtslint types"
},
"config": {
"blanket": {
"pattern": "cfb.js"
}
},
"files": [
"LICENSE",
"README.md",
"dist/",
"types/index.d.ts",
"types/tsconfig.json",
"cfb.js",
"xlscfb.flow.js"
],
"homepage": "http://sheetjs.com/",
"bugs": {
"url": "https://github.com/SheetJS/js-cfb/issues"
},
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
}

View File

@@ -1,128 +0,0 @@
/* index.d.ts (C) 2013-present SheetJS */
// TypeScript Version: 2.2
/** Version string */
export const version: string;
/** Parse a buffer or array */
export function parse(f: CFB$Blob, options?: CFB$ParsingOptions): CFB$Container;
/** Read a blob or file or binary string */
export function read(f: CFB$Blob | string, options?: CFB$ParsingOptions): CFB$Container;
/** Find a file entry given a path or file name */
export function find(cfb: CFB$Container, path: string): CFB$Entry | null;
/** Generate a container file */
export function write(cfb: CFB$Container, options?: CFB$WritingOptions): any;
/** Write a container file to the filesystem */
export function writeFile(cfb: CFB$Container, filename: string, options?: CFB$WritingOptions): any;
/** Utility functions */
export const utils: CFB$Utils;
export interface CFB$CommonOptions {
/** Data encoding */
type?: 'base64' | 'binary' | 'buffer' | 'file' | 'array';
/** If true, throw errors when features are not understood */
WTF?: boolean;
}
/** Options for read and readFile */
export interface CFB$ParsingOptions extends CFB$CommonOptions {
/** If true, include raw data in output */
raw?: boolean;
}
/** Options for write and writeFile */
export interface CFB$WritingOptions extends CFB$CommonOptions {
/** Output file type */
fileType?: 'cfb' | 'zip' | 'mad';
/** Override default root entry name (CFB only) */
root?: string;
/** Enable compression (ZIP only) */
compression?: boolean;
}
export type CFB$Blob = number[] | Uint8Array;
export enum CFB$EntryType { unknown, storage, stream, lockbytes, property, root }
export enum CFB$StorageType { fat, minifat }
/** CFB File Entry Object */
export interface CFB$Entry {
/** Case-sensitive internal name */
name: string;
/** CFB type (salient types: stream, storage) -- see CFB$EntryType */
type: number;
/** Raw Content (Buffer when available, Array of bytes otherwise) */
content: CFB$Blob;
/** Creation Time */
ct?: Date;
/** Modification Time */
mt?: Date;
/** Red/Black Tree color: 0 = red, 1 = black */
color: number;
/** Class ID represented as hex string */
clsid: string;
/** User-Defined State Bits */
state: number;
/** Starting Sector */
start: number;
/** Data Size */
size: number;
/** Storage location -- see CFB$StorageType */
storage?: string;
/** Content Type (used for MAD) */
ctype?: string;
}
/* File object */
export interface CFB$Container {
/* List of streams and storages */
FullPaths: string[];
/* Array of entries in the same order as FullPaths */
FileIndex: CFB$Entry[];
/* Raw Content, in chunks (Buffer when available, Array of bytes otherwise) */
raw?: {
header: CFB$Blob,
sectors: CFB$Blob[];
};
}
/** cfb_add options */
export interface CFB$AddOpts {
/** Skip existence and safety checks (best for bulk write operations) */
unsafe?: boolean;
}
/** General utilities */
export interface CFB$Utils {
cfb_new(opts?: any): CFB$Container;
cfb_add(cfb: CFB$Container, name: string, content: any, opts?: CFB$AddOpts): CFB$Entry;
cfb_del(cfb: CFB$Container, name: string): boolean;
cfb_mov(cfb: CFB$Container, old_name: string, new_name: string): boolean;
cfb_gc(cfb: CFB$Container): void;
ReadShift(size: number, t?: string): number|string;
WarnField(hexstr: string, fld?: string): void;
CheckField(hexstr: string, fld?: string): void;
prep_blob(blob: any, pos?: number): CFB$Blob;
bconcat(bufs: any[]): any;
}

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": [ "es5" ],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": false,
"baseUrl": ".",
"paths": { "cfb": ["."] },
"types": [],
"noEmit": true,
"strictFunctionTypes": true,
"forceConsistentCasingInFileNames": true
}
}

1856
xecel/node_modules/cfb/xlscfb.flow.js generated vendored

File diff suppressed because it is too large Load Diff

201
xecel/node_modules/codepage/LICENSE generated vendored
View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2013-present SheetJS LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

350
xecel/node_modules/codepage/README.md generated vendored
View File

@@ -1,350 +0,0 @@
# js-codepage
[Codepages](https://en.wikipedia.org/wiki/Codepage) are character encodings. In
many contexts, single- or double-byte character sets are used in lieu of Unicode
encodings. The codepages map between characters and numbers.
## Setup
In node:
```js
var cptable = require('codepage');
```
In the browser:
```html
<script src="cptable.js"></script>
<script src="cputils.js"></script>
```
Alternatively, use the full version in the dist folder:
```html
<script src="cptable.full.js"></script>
```
The complete set of codepages is large due to some Double Byte Character Set
encodings. A much smaller file that only includes SBCS codepages is provided in
this repo (`sbcs.js`), as well as a file for other projects (`cpexcel.js`)
If you know which codepages you need, you can include individual scripts for
each codepage. The individual files are provided in the `bits/` directory.
For example, to include only the Mac codepages:
```html
<script src="bits/10000.js"></script>
<script src="bits/10006.js"></script>
<script src="bits/10007.js"></script>
<script src="bits/10029.js"></script>
<script src="bits/10079.js"></script>
<script src="bits/10081.js"></script>
```
All of the browser scripts define and append to the `cptable` object. To rename
the object, edit the `JSVAR` shell variable in `make.sh` and run the script.
The utilities functions are contained in `cputils.js`, which assumes that the
appropriate codepage scripts were loaded.
The script will manipulate `module.exports` if available . This is not always
desirable. To prevent the behavior, define `DO_NOT_EXPORT_CODEPAGE`.
## Usage
Most codepages are indexed by number. To get the Unicode character for a given
codepoint, use the `dec` property:
```js
var unicode_cp10000_255 = cptable[10000].dec[255]; // ˇ
```
To get the codepoint for a given character, use the `enc` property:
```js
var cp10000_711 = cptable[10000].enc[String.fromCharCode(711)]; // 255
```
There are a few utilities that deal with strings and buffers:
```js
var 汇总 = cptable.utils.decode(936, [0xbb,0xe3,0xd7,0xdc]);
var buf = cptable.utils.encode(936, 汇总);
var sushi= cptable.utils.decode(65001, [0xf0,0x9f,0x8d,0xa3]); // 🍣
var sbuf = cptable.utils.encode(65001, sushi);
```
`cptable.utils.encode(CP, data, ofmt)` accepts a String or Array of characters
and returns a representation controlled by `ofmt`:
- Default output is a Buffer (or Array) of bytes (integers between 0 and 255)
- If `ofmt == 'str'`, return a binary String (byte `i` is `o.charCodeAt(i)`)
- If `ofmt == 'arr'`, return an Array of bytes
`cptable.utils.decode(CP, data)` accepts a byte String or Array of numbers or
Buffer and returns a JS string.
## Known Excel Codepages
A much smaller script, including only the codepages known to be used in Excel,
is available under the name `cpexcel`. It exposes the same variable `cptable`
and is suitable as a drop-in replacement when the full codepage tables are not
needed.
In node:
```js
var cptable = require('codepage/dist/cpexcel.full');
```
## Rolling your own script
The `make.sh` script in the repo can take a manifest and generate JS source.
Usage:
```bash
$ bash make.sh path_to_manifest output_file_name JSVAR
```
where
- `JSVAR` is the name of the exported variable (generally `cptable`)
- `output_file_name` is the output file (`cpexcel.js`, `cptable.js`, ...)
- `path_to_manifest` is the path to the manifest file.
The manifest file is expected to be a CSV with 3 columns:
```
<codepage number>,<source>,<size>
```
If a source is specified, it will try to download the specified file and parse.
The file format is expected to follow the format from the unicode.org site.
The size should be `1` for a single-byte codepage and `2` for a double-byte
codepage. For mixed codepages (which use some single- and some double-byte
codes), the script assumes the mapping is a prefix code and generates efficient
JS code.
Generated scripts only include the mapping. `cat` a mapping with `cputils.js`
to produce a complete script like `cpexcel.full.js`.
## Building the complete script
This script uses [voc](npm.im/voc). The script to build the codepage tables and
the JS source is `codepage.md`, so building involves `voc codepage.md`.
## Generated Codepages
The complete list of codepages can be found in the file `pages.csv`.
Some codepages are easier to implement algorithmically. Since those character
tables are not generated, there is no corresponding entry (they are "magic").
| CP# | Source | Description |
|--------:|:-----------:|:-----------------------------------------------------|
| ` 37` | unicode.org | IBM EBCDIC US-Canada |
| ` 437` | unicode.org | OEM United States |
| ` 500` | unicode.org | IBM EBCDIC International |
| ` 620` | NLS | Mazovia (Polish) MS-DOS |
| ` 708` | Windows 7 | Arabic (ASMO 708) |
| ` 720` | Windows 7 | Arabic (Transparent ASMO); Arabic (DOS) |
| ` 737` | unicode.org | OEM Greek (formerly 437G); Greek (DOS) |
| ` 775` | unicode.org | OEM Baltic; Baltic (DOS) |
| ` 808` | unicode.org | OEM Russian; Cyrillic + Euro symbol |
| ` 850` | unicode.org | OEM Multilingual Latin 1; Western European (DOS) |
| ` 852` | unicode.org | OEM Latin 2; Central European (DOS) |
| ` 855` | unicode.org | OEM Cyrillic (primarily Russian) |
| ` 857` | unicode.org | OEM Turkish; Turkish (DOS) |
| ` 858` | Windows 7 | OEM Multilingual Latin 1 + Euro symbol |
| ` 860` | unicode.org | OEM Portuguese; Portuguese (DOS) |
| ` 861` | unicode.org | OEM Icelandic; Icelandic (DOS) |
| ` 862` | unicode.org | OEM Hebrew; Hebrew (DOS) |
| ` 863` | unicode.org | OEM French Canadian; French Canadian (DOS) |
| ` 864` | unicode.org | OEM Arabic; Arabic (864) |
| ` 865` | unicode.org | OEM Nordic; Nordic (DOS) |
| ` 866` | unicode.org | OEM Russian; Cyrillic (DOS) |
| ` 869` | unicode.org | OEM Modern Greek; Greek, Modern (DOS) |
| ` 870` | Windows 7 | IBM EBCDIC Multilingual/ROECE (Latin 2) |
| ` 872` | unicode.org | OEM Cyrillic (primarily Russian) + Euro Symbol |
| ` 874` | unicode.org | Windows Thai |
| ` 875` | unicode.org | IBM EBCDIC Greek Modern |
| ` 895` | NLS | Kamenický (Czech) MS-DOS |
| ` 932` | unicode.org | Japanese Shift-JIS |
| ` 936` | unicode.org | Simplified Chinese GBK |
| ` 949` | unicode.org | Korean |
| ` 950` | unicode.org | Traditional Chinese Big5 |
| ` 1010` | IBM | IBM EBCDIC French |
| ` 1026` | unicode.org | IBM EBCDIC Turkish (Latin 5) |
| ` 1047` | Windows 7 | IBM EBCDIC Latin 1/Open System |
| ` 1132` | IBM | IBM EBCDIC Lao (1132 / 1133 / 1341) |
| ` 1140` | Windows 7 | IBM EBCDIC US-Canada (037 + Euro symbol) |
| ` 1141` | Windows 7 | IBM EBCDIC Germany (20273 + Euro symbol) |
| ` 1142` | Windows 7 | IBM EBCDIC Denmark-Norway (20277 + Euro symbol) |
| ` 1143` | Windows 7 | IBM EBCDIC Finland-Sweden (20278 + Euro symbol) |
| ` 1144` | Windows 7 | IBM EBCDIC Italy (20280 + Euro symbol) |
| ` 1145` | Windows 7 | IBM EBCDIC Latin America-Spain (20284 + Euro symbol) |
| ` 1146` | Windows 7 | IBM EBCDIC United Kingdom (20285 + Euro symbol) |
| ` 1147` | Windows 7 | IBM EBCDIC France (20297 + Euro symbol) |
| ` 1148` | Windows 7 | IBM EBCDIC International (500 + Euro symbol) |
| ` 1149` | Windows 7 | IBM EBCDIC Icelandic (20871 + Euro symbol) |
| ` 1200` | magic | Unicode UTF-16, little endian (BMP of ISO 10646) |
| ` 1201` | magic | Unicode UTF-16, big endian |
| ` 1250` | unicode.org | Windows Central Europe |
| ` 1251` | unicode.org | Windows Cyrillic |
| ` 1252` | unicode.org | Windows Latin I |
| ` 1253` | unicode.org | Windows Greek |
| ` 1254` | unicode.org | Windows Turkish |
| ` 1255` | unicode.org | Windows Hebrew |
| ` 1256` | unicode.org | Windows Arabic |
| ` 1257` | unicode.org | Windows Baltic |
| ` 1258` | unicode.org | Windows Vietnam |
| ` 1361` | Windows 7 | Korean (Johab) |
| `10000` | unicode.org | MAC Roman |
| `10001` | Windows 7 | Japanese (Mac) |
| `10002` | Windows 7 | MAC Traditional Chinese (Big5) |
| `10003` | Windows 7 | Korean (Mac) |
| `10004` | Windows 7 | Arabic (Mac) |
| `10005` | Windows 7 | Hebrew (Mac) |
| `10006` | unicode.org | Greek (Mac) |
| `10007` | unicode.org | Cyrillic (Mac) |
| `10008` | Windows 7 | MAC Simplified Chinese (GB 2312) |
| `10010` | Windows 7 | Romanian (Mac) |
| `10017` | Windows 7 | Ukrainian (Mac) |
| `10021` | Windows 7 | Thai (Mac) |
| `10029` | unicode.org | MAC Latin 2 (Central European) |
| `10079` | unicode.org | Icelandic (Mac) |
| `10081` | unicode.org | Turkish (Mac) |
| `10082` | Windows 7 | Croatian (Mac) |
| `12000` | magic | Unicode UTF-32, little endian byte order |
| `12001` | magic | Unicode UTF-32, big endian byte order |
| `20000` | Windows 7 | CNS Taiwan (Chinese Traditional) |
| `20001` | Windows 7 | TCA Taiwan |
| `20002` | Windows 7 | ETEN Taiwan (Chinese Traditional) |
| `20003` | Windows 7 | IBM5550 Taiwan |
| `20004` | Windows 7 | TeleText Taiwan |
| `20005` | Windows 7 | Wang Taiwan |
| `20105` | Windows 7 | Western European IA5 (IRV International Alphabet 5) |
| `20106` | Windows 7 | IA5 German (7-bit) |
| `20107` | Windows 7 | IA5 Swedish (7-bit) |
| `20108` | Windows 7 | IA5 Norwegian (7-bit) |
| `20127` | magic | US-ASCII (7-bit) |
| `20261` | Windows 7 | T.61 |
| `20269` | Windows 7 | ISO 6937 Non-Spacing Accent |
| `20273` | Windows 7 | IBM EBCDIC Germany |
| `20277` | Windows 7 | IBM EBCDIC Denmark-Norway |
| `20278` | Windows 7 | IBM EBCDIC Finland-Sweden |
| `20280` | Windows 7 | IBM EBCDIC Italy |
| `20284` | Windows 7 | IBM EBCDIC Latin America-Spain |
| `20285` | Windows 7 | IBM EBCDIC United Kingdom |
| `20290` | Windows 7 | IBM EBCDIC Japanese Katakana Extended |
| `20297` | Windows 7 | IBM EBCDIC France |
| `20420` | Windows 7 | IBM EBCDIC Arabic |
| `20423` | Windows 7 | IBM EBCDIC Greek |
| `20424` | Windows 7 | IBM EBCDIC Hebrew |
| `20833` | Windows 7 | IBM EBCDIC Korean Extended |
| `20838` | Windows 7 | IBM EBCDIC Thai |
| `20866` | Windows 7 | Russian Cyrillic (KOI8-R) |
| `20871` | Windows 7 | IBM EBCDIC Icelandic |
| `20880` | Windows 7 | IBM EBCDIC Cyrillic Russian |
| `20905` | Windows 7 | IBM EBCDIC Turkish |
| `20924` | Windows 7 | IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) |
| `20932` | Windows 7 | Japanese (JIS 0208-1990 and 0212-1990) |
| `20936` | Windows 7 | Simplified Chinese (GB2312-80) |
| `20949` | Windows 7 | Korean Wansung |
| `21025` | Windows 7 | IBM EBCDIC Cyrillic Serbian-Bulgarian |
| `21027` | NLS | Extended/Ext Alpha Lowercase |
| `21866` | Windows 7 | Ukrainian Cyrillic (KOI8-U) |
| `28591` | unicode.org | ISO 8859-1 Latin 1 (Western European) |
| `28592` | unicode.org | ISO 8859-2 Latin 2 (Central European) |
| `28593` | unicode.org | ISO 8859-3 Latin 3 |
| `28594` | unicode.org | ISO 8859-4 Baltic |
| `28595` | unicode.org | ISO 8859-5 Cyrillic |
| `28596` | unicode.org | ISO 8859-6 Arabic |
| `28597` | unicode.org | ISO 8859-7 Greek |
| `28598` | unicode.org | ISO 8859-8 Hebrew (ISO-Visual) |
| `28599` | unicode.org | ISO 8859-9 Turkish |
| `28600` | unicode.org | ISO 8859-10 Latin 6 |
| `28601` | unicode.org | ISO 8859-11 Latin (Thai) |
| `28603` | unicode.org | ISO 8859-13 Latin 7 (Estonian) |
| `28604` | unicode.org | ISO 8859-14 Latin 8 (Celtic) |
| `28605` | unicode.org | ISO 8859-15 Latin 9 |
| `28606` | unicode.org | ISO 8859-15 Latin 10 |
| `29001` | Windows 7 | Europa 3 |
| `38598` | Windows 7 | ISO 8859-8 Hebrew (ISO-Logical) |
| `47451` | unicode.org | Atari ST/TT |
| `50220` | magic | ISO 2022 JIS Japanese with no halfwidth Katakana |
| `50221` | magic | ISO 2022 JIS Japanese with halfwidth Katakana |
| `50222` | magic | ISO 2022 Japanese JIS X 0201-1989 (1 byte Kana-SO/SI)|
| `50225` | magic | ISO 2022 Korean |
| `50227` | magic | ISO 2022 Simplified Chinese |
| `51932` | Windows 7 | EUC Japanese |
| `51936` | Windows 7 | EUC Simplified Chinese |
| `51949` | Windows 7 | EUC Korean |
| `52936` | Windows 7 | HZ-GB2312 Simplified Chinese |
| `54936` | Windows 7 | GB18030 Simplified Chinese (4 byte) |
| `57002` | Windows 7 | ISCII Devanagari |
| `57003` | Windows 7 | ISCII Bengali |
| `57004` | Windows 7 | ISCII Tamil |
| `57005` | Windows 7 | ISCII Telugu |
| `57006` | Windows 7 | ISCII Assamese |
| `57007` | Windows 7 | ISCII Oriya |
| `57008` | Windows 7 | ISCII Kannada |
| `57009` | Windows 7 | ISCII Malayalam |
| `57010` | Windows 7 | ISCII Gujarati |
| `57011` | Windows 7 | ISCII Punjabi |
| `65000` | magic | Unicode (UTF-7) |
| `65001` | magic | Unicode (UTF-8) |
`unicode.org` refers to the Unicode Consortium Public Mappings, a database of
various mappings between Unicode characters and respective character sets. The
tables are processed by a few scripts in the build process.
`IBM` refers to the IBM coded character set database. Even though IBM uses a
different numbering scheme from Windows, the IBM numbers are used when there is
no conflict. The tables are manually generated from the symbol manifests.
`Windows 7` refers to direct inspection of Windows 7 machines using .NET class
`System.Text.Encoding`. The enclosed `MakeEncoding.cs` C# program brute-forces
code pages. `MakeEncoding.cs` deviates from unicode.org in some cases. When they
map a given code to different characters, unicode.org value is used. When
unicode.org does not prescribe a value, `MakeEncoding.cs` value is used.
`NLS` refers to the National Language Support files supplied in various versions
of Windows. In older versions of Windows (like Windows 98) these files followed
the name pattern `CP_#.NLS`, but newer versions use the name pattern `C_#.NLS`.
## Testing
`make test` will run the nodejs-based test.
To run the in-browser tests, run a local server and go to the `ctest` directory.
`make ctestserv` will start a python `SimpleHTTPServer` server on port 8000.
To update the browser artifacts, run `make ctest`.
## Sources
- [Unicode Consortium Public Mappings](http://www.unicode.org/Public/MAPPINGS/)
- [Windows Code Page Enumeration](http://msdn.microsoft.com/en-us/library/cc195051.aspx)
- [Windows Code Page Identifiers](http://msdn.microsoft.com/en-us/library/windows/desktop/dd317756.aspx)
- [IBM Coded Character Sets](https://www-01.ibm.com/software/globalization/ccsid/ccsid_registered.html)
- [ISO/IEC 2022 / ECMA-35](https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-035.pdf)
- [International Register of Coded Character Sets To Be Used With Escape Sequences](https://www.itscj.ipsj.or.jp/itscj_english/iso-ir/ISO-IR.pdf)
- [Japanese Character Encoding for Internet Messages](https://tools.ietf.org/html/rfc1468)
## License
Please consult the attached LICENSE file for details. All rights not explicitly
granted by the Apache 2.0 license are reserved by the Original Author.
## Badges
[![Sauce Test Status](https://saucelabs.com/browser-matrix/codepage.svg)](https://saucelabs.com/u/codepage)
[![Build Status](https://travis-ci.org/SheetJS/js-codepage.svg?branch=master)](https://travis-ci.org/SheetJS/js-codepage)
[![Coverage Status](http://img.shields.io/coveralls/SheetJS/js-codepage/master.svg)](https://coveralls.io/r/SheetJS/js-codepage?branch=master)
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/js-codepage?pixel)](https://github.com/SheetJS/js-codepage)

Some files were not shown because too many files have changed in this diff Show More