first commit

This commit is contained in:
2025-11-11 11:19:13 +07:00
parent c838b2a979
commit fe028d274b
64 changed files with 8125 additions and 0 deletions

95
plan-app/bydep.js Normal file
View File

@@ -0,0 +1,95 @@
const XLSX = require('xlsx');
const path = require('path');
function runBydep(inputPath, outputPath) {
return new Promise((resolve, reject) => {
if (!inputPath) {
return reject(new Error('❌ ไม่พบ path ของไฟล์ Excel'));
}
if (!outputPath) {
return reject(new Error('❌ ไม่พบ path สำหรับบันทึกไฟล์'));
}
console.log('📂 กำลังอ่านไฟล์ (bydep):', inputPath);
try {
// 📘 อ่านไฟล์ Excel
const excel = XLSX.readFile(path.resolve(inputPath));
const sheet = excel.Sheets[excel.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
// ⚙️ ฟังก์ชันลบตัวเลขและจุดนำหน้า
const cleanSource = (text) => {
if (!text) return '';
return text.toString().replace(/^\d+\.\s*/, '').trim();
};
// 🧮 เตรียมข้อมูลรวมตาม "แผนกวิชา"
const pivot = {};
data.forEach(row => {
const plan = row['แผนกวิชา'] || 'ไม่ระบุแผนกวิชา';
const source = cleanSource(row['แหล่งงบประมาณ']);
const budget = Number(row['งบประมาณ']) || 0;
if (!source || !plan) return;
if (!pivot[plan]) pivot[plan] = {};
if (!pivot[plan][source]) pivot[plan][source] = 0;
pivot[plan][source] += budget;
});
// 🧾 ดึงชื่อคอลัมน์ (แหล่งงบทั้งหมด)
const allSources = Array.from(
new Set(Object.values(pivot).flatMap(obj => Object.keys(obj)))
);
// 📊 ฟังก์ชัน format ตัวเลข
const formatNumber = (num) =>
num ? num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '';
// 🪄 สร้างตารางผลลัพธ์
const result = [];
let grandTotalAll = 0;
const columnTotals = {};
Object.entries(pivot).forEach(([plan, sources]) => {
const row = { 'แผนกวิชา': plan };
let rowTotal = 0;
allSources.forEach(source => {
const val = sources[source] || 0;
row[source] = val ? formatNumber(val) : '';
rowTotal += val;
columnTotals[source] = (columnTotals[source] || 0) + val;
});
grandTotalAll += rowTotal;
row['รวมทั้งหมด'] = formatNumber(rowTotal);
result.push(row);
});
// เพิ่มแถวสุดท้าย “รวมทั้งหมด”
const totalRow = { 'แผนกวิชา': 'รวมทั้งหมด' };
allSources.forEach(source => {
totalRow[source] = columnTotals[source] ? formatNumber(columnTotals[source]) : '';
});
totalRow['รวมทั้งหมด'] = formatNumber(grandTotalAll);
result.push(totalRow);
const newSheet = XLSX.utils.json_to_sheet(result);
const newBook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(newBook, newSheet, 'Summary');
XLSX.writeFile(newBook, outputPath); // ✅ ใช้ outputPath ที่รับเข้ามา
console.log(`✅ สรุปผลสำเร็จ → ${outputPath}`);
resolve(`✅ บันทึกไฟล์สำเร็จแล้ว`);
} catch (error) {
console.error('❌ เกิดข้อผิดพลาดใน bydep:', error);
reject(error);
}
});
}
module.exports = { runBydep };

95
plan-app/bypivot.js Normal file
View File

@@ -0,0 +1,95 @@
const XLSX = require('xlsx');
const path = require('path');
function runBypivot(inputPath, outputPath) {
return new Promise((resolve, reject) => {
if (!inputPath) {
return reject(new Error('❌ ไม่พบ path ของไฟล์ Excel'));
}
if (!outputPath) {
return reject(new Error('❌ ไม่พบ path สำหรับบันทึกไฟล์'));
}
console.log('📂 กำลังอ่านไฟล์ (bypivot):', inputPath);
try {
// 📘 อ่านไฟล์ Excel
const excel = XLSX.readFile(path.resolve(inputPath));
const sheet = excel.Sheets[excel.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
// ⚙️ ฟังก์ชันลบตัวเลขและจุดนำหน้า
const cleanSource = (text) => {
if (!text) return '';
return text.toString().replace(/^\d+\.\s*/, '').trim();
};
// 🧮 เตรียมข้อมูลรวมตาม "แผนงานในการขอซื้อของจ้าง"
const pivot = {};
data.forEach(row => {
const plan = row['แผนงานในการขอซื้อของจ้าง'] || 'ไม่ระบุแผนงาน';
const source = cleanSource(row['แหล่งงบประมาณ']);
const budget = Number(row['งบประมาณ']) || 0;
if (!source || !plan) return;
if (!pivot[plan]) pivot[plan] = {};
if (!pivot[plan][source]) pivot[plan][source] = 0;
pivot[plan][source] += budget;
});
// 🧾 ดึงชื่อคอลัมน์ (แหล่งงบทั้งหมด)
const allSources = Array.from(
new Set(Object.values(pivot).flatMap(obj => Object.keys(obj)))
);
// 📊 ฟังก์ชัน format ตัวเลข
const formatNumber = (num) =>
num ? num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '';
// 🪄 สร้างตารางผลลัพธ์
const result = [];
let grandTotalAll = 0;
const columnTotals = {};
Object.entries(pivot).forEach(([plan, sources]) => {
const row = { 'แผนงานในการขอซื้อของจ้าง': plan };
let rowTotal = 0;
allSources.forEach(source => {
const val = sources[source] || 0;
row[source] = val ? formatNumber(val) : '';
rowTotal += val;
columnTotals[source] = (columnTotals[source] || 0) + val;
});
grandTotalAll += rowTotal;
row['รวมทั้งหมด'] = formatNumber(rowTotal);
result.push(row);
});
// เพิ่มแถวสุดท้าย “รวมทั้งหมด”
const totalRow = { 'แผนงานในการขอซื้อของจ้าง': 'รวมทั้งหมด' };
allSources.forEach(source => {
totalRow[source] = columnTotals[source] ? formatNumber(columnTotals[source]) : '';
});
totalRow['รวมทั้งหมด'] = formatNumber(grandTotalAll);
result.push(totalRow);
const newSheet = XLSX.utils.json_to_sheet(result);
const newBook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(newBook, newSheet, 'Summary');
XLSX.writeFile(newBook, outputPath); // ✅ ใช้ outputPath ที่รับเข้ามา
console.log(`✅ สรุปผลสำเร็จ → ${outputPath}`);
resolve(`✅ บันทึกไฟล์สำเร็จแล้ว`);
} catch (error) {
console.error('❌ เกิดข้อผิดพลาดใน bypivot:', error);
reject(error);
}
});
}
module.exports = { runBypivot };

95
plan-app/bywork.js Normal file
View File

@@ -0,0 +1,95 @@
const XLSX = require('xlsx');
const path = require('path');
function runBywork(inputPath, outputPath) {
return new Promise((resolve, reject) => {
if (!inputPath) {
return reject(new Error('❌ ไม่พบ path ของไฟล์ Excel'));
}
if (!outputPath) {
return reject(new Error('❌ ไม่พบ path สำหรับบันทึกไฟล์'));
}
console.log('📂 กำลังอ่านไฟล์ (bywork):', inputPath);
try {
// 📘 อ่านไฟล์ Excel
const excel = XLSX.readFile(path.resolve(inputPath));
const sheet = excel.Sheets[excel.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
// ⚙️ ฟังก์ชันลบตัวเลขและจุดนำหน้า
const cleanSource = (text) => {
if (!text) return '';
return text.toString().replace(/^\d+\.\s*/, '').trim();
};
// 🧮 เตรียมข้อมูลรวมตาม "งาน"
const pivot = {};
data.forEach(row => {
const plan = row['งาน'] || 'ไม่ระบุงาน';
const source = cleanSource(row['แหล่งงบประมาณ']);
const budget = Number(row['งบประมาณ']) || 0;
if (!source || !plan) return;
if (!pivot[plan]) pivot[plan] = {};
if (!pivot[plan][source]) pivot[plan][source] = 0;
pivot[plan][source] += budget;
});
// 🧾 ดึงชื่อคอลัมน์ (แหล่งงบทั้งหมด)
const allSources = Array.from(
new Set(Object.values(pivot).flatMap(obj => Object.keys(obj)))
);
// 📊 ฟังก์ชัน format ตัวเลข
const formatNumber = (num) =>
num ? num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '';
// 🪄 สร้างตารางผลลัพธ์
const result = [];
let grandTotalAll = 0;
const columnTotals = {};
Object.entries(pivot).forEach(([plan, sources]) => {
const row = { 'งาน': plan };
let rowTotal = 0;
allSources.forEach(source => {
const val = sources[source] || 0;
row[source] = val ? formatNumber(val) : '';
rowTotal += val;
columnTotals[source] = (columnTotals[source] || 0) + val;
});
grandTotalAll += rowTotal;
row['รวมทั้งหมด'] = formatNumber(rowTotal);
result.push(row);
});
// เพิ่มแถวสุดท้าย “รวมทั้งหมด”
const totalRow = { 'งาน': 'รวมทั้งหมด' };
allSources.forEach(source => {
totalRow[source] = columnTotals[source] ? formatNumber(columnTotals[source]) : '';
});
totalRow['รวมทั้งหมด'] = formatNumber(grandTotalAll);
result.push(totalRow);
const newSheet = XLSX.utils.json_to_sheet(result);
const newBook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(newBook, newSheet, 'Summary');
XLSX.writeFile(newBook, outputPath); // ✅ ใช้ outputPath ที่รับเข้ามา
console.log(`✅ สรุปผลสำเร็จ → ${outputPath}`);
resolve(`✅ บันทึกไฟล์สำเร็จแล้ว`);
} catch (error) {
console.error('❌ เกิดข้อผิดพลาดใน bywork:', error);
reject(error);
}
});
}
module.exports = { runBywork };

253
plan-app/index.html Normal file
View File

@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>โปรแกรมจำแนกงบประมาณ</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#f4f7fb;
--card:#ffffff;
--primary:#2563eb;
--muted:#6b7280;
--accent:#10b981;
--danger:#ef4444;
--shadow: 0 6px 18px rgba(31,41,55,0.06);
--radius:12px;
}
*{box-sizing:border-box}
body{
font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background: linear-gradient(180deg,#f7fbff 0%,var(--bg) 60%);
margin:0;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:28px;
color:#0f172a;
}
.container{
width:100%;
max-width:920px;
background:var(--card);
border-radius:var(--radius);
box-shadow:var(--shadow);
padding:28px;
display:grid;
grid-template-columns: 1fr 360px;
gap:20px;
align-items:start;
}
header{
grid-column:1 / -1;
display:flex;
gap:16px;
align-items:center;
}
.logo{
width:56px;
height:56px;
border-radius:12px;
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
display:flex;
align-items:center;
justify-content:center;
color:#111827;
font-weight:700;
font-size:20px;
box-shadow: 0 6px 18px rgba(15,23,42,0.06);
border: 1px solid rgba(15,23,42,0.04);
}
h1{
margin:0;
font-size:18px;
letter-spacing:-0.2px;
}
p.lead{
margin:4px 0 0 0;
font-size:13px;
color:var(--muted);
}
.main{
padding-top:6px;
}
.step{
background:linear-gradient(180deg, #fff, #fbfdff);
border-radius:10px;
padding:14px;
margin-bottom:12px;
border:1px solid rgba(15,23,42,0.04);
}
.step h3{
margin:0 0 6px 0;
font-size:14px;
display:flex;
align-items:center;
gap:8px;
}
.file-row{
display:flex;
gap:10px;
align-items:center;
}
input[type="text"]{
flex:1;
padding:10px 12px;
border-radius:8px;
border:1px solid rgba(15,23,42,0.06);
background:#fbfdff;
color:#0f172a;
font-size:14px;
}
button, select{
border:0;
background:var(--primary);
color:#fff;
padding:10px 14px;
border-radius:8px;
cursor:pointer;
font-weight:600;
font-size:14px;
transition:transform .08s ease, box-shadow .08s ease;
box-shadow: 0 6px 14px rgba(37,99,235,0.12);
}
button.secondary{
background:#fff;
color:#0f172a;
border:1px solid rgba(15,23,42,0.06);
box-shadow:none;
}
button:active{ transform:translateY(1px) }
select{
width:100%;
background:linear-gradient(180deg,#ffffff,#fbfdff);
color:#0f172a;
text-align:left;
appearance:none;
}
.side{
padding:12px;
border-radius:10px;
background:linear-gradient(180deg,#fff,#fbfdff);
border:1px solid rgba(15,23,42,0.04);
height:100%;
display:flex;
flex-direction:column;
gap:12px;
}
.export-btn{
display:flex;
gap:8px;
align-items:center;
justify-content:center;
padding:12px;
font-size:16px;
border-radius:10px;
background:linear-gradient(90deg,var(--accent),#059669);
box-shadow:0 10px 30px rgba(16,185,129,0.12);
border:0;
}
.status{
font-size:13px;
color:var(--muted);
min-height:36px;
}
.small{
font-size:12px;
color:var(--muted);
}
.hint{
display:flex;
gap:10px;
align-items:center;
font-size:13px;
color:var(--muted);
}
footer{
grid-column:1 / -1;
margin-top:8px;
text-align:right;
font-size:12px;
color:var(--muted);
}
@media (max-width:880px){
.container{ grid-template-columns: 1fr; padding:18px; }
.logo{ width:48px;height:48px }
}
</style>
</head>
<body>
<div class="container" role="application" aria-label="โปรแกรมจำแนกงบประมาณ">
<header>
<div class="logo" style="overflow:hidden;">
<img src="./logo.ico" alt="BC" style="width:100%;height:100%;object-fit:contain;display:block;border-radius:inherit;">
</div>
<div>
<h1>📊 โปรแกรมจำแนกงบประมาณ — รุ่นทดสอบ</h1>
<p class="lead">อัปโหลดไฟล์ Excel แล้วเลือกวิธีการจำแนก จากนั้นกด Export เพื่อสร้างไฟล์ผลลัพธ์</p>
</div>
</header>
<main class="main">
<section class="step" aria-labelledby="step1">
<h3 id="step1">1. เลือกไฟล์ Excel</h3>
<div class="file-row">
<input type="text" id="filePath" readonly placeholder="ยังไม่ได้เลือกไฟล์">
<button id="btnSelect" class="secondary" title="เลือกไฟล์">เลือกไฟล์</button>
</div>
<p class="small" style="margin-top:8px">รองรับ .xlsx, .xls — สามารถลากไฟล์มาวางได้ (ถ้า renderer รองรับ)</p>
</section>
<section class="step" aria-labelledby="step2">
<h3 id="step2">2. เลือกรูปแบบการจำแนก</h3>
<div style="display:grid;grid-template-columns:1fr;gap:8px;">
<select id="mode" aria-label="เลือกรูปแบบการจำแนก">
<option value="bywork.js">ตามงาน</option>
<option value="bydep.js">ตามแผนกวิชา</option>
<option value="bypivot.js">ตามแผนงานในการขอซื้อของจ้าง</option>
</select>
</div>
</section>
<section class="step" aria-labelledby="step3">
<h3 id="step3">3. สร้างไฟล์ผลลัพธ์</h3>
<p class="small">หลังจากกด Export ระบบจะประมวลผลและดาวน์โหลดไฟล์ Excel ที่แบ่งหมวดงบประมาณแล้ว</p>
</section>
</main>
<aside class="side" aria-labelledby="actions">
<div id="actions">
<button id="btnRun" class="export-btn" title="Export Excel">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" style="margin-right:6px" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M12 3v12" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 7.5L12 3l3.5 4.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="4" y="17" width="16" height="3" rx="1.2" fill="white"/>
</svg>
Export Excel
</button>
</div>
<div class="status" id="status">สถานะ: พร้อมใช้งาน</div>
<div class="hint">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2a10 10 0 100 20 10 10 0 000-20z" stroke="#9CA3AF" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 10h1v5h1" stroke="#9CA3AF" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7h.01" stroke="#9CA3AF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<div style="font-weight:600">คำแนะนำ</div>
<div style="color:var(--muted);margin-top:4px">ตรวจสอบความถูกต้องของคอลัมน์ในไฟล์ก่อน Export เพื่อผลลัพธ์ที่แม่นยำ</div>
</div>
</div>
</aside>
<footer>เวอร์ชันทดสอบ • เก็บไฟล์ต้นฉบับไว้ก่อนการใช้งาน</footer>
</div>
<script src="renderer.js"></script>
</body>
</html>

BIN
plan-app/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

67
plan-app/main.js Normal file
View File

@@ -0,0 +1,67 @@
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const { runBypivot } = require('./bypivot.js');
const { runBydep } = require('./bydep.js');
const { runBywork } = require('./bywork.js');
const scriptRunners = {
'bypivot.js': { runner: runBypivot, defaultName: 'output_แผนงานในการขอซื้อของจ้าง.xlsx' },
'bydep.js': { runner: runBydep, defaultName: 'output_แผนกวิชา.xlsx' },
'bywork.js': { runner: runBywork, defaultName: 'output_งาน.xlsx' },
};
function createWindow() {
const win = new BrowserWindow({
width: 650,
height: 450,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
win.loadFile(path.join(__dirname, 'index.html'));
// win.webContents.openDevTools();
}
ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({
title: 'เลือกไฟล์ Excel',
filters: [{ name: 'Excel Files', extensions: ['xlsx'] }],
properties: ['openFile']
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
return result.filePaths[0];
});
// ✅ ส่วนรันสคริปต์ที่ปรับปรุงใหม่ + เพิ่ม Save Dialog
ipcMain.handle('run-script', async (_, { script, filePath }) => {
const scriptInfo = scriptRunners[script];
if (!scriptInfo) {
throw new Error(`ไม่พบสคริปต์สำหรับ: ${script}`);
}
// 🚀 แสดงหน้าต่าง Save Dialog
const saveResult = await dialog.showSaveDialog({
title: 'เลือกตำแหน่งที่จะบันทึกไฟล์',
defaultPath: scriptInfo.defaultName,
filters: [{ name: 'Excel Files', extensions: ['xlsx'] }]
});
// ถ้าผู้ใช้ไม่เลือกที่บันทึก ให้ยกเลิก
if (saveResult.canceled || !saveResult.filePath) {
return 'ยกเลิกการบันทึก'; // ส่งข้อความกลับไปบอกสถานะ
}
const outputPath = saveResult.filePath;
// เรียกใช้ฟังก์ชันโดยตรงและส่งทั้ง input และ output path
return scriptInfo.runner(filePath, outputPath);
});
app.whenReady().then(createWindow);

5036
plan-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
plan-app/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "budget-classifier",
"version": "1.0.0",
"description": "โปรแกรมจำแนกงบประมาณ (Electron GUI)",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
},
"build": {
"appId": "com.yourcompany.budgetclassifier",
"productName": "Budget Classifier",
"win": {
"target": "nsis",
"icon": "./logo.ico"
},
"files": [
"**/*",
"!node_modules/.cache/**/*",
"!*.log"
]
},
"dependencies": {
"xlsx": "^0.18.5"
},
"devDependencies": {
"electron": "^32.0.0",
"electron-builder": "^24.13.3"
}
}

32
plan-app/renderer.js Normal file
View File

@@ -0,0 +1,32 @@
// renderer.js
const { ipcRenderer } = require('electron');
document.getElementById('btnSelect').addEventListener('click', async () => {
console.log('🟢 clicked select'); // Debug
const filePath = await ipcRenderer.invoke('select-file');
console.log('📁 File selected:', filePath);
if (filePath) document.getElementById('filePath').value = filePath;
});
document.getElementById('btnRun').addEventListener('click', async () => {
const filePath = document.getElementById('filePath').value;
const mode = document.getElementById('mode').value;
if (!filePath) {
document.getElementById('status').innerText = '⚠️ กรุณาเลือกไฟล์ Excel ก่อน';
return;
}
document.getElementById('status').innerText = 'กำลังประมวลผล...';
try {
const resultMessage = await ipcRenderer.invoke('run-script', { script: mode, filePath });
// ตรวจสอบข้อความที่ส่งกลับมาจาก main process
if (resultMessage === 'ยกเลิกการบันทึก') {
document.getElementById('status').innerText = 'สถานะ: ยกเลิกโดยผู้ใช้';
} else {
document.getElementById('status').innerText = `${resultMessage}`; // แสดงข้อความสำเร็จที่ได้รับ
}
} catch (err) {
document.getElementById('status').innerText = '❌ เกิดข้อผิดพลาด: ' + err;
}
});