Compare commits
80 Commits
cbf210cc8b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8df36bd3e2 | |||
| d7c19bbc5b | |||
|
|
f2d988681a | ||
|
|
5ad8079465 | ||
|
|
fc8332f25b | ||
|
|
dd07f09243 | ||
|
|
98e69ca5f0 | ||
|
|
20f0bb12fa | ||
|
|
351e348af1 | ||
|
|
e881d7311b | ||
|
|
b32515779f | ||
|
|
16c3c1dc15 | ||
|
|
15e2cbae68 | ||
|
|
192451ecce | ||
|
|
8d112178d1 | ||
| b5d4a8ccfc | |||
| 62944baf24 | |||
|
|
f416b065e3 | ||
|
|
e0e7bd44e7 | ||
|
|
1cb3a2bc2d | ||
| 0f176deb1b | |||
|
|
b46197ca06 | ||
|
|
5e22a0af02 | ||
| d29744bcfb | |||
| d3decda0f7 | |||
|
|
76d48f895f | ||
| 9771fa1360 | |||
| 5a1ed0887e | |||
| 5bf68cfe40 | |||
| 7f405281c6 | |||
| 8d8f7e278b | |||
| 32eae26d45 | |||
| be31d4a2b1 | |||
| 3f88f91281 | |||
| fb52941dc6 | |||
| ac3b028b7c | |||
| 25f90e9c93 | |||
| cb846dd346 | |||
| 10b45d8439 | |||
| d38a59a7c1 | |||
|
|
b1fef4c600 | ||
|
|
74dead1f4b | ||
|
|
18a8548596 | ||
| aa0b17740a | |||
| 8fd3ada2f3 | |||
| 4b29570757 | |||
| 712e17ece8 | |||
| 40383733cd | |||
| 4a548e38b5 | |||
| 7584e0fb8e | |||
| 2dee76e1c7 | |||
|
|
02b1a6f31b | ||
|
|
2237163847 | ||
|
|
f8344e7afc | ||
| 75c40798c0 | |||
| 4cb135d251 | |||
| 9751b0ac6e | |||
| c99ef8e64d | |||
| 2dcd432802 | |||
| e296f41198 | |||
| 0d26b67165 | |||
| e456af98d4 | |||
| f1339b22db | |||
| cfcd37be31 | |||
| 3ee63ebd7f | |||
| 21bd8c64ff | |||
| a705281ad7 | |||
| b48a241c26 | |||
| 66f2bffabb | |||
| b662469176 | |||
| f68c856340 | |||
| 1a3cf7d9ff | |||
| 0fea9c08c9 | |||
| 77c7d0574b | |||
| 5d3f409a68 | |||
| b06a28d76f | |||
| 92e5f28cf1 | |||
| e708a48075 | |||
|
|
8e28cf99a7 | ||
|
|
b31682c041 |
27
.gitea/workflows/build-image.yml
Normal file
27
.gitea/workflows/build-image.yml
Normal 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
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
node_modules
|
||||
|
||||
package-lock.json
|
||||
/exthernal-rentroom-api
|
||||
uploads
|
||||
|
||||
33
@knowleadge/dbchange/11252025.txt
Normal file
33
@knowleadge/dbchange/11252025.txt
Normal 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;
|
||||
@@ -12,3 +12,46 @@ CREATE TABLE IF NOT EXISTS dbo.prjmst
|
||||
CONSTRAINT prjmst_pkey PRIMARY KEY (prjseq, prjnam)
|
||||
)
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dbo.trnmst
|
||||
(
|
||||
trnseq integer NOT NULL, -- เลขที่รายการ หรือ เลข บิล
|
||||
trnprjnam character varying(150) COLLATE pg_catalog."default" NOT NULL, -- ชื่อโปรเจ็ค
|
||||
trnprjseq integer , -- เลขที่โปรเจ็ค
|
||||
trnexpbdg numeric(14,2), -- จำนวนเงินที่จ่ายออก
|
||||
trnbdgcod character varying(3) COLLATE pg_catalog."default", -- รหัสงบ
|
||||
trncomstt character varying(3) COLLATE pg_catalog."default", -- สถานะ รอ หรือ เลือกงบแล้ว อณุมัติ
|
||||
trnacpdtm character(12) COLLATE pg_catalog."default", -- วันที่เข้าสู่ระบบ
|
||||
CONSTRAINT trnmst_pkey PRIMARY KEY (trnseq, trnprjnam)
|
||||
)
|
||||
|
||||
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;
|
||||
|
||||
|
||||
59
@knowleadge/dbchange/202511300710.txt
Normal file
59
@knowleadge/dbchange/202511300710.txt
Normal 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;
|
||||
63
@knowleadge/dbchange/alter new.txt
Normal file
63
@knowleadge/dbchange/alter new.txt
Normal 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;
|
||||
23
@knowleadge/dbchange/howtoalter table.txt
Normal file
23
@knowleadge/dbchange/howtoalter table.txt
Normal 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
20
Dockerfile
Normal 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
4
entrypoint
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
./start-login.sh &
|
||||
./start-ttc.sh &
|
||||
exec ./start-accountingwep.sh
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ export class accountingSearch {
|
||||
try {
|
||||
// let username = req.body.request.username;
|
||||
// let password = req.body.request.password;
|
||||
let token = req.body.request.token;
|
||||
const decoded = verifyToken(token);
|
||||
let token = req.headers.authorization?.split(' ')[1];''
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
let id = decoded.id
|
||||
let username = decoded.name
|
||||
database = decoded.organization
|
||||
let id = decoded.id
|
||||
let username = decoded.name
|
||||
database = decoded.organization
|
||||
|
||||
aryResult = await this.accountingSearchService.getAccountingSearch(database, id, username); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
|
||||
// this.generalService.devhint(1, 'accountingSearch.js', 'Login success');
|
||||
@@ -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
|
||||
@@ -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
|
||||
146
exthernal-accountingwep-api/src/controllers/reportController.js
Normal file
146
exthernal-accountingwep-api/src/controllers/reportController.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 นั้น ๆ
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export class AccountingSumService {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
async getCategoryColorMap(database) {
|
||||
const sql = `
|
||||
SELECT dtlcod, dtlnam, dtlmsc as dtlclr
|
||||
|
||||
43
exthernal-accountingwep-api/src/services/reportService.js
Normal file
43
exthernal-accountingwep-api/src/services/reportService.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 หรืออื่นๆ เรียกใช้ได้ด้วย
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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() }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
70
exthernal-login-api/src/interfaces/Interface.js
Normal file
70
exthernal-login-api/src/interfaces/Interface.js
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
86
exthernal-login-api/src/interfaces/table/actmstInterface.js
Normal file
86
exthernal-login-api/src/interfaces/table/actmstInterface.js
Normal 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)
|
||||
}
|
||||
}
|
||||
86
exthernal-login-api/src/interfaces/table/bdgmstInterface.js
Normal file
86
exthernal-login-api/src/interfaces/table/bdgmstInterface.js
Normal 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)
|
||||
}
|
||||
}
|
||||
86
exthernal-login-api/src/interfaces/table/usrmstInterface.js
Normal file
86
exthernal-login-api/src/interfaces/table/usrmstInterface.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
88
exthernal-login-api/src/services/registerservice OLD.js
Normal file
88
exthernal-login-api/src/services/registerservice OLD.js
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
25
exthernal-login-api/src/utils/validate.js
Normal file
25
exthernal-login-api/src/utils/validate.js
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
@@ -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 ออกมา
|
||||
|
||||
85
exthernal-ttc-api/src/controllers/budgetExpenseController.js
Normal file
85
exthernal-ttc-api/src/controllers/budgetExpenseController.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,6 @@ export class budgetSearch {
|
||||
let token = req.headers.authorization?.split(' ')[1];
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
let id = decoded.id
|
||||
let username = decoded.name
|
||||
database = decoded.organization
|
||||
let columnParams = req.query.column
|
||||
var column = ""
|
||||
@@ -38,10 +36,10 @@ 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, id, column, condition);
|
||||
aryResult = await this.budgetSearchService.getBudgetSearch(database, column, condition);
|
||||
|
||||
} catch (error) {
|
||||
idx = 1;
|
||||
|
||||
128
exthernal-ttc-api/src/controllers/projectAddController.js
Normal file
128
exthernal-ttc-api/src/controllers/projectAddController.js
Normal 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);
|
||||
}
|
||||
}
|
||||
114
exthernal-ttc-api/src/controllers/projectDownloadController.js
Normal file
114
exthernal-ttc-api/src/controllers/projectDownloadController.js
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
exthernal-ttc-api/src/controllers/projectEditController.js
Normal file
128
exthernal-ttc-api/src/controllers/projectEditController.js
Normal 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);
|
||||
}
|
||||
}
|
||||
98
exthernal-ttc-api/src/controllers/projectSearchController.js
Normal file
98
exthernal-ttc-api/src/controllers/projectSearchController.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ProjectSearchService } from '../services/projectSearchService.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 projectSearch {
|
||||
|
||||
constructor() {
|
||||
this.generalService = new GeneralService();
|
||||
this.projectSearchService = new ProjectSearchService();
|
||||
}
|
||||
|
||||
async onNavigate(req, res) {
|
||||
this.generalService.devhint(1, 'projectSearch.js', 'onNavigate() start');
|
||||
let organization = req.body.organization || 'dbo'; // Default Schema
|
||||
const prommis = await this.onProjectSearch(req, res, organization);
|
||||
return prommis;
|
||||
}
|
||||
|
||||
async onProjectSearch(req, res, database) {
|
||||
let idx = -1
|
||||
let aryResult = []
|
||||
let condition = {}
|
||||
let column = ""
|
||||
try {
|
||||
let token = req.headers.authorization?.split(' ')[1];
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
// ใช้ Organization จาก Token ถ้ามี
|
||||
database = decoded.organization || database
|
||||
|
||||
let columnParams = req.query.column
|
||||
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;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
idx = 1;
|
||||
} finally {
|
||||
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
|
||||
if (!aryResult || aryResult.length === 0) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
|
||||
return aryResult
|
||||
}
|
||||
}
|
||||
}
|
||||
165
exthernal-ttc-api/src/controllers/reportController.js
Normal file
165
exthernal-ttc-api/src/controllers/reportController.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
118
exthernal-ttc-api/src/controllers/socketController.js
Normal file
118
exthernal-ttc-api/src/controllers/socketController.js
Normal 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
84
exthernal-ttc-api/src/interfaces/table/prjmstInterface.js
Normal file
84
exthernal-ttc-api/src/interfaces/table/prjmstInterface.js
Normal 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)
|
||||
}
|
||||
}
|
||||
84
exthernal-ttc-api/src/interfaces/table/trnmstInterface.js
Normal file
84
exthernal-ttc-api/src/interfaces/table/trnmstInterface.js
Normal 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)
|
||||
}
|
||||
}
|
||||
94
exthernal-ttc-api/src/middlewares/uploadMiddleware.js
Normal file
94
exthernal-ttc-api/src/middlewares/uploadMiddleware.js
Normal 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()
|
||||
})
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import express from 'express'
|
||||
// import { budgetSetup } from '../controllers/budgetSetupController.js'
|
||||
import { budgetSearch } from '../controllers/budgetSearchController.js'
|
||||
import { budgetAdd } from '../controllers/budgetAddController.js'
|
||||
|
||||
// import { authMiddleware } from '../middlewares/auth.js'
|
||||
// import { sendResponse } from '../utils/response.js'
|
||||
import { projectSearch } from '../controllers/projectSearchController.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_put = 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_budgetAdd_post = new budgetAdd()
|
||||
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_put.onNavigate(req, res)
|
||||
const result = await controller_budgetAdd_post.onNavigate(req, res)
|
||||
if (result) return res.json(result)
|
||||
})
|
||||
|
||||
@@ -25,15 +29,39 @@ router.post('/budgetsearch', async (req, res) => {
|
||||
if (result) return res.json(result)
|
||||
})
|
||||
|
||||
// router.get('/projectsearch', async (req, res) => {
|
||||
// const result = await controller_budgetSetup_post.onNavigate(req, res)
|
||||
// if (result) return res.json(result)
|
||||
// })
|
||||
router.post('/projectsearch', async (req, res) => {
|
||||
const result = await controller_projectSearch_post.onNavigate(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
|
||||
@@ -13,9 +13,23 @@ export class BudgetAddService {
|
||||
bdgnam
|
||||
FROM ${database}.bdgmst
|
||||
WHERE bdgnam = $1
|
||||
`
|
||||
`
|
||||
const params = [name]
|
||||
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;
|
||||
}
|
||||
}
|
||||
144
exthernal-ttc-api/src/services/budgetExpenseService.js
Normal file
144
exthernal-ttc-api/src/services/budgetExpenseService.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export class BudgetSearchService {
|
||||
this.generalService = new GeneralService()
|
||||
}
|
||||
|
||||
async getBudgetSearch(database, id, column, condition) {
|
||||
async getBudgetSearch(database, column, condition) {
|
||||
const sql = `
|
||||
SELECT
|
||||
${column}
|
||||
|
||||
34
exthernal-ttc-api/src/services/projectAddService.js
Normal file
34
exthernal-ttc-api/src/services/projectAddService.js
Normal 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
|
||||
}
|
||||
}
|
||||
34
exthernal-ttc-api/src/services/projectEditService.js
Normal file
34
exthernal-ttc-api/src/services/projectEditService.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { GeneralService } from '../share/generalservice.js';
|
||||
|
||||
export class ProjectSearchService {
|
||||
|
||||
constructor() {
|
||||
this.generalService = new GeneralService();
|
||||
}
|
||||
|
||||
// 🟢 ฟังก์ชันเดิม (Simple Search) - คืนสภาพเดิมเพื่อไม่ให้กระทบ Service อื่น
|
||||
// ใช้สำหรับค้นหาข้อมูลในตาราง prjmst อย่างเดียว
|
||||
async getProjectSearch(database, column, condition) {
|
||||
const selectCol = column || '*';
|
||||
const sql = `
|
||||
SELECT ${selectCol}
|
||||
FROM ${database}.prjmst
|
||||
WHERE 1=1
|
||||
`;
|
||||
return await this.generalService.executeQueryConditions(database, sql, condition);
|
||||
}
|
||||
|
||||
// ดึงข้อมูล: ลำดับ, รหัส, ชื่อโครงการ, ผู้รับผิดชอบ, งบขอ, หมวดงบ, งบอนุมัติ, สถานะ
|
||||
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;
|
||||
}
|
||||
}
|
||||
45
exthernal-ttc-api/src/services/reportService.js
Normal file
45
exthernal-ttc-api/src/services/reportService.js
Normal 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;
|
||||
}
|
||||
}
|
||||
32
exthernal-ttc-api/src/services/socketService.js
Normal file
32
exthernal-ttc-api/src/services/socketService.js
Normal 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
|
||||
}
|
||||
}
|
||||
26
exthernal-ttc-api/src/services/transactionSearchService.js
Normal file
26
exthernal-ttc-api/src/services/transactionSearchService.js
Normal 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
|
||||
80
exthernal-ttc-api/src/socket/socketManager.js
Normal file
80
exthernal-ttc-api/src/socket/socketManager.js
Normal 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}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
16
exthernal-ttc-api/src/utils/date.js
Normal file
16
exthernal-ttc-api/src/utils/date.js
Normal 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}`;
|
||||
}
|
||||
@@ -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
6
listen-pipe.sh
Normal 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
|
||||
@@ -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
3
start-accountingwep.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
cd /server/exthernal-accountingwep-api
|
||||
npm start
|
||||
3
start-login.sh
Normal file
3
start-login.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
cd /server/exthernal-login-api
|
||||
npm start
|
||||
3
start-ttc.sh
Normal file
3
start-ttc.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
cd /server/exthernal-ttc-api
|
||||
npm start
|
||||
16
xecel/node_modules/.bin/crc32
generated
vendored
16
xecel/node_modules/.bin/crc32
generated
vendored
@@ -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
17
xecel/node_modules/.bin/crc32.cmd
generated
vendored
@@ -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
28
xecel/node_modules/.bin/crc32.ps1
generated
vendored
@@ -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
16
xecel/node_modules/.bin/xlsx
generated
vendored
@@ -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
17
xecel/node_modules/.bin/xlsx.cmd
generated
vendored
@@ -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
28
xecel/node_modules/.bin/xlsx.ps1
generated
vendored
@@ -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
111
xecel/node_modules/.package-lock.json
generated
vendored
@@ -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
201
xecel/node_modules/adler-32/LICENSE
generated
vendored
@@ -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
140
xecel/node_modules/adler-32/README.md
generated
vendored
@@ -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
|
||||
|
||||
[](https://saucelabs.com/u/adler32)
|
||||
|
||||
[](https://github.com/SheetJS/js-adler32/actions)
|
||||
|
||||
[](https://coveralls.io/r/SheetJS/js-adler32?branch=master)
|
||||
|
||||
[](https://github.com/SheetJS/js-adler32)
|
||||
92
xecel/node_modules/adler-32/adler32.js
generated
vendored
92
xecel/node_modules/adler-32/adler32.js
generated
vendored
@@ -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;
|
||||
}));
|
||||
35
xecel/node_modules/adler-32/package.json
generated
vendored
35
xecel/node_modules/adler-32/package.json
generated
vendored
@@ -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" }
|
||||
}
|
||||
14
xecel/node_modules/adler-32/types/index.d.ts
generated
vendored
14
xecel/node_modules/adler-32/types/index.d.ts
generated
vendored
@@ -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;
|
||||
15
xecel/node_modules/adler-32/types/tsconfig.json
generated
vendored
15
xecel/node_modules/adler-32/types/tsconfig.json
generated
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
14
xecel/node_modules/adler-32/types/tslint.json
generated
vendored
14
xecel/node_modules/adler-32/types/tslint.json
generated
vendored
@@ -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
201
xecel/node_modules/cfb/LICENSE
generated
vendored
@@ -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
161
xecel/node_modules/cfb/README.md
generated
vendored
@@ -1,161 +0,0 @@
|
||||
# Container File Blobs
|
||||
|
||||
Pure JS implementation of various container file formats, including ZIP and CFB.
|
||||
|
||||
[](https://travis-ci.org/SheetJS/js-cfb)
|
||||
[](https://coveralls.io/r/SheetJS/js-cfb?branch=master)
|
||||
[](https://david-dm.org/sheetjs/js-cfb)
|
||||
[](https://npmjs.org/package/cfb)
|
||||
[](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
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
201
xecel/node_modules/cfb/dist/LICENSE
generated
vendored
@@ -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
1979
xecel/node_modules/cfb/dist/cfb.js
generated
vendored
File diff suppressed because it is too large
Load Diff
3
xecel/node_modules/cfb/dist/cfb.min.js
generated
vendored
3
xecel/node_modules/cfb/dist/cfb.min.js
generated
vendored
File diff suppressed because one or more lines are too long
1
xecel/node_modules/cfb/dist/cfb.min.map
generated
vendored
1
xecel/node_modules/cfb/dist/cfb.min.map
generated
vendored
File diff suppressed because one or more lines are too long
1856
xecel/node_modules/cfb/dist/xlscfb.js
generated
vendored
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
68
xecel/node_modules/cfb/package.json
generated
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
128
xecel/node_modules/cfb/types/index.d.ts
generated
vendored
128
xecel/node_modules/cfb/types/index.d.ts
generated
vendored
@@ -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;
|
||||
}
|
||||
15
xecel/node_modules/cfb/types/tsconfig.json
generated
vendored
15
xecel/node_modules/cfb/types/tsconfig.json
generated
vendored
@@ -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
1856
xecel/node_modules/cfb/xlscfb.flow.js
generated
vendored
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user