forked from ttc/micro-frontend
first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*/dist
|
||||
*/node_modules
|
||||
95
plan-app/bydep.js
Normal file
95
plan-app/bydep.js
Normal 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
95
plan-app/bypivot.js
Normal 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
95
plan-app/bywork.js
Normal 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
253
plan-app/index.html
Normal 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
BIN
plan-app/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
67
plan-app/main.js
Normal file
67
plan-app/main.js
Normal 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
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
30
plan-app/package.json
Normal 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
32
plan-app/renderer.js
Normal 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;
|
||||
}
|
||||
});
|
||||
17
templete/.editorconfig
Normal file
17
templete/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
42
templete/.gitignore
vendored
Normal file
42
templete/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
5
templete/.postcssrc.json
Normal file
5
templete/.postcssrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
||||
4
templete/.vscode/extensions.json
vendored
Normal file
4
templete/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
||||
33
templete/.vscode/launch.json
vendored
Normal file
33
templete/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
// {
|
||||
// // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
// "version": "0.2.0",
|
||||
// "configurations": [
|
||||
// {
|
||||
// "name": "ng serve",
|
||||
// "type": "chrome",
|
||||
// "request": "launch",
|
||||
// "preLaunchTask": "npm: start",
|
||||
// "url": "http://localhost:4200/"
|
||||
// },
|
||||
// {
|
||||
// "name": "ng test",
|
||||
// "type": "chrome",
|
||||
// "request": "launch",
|
||||
// "preLaunchTask": "npm: test",
|
||||
// "url": "http://localhost:9876/debug.html"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Launch Chrome against localhost",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:4200",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
templete/.vscode/tasks.json
vendored
Normal file
42
templete/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
59
templete/README.md
Normal file
59
templete/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# AccountingNgNuttakit
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.18.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
123
templete/angular.json
Normal file
123
templete/angular.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"accounting-ng-nuttakit": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"standalone": false
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"standalone": false
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/accounting-ng-nuttakit",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"host": "0.0.0.0",
|
||||
"allowedHosts": [
|
||||
"accounting.nuttakit.work"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "accounting-ng-nuttakit:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "accounting-ng-nuttakit:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
10
templete/capacitor.config.ts
Normal file
10
templete/capacitor.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'accounting.nuttakit.work',
|
||||
appName: 'accounting-ng-nuttakit',
|
||||
webDir: "dist/accounting-ng-nuttakit/browser"
|
||||
bundledWebRuntime: false
|
||||
};
|
||||
|
||||
export default config;
|
||||
80
templete/package.json
Normal file
80
templete/package.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "accounting-ng-nuttakit",
|
||||
"version": "1.0.0",
|
||||
"main": "electron/main.js",
|
||||
"author": "Nuttakit",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"electron": "ng build --base-href ./ && electron .",
|
||||
"dist": "ng build --configuration production && electron-builder"
|
||||
},
|
||||
"build": {
|
||||
"appId": "accounting.nuttakit.work",
|
||||
"productName": "accounting-nuttakit",
|
||||
"asar": false,
|
||||
"directories": {
|
||||
"output": "dist_electron"
|
||||
},
|
||||
"files": [
|
||||
"dist/accounting-ng-nuttakit/browser/**/*",
|
||||
"electron/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": ["nsis", "msi"],
|
||||
"icon": "public/favicon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": true,
|
||||
"allowElevation": true,
|
||||
"runAfterFinish": false
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.15",
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@capacitor/android": "^7.4.4",
|
||||
"@capacitor/angular": "^2.0.3",
|
||||
"@capacitor/core": "latest",
|
||||
"@fortawesome/angular-fontawesome": "^1.0.0",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"bootstrap": "^5.3.8",
|
||||
"postcss": "^8.5.6",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.18",
|
||||
"@angular/cli": "^19.2.18",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@capacitor/cli": "latest",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"electron": "^39.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
}
|
||||
BIN
templete/public/favicon.ico
Normal file
BIN
templete/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
templete/public/logo.png
Normal file
BIN
templete/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
35
templete/src/app/app-routing.module.ts
Normal file
35
templete/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { SidebarContentComponent } from './content/sidebar-content/sidebar-content.component';
|
||||
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
|
||||
|
||||
const routes: Routes = [
|
||||
|
||||
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) },
|
||||
|
||||
{ path: 'license', component: LicensePrivacyTermsComponent},
|
||||
|
||||
{
|
||||
path: 'main',
|
||||
component: SidebarContentComponent, // ✅ ใช้ layout นี้โดยตรง
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./controls/main-control/main-control.module').then(
|
||||
(m) => m.MainControlModule
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
||||
|
||||
{ path: '**', redirectTo: 'login' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
0
templete/src/app/app.component.css
Normal file
0
templete/src/app/app.component.css
Normal file
1
templete/src/app/app.component.html
Normal file
1
templete/src/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
11
templete/src/app/app.component.ts
Normal file
11
templete/src/app/app.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
standalone: false,
|
||||
styleUrl: './app.component.css'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'accounting-ng-nuttakit';
|
||||
}
|
||||
45
templete/src/app/app.module.ts
Normal file
45
templete/src/app/app.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NgModule, Component } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
// import { LayoutComponent } from './content/content/layout/layout.component';
|
||||
import { SidebarContentComponent } from './content/sidebar-content/sidebar-content.component';
|
||||
import { SidebarComponent } from './component/sidebar/sidebar.component';
|
||||
// import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
|
||||
import { MainDashboardContentComponent } from './content/main-dashboard-content/main-dashboard-content.component';
|
||||
// import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component';
|
||||
// import { LoginForgotComponent } from './component/login-forgot/login-forgot.component';
|
||||
// import { LoginPageComponent } from './component/login-page/login-page.component';
|
||||
// import { LoginContentComponent } from './content/login-content/login-content.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
// LayoutComponent,
|
||||
SidebarContentComponent,
|
||||
SidebarComponent,
|
||||
LicensePrivacyTermsComponent,
|
||||
MainDashboardContentComponent,
|
||||
// MainDashboardComponent,
|
||||
// LoginForgotComponent,
|
||||
// LoginPageComponent,
|
||||
// LoginPageComponentComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
// ReactiveFormsModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
FontAwesomeModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,68 @@
|
||||
.policy-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(135deg, #f3f6f9 0%, #e9eff5 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
color: #1a1f36;
|
||||
font-family: "Sarabun", "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
|
||||
padding: 40px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #6b737a;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
color: #0b1a2b;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0078d4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #6b737a;
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<div class="policy-container">
|
||||
<div class="card">
|
||||
<h1 class="page-title">ข้อตกลงสิทธิ์การใช้งาน นโยบายความเป็นส่วนตัว และเงื่อนไขการให้บริการ</h1>
|
||||
<p class="subtitle">ปรับปรุงล่าสุด: 27 ตุลาคม 2025</p>
|
||||
|
||||
<section>
|
||||
<h2>1. ข้อตกลงสิทธิ์การใช้งาน (License Agreement)</h2>
|
||||
<p>
|
||||
ซอร์สโค้ด ส่วนประกอบ และทรัพย์สินการออกแบบทั้งหมดภายใต้โครงการ Nuttakit Software
|
||||
อยู่ภายใต้สัญญาอนุญาตแบบ <strong>MIT License</strong> เว้นแต่จะมีการระบุเป็นอย่างอื่นโดยเฉพาะ
|
||||
</p>
|
||||
<p>
|
||||
ท่านได้รับสิทธิ์ในการใช้งาน คัดลอก แก้ไข รวม รวมเข้ากับซอฟต์แวร์อื่น เผยแพร่ หรือแจกจ่ายซอฟต์แวร์นี้
|
||||
เพื่อวัตถุประสงค์ส่วนตัวหรือเชิงพาณิชย์ได้
|
||||
โดยต้องคงไว้ซึ่งข้อความลิขสิทธิ์และข้อความอนุญาตนี้ในสำเนาทั้งหมดของซอฟต์แวร์
|
||||
</p>
|
||||
<p>
|
||||
ซอฟต์แวร์นี้ถูกจัดให้ “ตามสภาพ” (AS IS)
|
||||
โดยไม่มีการรับประกันใด ๆ ไม่ว่าจะโดยชัดแจ้งหรือโดยนัย
|
||||
รวมถึงแต่ไม่จำกัดเฉพาะการรับประกันความเหมาะสมในการใช้งานหรือความถูกต้องของข้อมูล
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. นโยบายความเป็นส่วนตัว (Privacy Policy)</h2>
|
||||
<p>
|
||||
NuttakitSoftwareให้ความสำคัญกับความเป็นส่วนตัวของผู้ใช้งาน
|
||||
แอปพลิเคชันของเราจะเก็บข้อมูลเพียงบางส่วนเท่านั้น เช่น รหัสอุปกรณ์
|
||||
บันทึกการทำงาน หรือสถิติการใช้งาน
|
||||
เพื่อใช้ในการวิเคราะห์ ปรับปรุง และพัฒนาประสิทธิภาพของระบบ
|
||||
</p>
|
||||
<p>
|
||||
ข้อมูลส่วนบุคคล (Personal Identifiable Information — PII)
|
||||
จะไม่ถูกขาย แบ่งปัน หรือโอนไปยังบุคคลที่สามโดยไม่ได้รับความยินยอมจากท่านอย่างชัดเจน
|
||||
</p>
|
||||
<p>
|
||||
เราใช้มาตรฐานการเข้ารหัสระดับอุตสาหกรรม (AES-CBC)
|
||||
เพื่อรักษาความปลอดภัยในการส่งข้อมูลระหว่างแอปพลิเคชัน เซิร์ฟเวอร์ และ API
|
||||
</p>
|
||||
<p>
|
||||
ผู้ใช้สามารถร้องขอให้ลบหรือขอรับสำเนาข้อมูลของตนเองได้ตลอดเวลา
|
||||
โดยติดต่อทีมสนับสนุนของเรา
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. เงื่อนไขการให้บริการ (Terms of Service)</h2>
|
||||
<p>
|
||||
เมื่อท่านใช้งานซอฟต์แวร์หรือบริการของเรา ถือว่าท่านยอมรับและปฏิบัติตามกฎหมายและข้อบังคับที่เกี่ยวข้องทั้งหมด
|
||||
</p>
|
||||
<p>
|
||||
ท่านจะต้องไม่ใช้ซอฟต์แวร์ของเราในทางที่ผิด
|
||||
ไม่พยายามถอดรหัส แก้ไข ดัดแปลง หรือแสวงหาประโยชน์จากช่องโหว่ในระบบโดยไม่ได้รับอนุญาต
|
||||
</p>
|
||||
<p>
|
||||
Nuttakit ขอสงวนสิทธิ์ในการแก้ไขหรือยุติการให้บริการโดยไม่ต้องแจ้งให้ทราบล่วงหน้า
|
||||
หากตรวจพบการละเมิดความปลอดภัยหรือการใช้งานที่ไม่เหมาะสม
|
||||
</p>
|
||||
<p>
|
||||
ทีมพัฒนา Nuttakit และผู้ร่วมพัฒนาไม่รับผิดชอบต่อความเสียหายใด ๆ
|
||||
ที่อาจเกิดจากการใช้งานหรือไม่สามารถใช้งานซอฟต์แวร์นี้ได้
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. ช่องทางการติดต่อ (Contact)</h2>
|
||||
<p>
|
||||
หากท่านมีข้อสงสัยหรือข้อกังวลเกี่ยวกับข้อมูลส่วนบุคคลหรือเงื่อนไขการให้บริการ
|
||||
สามารถติดต่อเราได้ที่:
|
||||
<br />
|
||||
<strong>อีเมล:</strong> support@nuttakit.work<br />
|
||||
<strong>เว็บไซต์:</strong>
|
||||
<a href="https://nuttakit.work" target="_blank">https://nuttakit.work</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<hr />
|
||||
<p class="footer-text">© 2025 Nuttakit Software. สงวนลิขสิทธิ์ทั้งหมด</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-license-privacy-terms',
|
||||
standalone: false,
|
||||
templateUrl: './license-privacy-terms.component.html',
|
||||
styleUrl: './license-privacy-terms.component.css'
|
||||
})
|
||||
export class LicensePrivacyTermsComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
:root {
|
||||
--bg-1: #f3f6f9;
|
||||
--card-bg: #ffffff;
|
||||
--muted: #6b737a;
|
||||
--text: #0b1a2b;
|
||||
--primary: #0078d4;
|
||||
--primary-600: #0065b8;
|
||||
--radius: 8px;
|
||||
--shadow: 0 10px 30px rgba(11,26,43,0.08);
|
||||
--glass: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
/* Page layout */
|
||||
.login-widget {
|
||||
/* Fill the viewport and center the card. Do not let the page itself
|
||||
scroll; the card gets an internal max-height instead. */
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px 18px;
|
||||
background: linear-gradient(180deg, #f7f9fb 0%, var(--bg-1) 100%);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
/* Card */
|
||||
.login-widget .card{
|
||||
width: 380px;
|
||||
max-width: calc(100% - 40px);
|
||||
background: var(--card-bg);
|
||||
border-radius: calc(var(--radius) + 2px);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
/* Constrain the card so it never forces the page to scroll. If content
|
||||
grows, the card will scroll internally. */
|
||||
max-height: calc(100vh - 56px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Modal/backdrop styles */
|
||||
.login-backdrop{
|
||||
position: fixed;
|
||||
inset: 0; /* top:0; right:0; bottom:0; left:0; */
|
||||
background: rgba(0,0,0,0.38);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1040;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-modal{ width: 480px; max-width: 480px; }
|
||||
|
||||
.modal-card{
|
||||
border-radius: 12px;
|
||||
padding: 0; /* card children control internal padding */
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
||||
}
|
||||
|
||||
/* Slightly larger brand area inside modal */
|
||||
.modal-card .brand{ padding: 18px; }
|
||||
|
||||
/* Make the primary button pill-shaped and slightly larger */
|
||||
button.primary{
|
||||
color: #000;
|
||||
border-radius: 999px;
|
||||
padding: 10px 18px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Make biometric and other action buttons visually lighter */
|
||||
.biometric{
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* On small screens reduce modal padding and width to avoid overflow */
|
||||
@media (max-width: 420px){
|
||||
.login-backdrop{ padding: 12px; }
|
||||
.login-modal{ max-width: 100%; }
|
||||
.modal-card .brand{ padding: 12px; }
|
||||
}
|
||||
|
||||
/* Brand area */
|
||||
.brand{
|
||||
text-align: center;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #eef2f5;
|
||||
}
|
||||
.brand .logo{
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
object-fit: contain;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.brand h1{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.2px;
|
||||
color: var(--text);
|
||||
}
|
||||
.brand .subtitle{
|
||||
margin: 6px 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Form area */
|
||||
.form{
|
||||
/* keep compact spacing inside the card */
|
||||
/* width: 410px; */
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 6px 0 2px;
|
||||
}
|
||||
|
||||
/* Field label wrapper */
|
||||
.field{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.field .label-text{
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="text"]{
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 12px;
|
||||
font-size: 15px;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
border: 1px solid #d8dee6;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: box-shadow .14s ease, border-color .14s ease, transform .06s ease;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input::placeholder{
|
||||
color: #9aa3ad;
|
||||
}
|
||||
input:focus{
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 6px 20px rgba(0,120,212,0.10);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Checkbox / stay signed */
|
||||
.stay-signed{
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.stay-signed input[type="checkbox"]{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Actions row */
|
||||
.actions{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
button.primary{
|
||||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
||||
color: #000000;
|
||||
border: none;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
||||
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
||||
}
|
||||
button.primary:hover:not(:disabled){
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
||||
}
|
||||
button.primary:active{
|
||||
transform: translateY(0);
|
||||
}
|
||||
button.primary:disabled{
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Alternative options */
|
||||
.alt-options{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.biometric{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(0,120,212,0.14);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.biometric svg{ display: block; opacity: .95; }
|
||||
.biometric:hover{
|
||||
background: rgba(0,120,212,0.04);
|
||||
}
|
||||
|
||||
/* Help link */
|
||||
.help-link{
|
||||
margin-left: auto;
|
||||
font-size: 13px;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.help-link:hover{ text-decoration: underline; }
|
||||
|
||||
/* Footer */
|
||||
.footer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eef2f5;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.footer a{
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.footer a:hover{ text-decoration: underline; }
|
||||
.divider{ color: #d0d6db; }
|
||||
|
||||
/* Focus styles for keyboard users */
|
||||
:focus{
|
||||
outline: none;
|
||||
}
|
||||
:focus-visible{
|
||||
outline: 3px solid rgba(0,120,212,0.12);
|
||||
outline-offset: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Small screens */
|
||||
@media (max-width:420px){
|
||||
.login-widget .card{
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
.brand h1{ font-size: 18px; }
|
||||
.brand .subtitle{
|
||||
font-family: "Kanit";
|
||||
font-weight: 1000;
|
||||
font-style: normal; }
|
||||
.biometric span, .primary{ font-size: 13px; }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<div class="login-backdrop">
|
||||
<div class="login-modal d-flex align-items-center justify-content-center ">
|
||||
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||
<div class="brand">
|
||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
||||
<h1 id="signin-title" class='kanit-bold'>ลืมรหัสผ่าน</h1>
|
||||
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
|
||||
</div>
|
||||
<form [formGroup]="forgotFrm" class="form px-3 pb-3 login-mobile">
|
||||
<label class="field">
|
||||
<span class="label-text ">อีเมล์</span>
|
||||
<input type="email" formControlName="email" class="px-2 " id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
|
||||
</label>
|
||||
|
||||
@if (isSendOtp === true) {
|
||||
<label class="field">
|
||||
<span class="label-text">รหัสยืนยัน OTP</span>
|
||||
<input type="email" formControlName="otp" autocomplete="otp" placeholder="123456" alt required/>
|
||||
</label>
|
||||
}
|
||||
<!-- <div class="justify-end flex"> -->
|
||||
<!-- <label class="stay-signed">
|
||||
<input type="checkbox" formControlName="remember" />
|
||||
<span>จดจำรหัสผ่าน</span>
|
||||
</label> -->
|
||||
<div class="flex flex-row gap-2 mt-4 justify-end">
|
||||
<div class="flex-row hover:-translate-y-2.5 transform transition-all">
|
||||
<button class="bg-[linear-gradient(180deg,var(--primary)_0%,var(--primary-600)_100%)]
|
||||
text-black
|
||||
border-0
|
||||
px-3.5 py-2.5
|
||||
rounded-md
|
||||
font-semibold
|
||||
cursor-pointer
|
||||
text-[14px]
|
||||
shadow-[0_6px_18px_var(--color-blue-500)]
|
||||
transition
|
||||
ease-in-out
|
||||
duration-100
|
||||
hover:scale-[1.02]
|
||||
active:opacity-90" (click)="isModalOpen = true">
|
||||
เปิด Modal
|
||||
</button>
|
||||
</div>
|
||||
@if (isSendOtp === false) {
|
||||
<div class="flex justify-end">
|
||||
@if (isLoading === true) {
|
||||
<button type="submit" class="primary cursor-progress!" disabled>
|
||||
กำลังส่ง...
|
||||
</button>
|
||||
} @else {
|
||||
<button type="submit" class="primary" (click)="onSubmin()">
|
||||
{{ 'ยืนยันส่ง OTP รีเซ็ตรหัสผ่าน' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else if(isSendOtp === true) {
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" class="primary" (click)="onSubmin()">
|
||||
{{ 'ส่งอีกครั้ง' }}
|
||||
</button>
|
||||
<button type="submit" class="primary" (click)="onVerifySubmit()">
|
||||
{{ 'รีเซ็ตรหัสผ่าน' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- <button mat-raised-button color="primary" [disabled]="isLoading">
|
||||
{{ isLoading ? 'กำลังส่ง...' : 'ส่งรหัส OTP' }}
|
||||
</button> -->
|
||||
<!-- } -->
|
||||
<!-- </div> -->
|
||||
</form>
|
||||
@if(isModalOpen){
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50" [formGroup]="forgotFrm">
|
||||
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-fit">
|
||||
<h2 class="text-xl font-bold mb-4">เปลี่ยนรหัสผ่าน</h2>
|
||||
<hr class="w-full h-1 bg-gray-300 rounded-sm shadow-neutral-400 md:my-1">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">รหัสผ่านใหม่</label>
|
||||
<input type="password" id="newPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" formControlName="newPassword" placeholder="กรอกรหัสผ่านใหม่">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ยืนยันรหัสผ่านใหม่</label>
|
||||
<input type="password" id="confirmPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" formControlName="confirmPassword" placeholder="กรอกยืนยันรหัสผ่านใหม่">
|
||||
@if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
|
||||
<span class="text-red-600 md">รหัสผ่านไม่ตรงกัน</span>
|
||||
}
|
||||
</div>
|
||||
<!-- <hr class="w-full h-[] bg-gray-100 border-0 rounded-sm md:my-1 dark:bg-gray-700"> -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="bg-red-500 text-white px-4 py-2 rounded" (click)="isModalOpen = false">
|
||||
ปิด
|
||||
</button>
|
||||
<button class="bg-green-500 text-white px-4 py-2 rounded shadow-[0_1px_18px_var(--color-green-300)] hover:-translate-y-1.5 hover:shadow-[0_6px_18px_var(--color-green-500)] transition-all duration-500 ease-in-out" (click)="onSetNewPassword()">
|
||||
ยืนยัน
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||
import { GeneralService } from '../../services/generalservice';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-forgot',
|
||||
standalone: false,
|
||||
templateUrl: './login-forgot.component.html',
|
||||
styleUrl: './login-forgot.component.css'
|
||||
})
|
||||
export class LoginForgotComponent implements OnInit {
|
||||
// @Input() brandName = 'Contoso';
|
||||
// @Input() subtitle = 'to your account';
|
||||
// @Input() mode = '';
|
||||
@Output() otpEventSubmit = new EventEmitter<any>();
|
||||
@Output() otpVerifyEventSubmit = new EventEmitter<any>();
|
||||
|
||||
forgotFrm!: FormGroup;
|
||||
isLoading: boolean = false;
|
||||
isSendOtp: boolean = false;
|
||||
isModalOpen: boolean = false;
|
||||
// busy = false;
|
||||
// message = '';
|
||||
|
||||
constructor(
|
||||
// private generalService: GeneralService
|
||||
// private fb: FormBuilder
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupFormControl();
|
||||
}
|
||||
|
||||
setupFormControl(){
|
||||
this.forgotFrm = new FormGroup({
|
||||
email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
|
||||
otp: new FormControl('',[Validators.required, Validators.maxLength(6)]),
|
||||
newPassword: new FormControl('',[Validators.required, Validators.maxLength(50)]),
|
||||
confirmPassword: new FormControl('',[Validators.required, Validators.maxLength(50)])
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
EventSubmit(event: any){
|
||||
this.otpEventSubmit.emit(event);
|
||||
}
|
||||
|
||||
|
||||
VerifyEventSubmit(event: any){
|
||||
this.otpVerifyEventSubmit.emit(event);
|
||||
}
|
||||
|
||||
onSubmin(){
|
||||
let data = {
|
||||
email: this.forgotFrm.get('email')?.value
|
||||
}
|
||||
|
||||
this.EventSubmit(data);
|
||||
}
|
||||
|
||||
onVerifySubmit(){
|
||||
let data = {
|
||||
email: this.forgotFrm.get('email')?.value,
|
||||
otp: this.forgotFrm.get('otp')?.value
|
||||
}
|
||||
this.VerifyEventSubmit(data);
|
||||
}
|
||||
|
||||
onSetNewPassword(){
|
||||
let newpassword = this.forgotFrm.get('newPassword')?.value;
|
||||
let confirmPassword = this.forgotFrm.get('confirmPassword')?.value;
|
||||
|
||||
let data = {
|
||||
email: this.forgotFrm.get('email')?.value,
|
||||
otp: this.forgotFrm.get('otp')?.value,
|
||||
newPassword: newpassword
|
||||
}
|
||||
|
||||
if (newpassword.trim() === confirmPassword.trim()) {
|
||||
// this.VerifyEventSubmit(data);
|
||||
console.log("Password matched! (รหัสผ่านตรงกัน)");
|
||||
} else {
|
||||
console.error("Password mismatched! (รหัสผ่านไม่ตรงกัน)");
|
||||
}
|
||||
|
||||
// console.log(newpassword.value);
|
||||
|
||||
}
|
||||
// otp: }
|
||||
}
|
||||
289
templete/src/app/component/login-page/login-page.component.css
Normal file
289
templete/src/app/component/login-page/login-page.component.css
Normal file
@@ -0,0 +1,289 @@
|
||||
:root {
|
||||
--bg-1: #f3f6f9;
|
||||
--card-bg: #ffffff;
|
||||
--muted: #6b737a;
|
||||
--text: #0b1a2b;
|
||||
--primary: #0078d4;
|
||||
--primary-600: #0065b8;
|
||||
--radius: 8px;
|
||||
--shadow: 0 10px 30px rgba(11,26,43,0.08);
|
||||
--glass: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
/* Page layout */
|
||||
.login-widget {
|
||||
/* Fill the viewport and center the card. Do not let the page itself
|
||||
scroll; the card gets an internal max-height instead. */
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px 18px;
|
||||
background: linear-gradient(180deg, #f7f9fb 0%, var(--bg-1) 100%);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
/* Card */
|
||||
.login-widget .card{
|
||||
width: 380px;
|
||||
max-width: calc(100% - 40px);
|
||||
background: var(--card-bg);
|
||||
border-radius: calc(var(--radius) + 2px);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
/* Constrain the card so it never forces the page to scroll. If content
|
||||
grows, the card will scroll internally. */
|
||||
max-height: calc(100vh - 56px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Modal/backdrop styles */
|
||||
.login-backdrop{
|
||||
position: fixed;
|
||||
inset: 0; /* top:0; right:0; bottom:0; left:0; */
|
||||
background: rgba(0,0,0,0.38);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1040;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-modal{ width: 480px; max-width: 480px; }
|
||||
|
||||
.modal-card{
|
||||
border-radius: 12px;
|
||||
padding: 0; /* card children control internal padding */
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
||||
}
|
||||
|
||||
/* Slightly larger brand area inside modal */
|
||||
.modal-card .brand{ padding: 18px; }
|
||||
|
||||
/* Make the primary button pill-shaped and slightly larger */
|
||||
button.primary{
|
||||
color: #000;
|
||||
border-radius: 999px;
|
||||
padding: 10px 18px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Make biometric and other action buttons visually lighter */
|
||||
.biometric{
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* On small screens reduce modal padding and width to avoid overflow */
|
||||
@media (max-width: 420px){
|
||||
.login-backdrop{ padding: 12px; }
|
||||
.login-modal{ max-width: 100%; }
|
||||
.modal-card .brand{ padding: 12px; }
|
||||
}
|
||||
|
||||
/* Brand area */
|
||||
.brand{
|
||||
text-align: center;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #eef2f5;
|
||||
}
|
||||
.brand .logo{
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
object-fit: contain;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.brand h1{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.2px;
|
||||
color: var(--text);
|
||||
}
|
||||
.brand .subtitle{
|
||||
margin: 6px 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Form area */
|
||||
.form{
|
||||
/* keep compact spacing inside the card */
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 6px 0 2px;
|
||||
}
|
||||
|
||||
/* Field label wrapper */
|
||||
.field{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.field .label-text{
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="text"]{
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 12px;
|
||||
font-size: 15px;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
border: 1px solid #d8dee6;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: box-shadow .14s ease, border-color .14s ease, transform .06s ease;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input::placeholder{
|
||||
color: #9aa3ad;
|
||||
}
|
||||
input:focus{
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 6px 20px rgba(0,120,212,0.10);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Checkbox / stay signed */
|
||||
.stay-signed{
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.stay-signed input[type="checkbox"]{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Actions row */
|
||||
.actions{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
button.primary{
|
||||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
||||
color: #000000;
|
||||
border: none;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
||||
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
||||
}
|
||||
button.primary:hover:not(:disabled){
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
||||
}
|
||||
button.primary:active{
|
||||
transform: translateY(0);
|
||||
}
|
||||
button.primary:disabled{
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Alternative options */
|
||||
.alt-options{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.biometric{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(0,120,212,0.14);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.biometric svg{ display: block; opacity: .95; }
|
||||
.biometric:hover{
|
||||
background: rgba(0,120,212,0.04);
|
||||
}
|
||||
|
||||
/* Help link */
|
||||
.help-link{
|
||||
margin-left: auto;
|
||||
font-size: 13px;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.help-link:hover{ text-decoration: underline; }
|
||||
|
||||
/* Footer */
|
||||
.footer{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eef2f5;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.footer a{
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.footer a:hover{ text-decoration: underline; }
|
||||
.divider{ color: #d0d6db; }
|
||||
|
||||
/* Focus styles for keyboard users */
|
||||
:focus{
|
||||
outline: none;
|
||||
}
|
||||
:focus-visible{
|
||||
outline: 3px solid rgba(0,120,212,0.12);
|
||||
outline-offset: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Small screens */
|
||||
@media (max-width:420px){
|
||||
.login-widget .card{
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
.brand h1{ font-size: 18px; }
|
||||
.brand .subtitle{
|
||||
font-family: "Kanit";
|
||||
font-weight: 1000;
|
||||
font-style: normal; }
|
||||
.biometric span, .primary{ font-size: 13px; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Modal-like backdrop that covers the viewport -->
|
||||
<div class="login-backdrop">
|
||||
<div class="login-modal d-flex align-items-center justify-content-center">
|
||||
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||
<div class="brand">
|
||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
||||
<h1 id="signin-title" class='kanit-bold'>เข้าสู่ระบบ</h1>
|
||||
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของคุณ</p>
|
||||
</div>
|
||||
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form px-3 pb-3">
|
||||
<label class="field">
|
||||
<span class="label-text">อีเมล์</span>
|
||||
<input type="email" formControlName="email" autocomplete="username" placeholder="nuttakit@gmail.com" required />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label-text">รหัสผ่าน</span>
|
||||
<input type="password" formControlName="password" autocomplete="current-password" required />
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<label class="stay-signed">
|
||||
<input type="checkbox" formControlName="remember" />
|
||||
<span>จดจำรหัสผ่าน</span>
|
||||
</label>
|
||||
<!-- <fa-icon [icon]="faCoffee" /> -->
|
||||
<button type="submit" class="primary" [disabled]="!(loginForm.get('email')?.valid && loginForm.get('password')?.value)" (click)="OnNavigateDashboard()">
|
||||
เข้าสู่ระบบ
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="alt-options">
|
||||
<button type="button" class="biometric" (click)="useBiometric()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" fill="currentColor" opacity=".9"/>
|
||||
<path d="M6.2 10.9A6 6 0 0 1 12 6a6 6 0 0 1 5.8 4.9M12 22v-2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>เข้าสู่ระบบด้วย Windows Hello / Touch ID</span>
|
||||
</button>
|
||||
|
||||
<a class="help-link" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
|
||||
<span class="divider">•</span>
|
||||
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
113
templete/src/app/component/login-page/login-page.component.ts
Normal file
113
templete/src/app/component/login-page/login-page.component.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { faCoffee } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-page',
|
||||
standalone: false,
|
||||
templateUrl: './login-page.component.html',
|
||||
styleUrls: ['./login-page.component.css'],
|
||||
})
|
||||
export class LoginPageComponent implements OnInit {
|
||||
@Input() brandName = 'Contoso';
|
||||
@Input() subtitle = 'to your account';
|
||||
@Input() mode = '';
|
||||
@Output() signedIn = new EventEmitter<{ email: string; remember: boolean }>();
|
||||
|
||||
faCoffee = faCoffee;
|
||||
loginForm!: FormGroup;
|
||||
busy = false;
|
||||
message = '';
|
||||
|
||||
constructor(
|
||||
// private fb: FormBuilder
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupFormControl();
|
||||
}
|
||||
|
||||
setupFormControl(): void {
|
||||
this.loginForm = new FormGroup({
|
||||
email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
|
||||
password: new FormControl( '', [Validators.required, Validators.maxLength(50)]),
|
||||
remember: new FormControl(false)
|
||||
});
|
||||
}
|
||||
|
||||
async signIn(): Promise<void> {new FormControl
|
||||
if (this.loginForm.invalid) return;
|
||||
this.busy = true;
|
||||
this.message = '';
|
||||
try {
|
||||
// TODO: call your auth API here
|
||||
await fakeNetworkDelay();
|
||||
this.signedIn.emit({
|
||||
email: this.loginForm.value.email,
|
||||
remember: this.loginForm.value.remember,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this.message = err?.message || 'Sign-in failed.';
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async useBiometric(): Promise<void> {
|
||||
this.message = '';
|
||||
if (!('credentials' in navigator) || !('get' in (navigator as any).credentials)) {
|
||||
this.message = 'Biometric authentication is not available on this device/browser.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.busy = true;
|
||||
// Example WebAuthn / PublicKeyCredential call. In a real application,
|
||||
// you must obtain the challenge and allowedCredentials from your server.
|
||||
const publicKeyCredentialRequestOptions = {
|
||||
// challenge must be provided by server as ArrayBuffer
|
||||
challenge: Uint8Array.from('server-provided-challenge', c => c.charCodeAt(0)).buffer,
|
||||
timeout: 60000,
|
||||
userVerification: 'preferred',
|
||||
} as any;
|
||||
|
||||
const credential: any = await (navigator as any).credentials.get({
|
||||
publicKey: publicKeyCredentialRequestOptions,
|
||||
});
|
||||
|
||||
// Send credential to server for verification (not implemented here).
|
||||
// Example: await this.authService.verifyWebAuthn(credential);
|
||||
|
||||
// On success:
|
||||
this.signedIn.emit({ email: '', remember: true });
|
||||
} catch (err: any) {
|
||||
this.message = err?.message || 'Biometric sign-in cancelled or failed.';
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
OnNavigateDashboard(){
|
||||
this.router.navigate(['main/dashboard']);
|
||||
}
|
||||
|
||||
forgotPassword(): void {
|
||||
// emit or navigate
|
||||
this.router.navigate(['/login/forgot-password']);
|
||||
}
|
||||
|
||||
createAccount(): void {
|
||||
this.message = 'Create account flow not implemented.';
|
||||
}
|
||||
|
||||
privacy(): void {
|
||||
this.router.navigate(['/license']);
|
||||
}
|
||||
}
|
||||
|
||||
/* tiny helper */
|
||||
function fakeNetworkDelay() {
|
||||
return new Promise(resolve => setTimeout(resolve, 600));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>works</p>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-dashboard',
|
||||
standalone: false,
|
||||
templateUrl: './main-dashboard.component.html',
|
||||
styleUrl: './main-dashboard.component.css'
|
||||
})
|
||||
export class MainDashboardComponent {
|
||||
|
||||
}
|
||||
39
templete/src/app/component/sidebar/sidebar.component.css
Normal file
39
templete/src/app/component/sidebar/sidebar.component.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/* .sidebar {
|
||||
width: 220px;
|
||||
background: #222;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
} */
|
||||
/* .sidebar ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.sidebar li {
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.sidebar li:hover {
|
||||
color: #00bcd4;
|
||||
} */
|
||||
|
||||
@keyframes spin-slow {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 8s linear infinite;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.expanded {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
62
templete/src/app/component/sidebar/sidebar.component.html
Normal file
62
templete/src/app/component/sidebar/sidebar.component.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<div
|
||||
class="h-screen bg-linear-to-b from-amber-950 to-amber-900 text-gray-100 shadow-2xl flex flex-col relative transition-all duration-300 ease-in-out"
|
||||
[@sidebarState]="isOpen ? 'expanded' : 'collapsed'">
|
||||
|
||||
<button
|
||||
(click)="toggleSidebar()"
|
||||
class="absolute -right-3 top-6 bg-amber-700 hover:bg-amber-600 text-white rounded-full p-2 shadow-md transition-all duration-300">
|
||||
<i class="fas" [ngClass]="isOpen ? 'fa-angle-left' : 'fa-angle-right'"></i>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3 p-5">
|
||||
<h3 *ngIf="isOpen" class="text-2xl font-bold transition-all duration-300">
|
||||
Global Sidebar
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<hr class="border-t border-amber-700 mx-4 my-4 opacity-70">
|
||||
|
||||
<ul class="flex flex-col gap-2 px-2 grow">
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/profile')">
|
||||
<i class="fas fa-user-circle text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium">Profile</span>
|
||||
</li>
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/report')">
|
||||
<i class="fas fa-chart-bar text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium">Report</span>
|
||||
</li>
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/dashboard')">
|
||||
<i class="fas fa-tachometer-alt text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium">Dashboard</span>
|
||||
</li>
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer mt-auto
|
||||
hover:bg-red-700 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/logout')">
|
||||
<i class="fas fa-sign-out-alt text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium text-red-200">Logout</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="isMobile && showOverlay"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300"
|
||||
(click)="toggleSidebar()">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-1 bg-gray-100 text-gray-900 overflow-y-auto">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
66
templete/src/app/component/sidebar/sidebar.component.ts
Normal file
66
templete/src/app/component/sidebar/sidebar.component.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Component, HostListener, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: false,
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrls: ['./sidebar.component.css'],
|
||||
animations: [
|
||||
trigger('sidebarState', [
|
||||
state('expanded', style({
|
||||
width: '220px',
|
||||
opacity: 1
|
||||
})),
|
||||
state('collapsed', style({
|
||||
width: '70px',
|
||||
opacity: 0.95
|
||||
})),
|
||||
transition('expanded <=> collapsed', [
|
||||
animate('300ms ease-in-out')
|
||||
])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class SidebarComponent implements OnInit {
|
||||
isOpen = true; // ขยายไหม
|
||||
isMobile = false; // ตรวจอุปกรณ์
|
||||
showOverlay = false; // สำหรับ mobile overlay
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.checkDevice();
|
||||
window.addEventListener('resize', () => this.checkDevice());
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
checkDevice() {
|
||||
this.isMobile = window.innerWidth <= 768;
|
||||
if (this.isMobile) {
|
||||
this.isOpen = false; // ซ่อน sidebar ตอนเข้า mobile
|
||||
} else {
|
||||
this.isOpen = true; // เปิดไว้ตลอดใน desktop
|
||||
this.showOverlay = false;
|
||||
}
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
if (this.isMobile) {
|
||||
this.showOverlay = !this.showOverlay;
|
||||
this.isOpen = this.showOverlay;
|
||||
} else {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
navigate(path: string) {
|
||||
this.router.navigate([path]);
|
||||
}
|
||||
|
||||
logout() {
|
||||
// TODO: handle logout logic
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100vh; /* ครอบเต็มหน้าจอ */
|
||||
overflow: hidden; /* ปิด scroll bar */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column; /* ✅ แก้ตรงนี้ จาก row → column */
|
||||
align-items: center; /* ✅ จัดให้อยู่กลางแนวนอน */
|
||||
justify-content: center; /* ✅ จัดให้อยู่กลางแนวตั้ง */
|
||||
text-align: center; /* ✅ ให้ข้อความตรงกลาง */
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="justify-content-center flex-column">
|
||||
<!-- <h2>Login | เข้าสู่ระบบ ({{ mode }})</h2> -->
|
||||
@if (mode == "default") {
|
||||
<app-login-page [mode]="mode"></app-login-page>
|
||||
} @else if(mode == "forgot-password"){
|
||||
<app-login-forgot (otpEventSubmit)="onOtpSendSubmit($event)" (otpVerifyEventSubmit)="onVerifySubmit($event)"></app-login-forgot>
|
||||
}
|
||||
<!-- @else {
|
||||
|
||||
} -->
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { GeneralService } from '../../services/generalservice';
|
||||
import { LoginForgotComponent } from '../../../app/component/login-forgot/login-forgot.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-content',
|
||||
standalone: false,
|
||||
templateUrl: './login-content.component.html',
|
||||
styleUrl: './login-content.component.css'
|
||||
})
|
||||
export class LoginContentComponent implements OnInit {
|
||||
@ViewChild(LoginForgotComponent) loginForgotComponent!: LoginForgotComponent;
|
||||
mode: 'forgot-password' | 'default' = 'default';
|
||||
|
||||
constructor(
|
||||
private generalService: GeneralService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
let param = this.route.snapshot.paramMap.get('mode');
|
||||
|
||||
if (param === 'forgot-password') {
|
||||
this.mode = 'forgot-password';
|
||||
} else {
|
||||
this.router.navigate(['/login']);
|
||||
this.mode = 'default';
|
||||
}
|
||||
|
||||
switch (this.mode) {
|
||||
case 'default':
|
||||
break;
|
||||
|
||||
case 'forgot-password':
|
||||
break;
|
||||
|
||||
default:
|
||||
this.mode = 'default'; // กันพลาดไว้เลย
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onOtpSendSubmit(value: any){
|
||||
let uri = 'http://49.231.182.24:1012/api/otp/send';
|
||||
let request = {
|
||||
email: value.email
|
||||
// otp: value.otp
|
||||
}
|
||||
|
||||
this.loginForgotComponent.isLoading = true;
|
||||
this.generalService.postRequest(uri, request).subscribe({
|
||||
next: (result: any) => {
|
||||
if (result.code === '200') {
|
||||
console.log(`✅ OTP ส่งไปที่ ${value.email}`);
|
||||
} else {
|
||||
console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
this.loginForgotComponent.isSendOtp = false;
|
||||
this.loginForgotComponent.isLoading = false;
|
||||
console.error('❌ Error sending OTP:', error);
|
||||
},
|
||||
complete: () => {
|
||||
this.loginForgotComponent.isLoading = false;
|
||||
this.loginForgotComponent.isSendOtp = true;
|
||||
console.log('📨 OTP send request completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onVerifySubmit(value: any){
|
||||
let uri = 'http://49.231.182.24:1012/api/otp/verify';
|
||||
let request = {
|
||||
email: value.email,
|
||||
otp: value.otp
|
||||
}
|
||||
this.generalService.postRequest(uri, request).subscribe({
|
||||
next: (result: any) => {
|
||||
if (result.code === '200') {
|
||||
console.log(`OTP ส่งไปยืนยันสำเร็จ`);
|
||||
} else {
|
||||
console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error('❌ Error sending OTP:', error);
|
||||
},
|
||||
complete: () => {
|
||||
this.router.navigate(['/login']);
|
||||
console.log('📨 OTP send request completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>main-dashboard-content works!</p>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MainDashboardContentComponent } from './main-dashboard-content.component';
|
||||
|
||||
describe('MainDashboardContentComponent', () => {
|
||||
let component: MainDashboardContentComponent;
|
||||
let fixture: ComponentFixture<MainDashboardContentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [MainDashboardContentComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MainDashboardContentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-dashboard-content',
|
||||
standalone: false,
|
||||
templateUrl: './main-dashboard-content.component.html',
|
||||
styleUrl: './main-dashboard-content.component.css'
|
||||
})
|
||||
export class MainDashboardContentComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
.main-container {
|
||||
flex: 1;
|
||||
background: #f5f5f5;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar (เฉพาะ main) -->
|
||||
<app-sidebar></app-sidebar>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50 text-gray-900">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar-content',
|
||||
standalone: false,
|
||||
templateUrl: './sidebar-content.component.html',
|
||||
styleUrl: './sidebar-content.component.css'
|
||||
})
|
||||
export class SidebarContentComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { LoginContentComponent } from '../../content/login-content/login-content.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: LoginContentComponent },
|
||||
{ path: ':mode', component: LoginContentComponent } // ตัวอย่าง param /login/reset
|
||||
// { path: 'forgot-password', component: LoginContentComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class LoginControlRoutingModule { }
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LoginContentComponent } from '../../content/login-content/login-content.component';
|
||||
import { LoginControlRoutingModule } from './login-control-routing.module';
|
||||
import { LoginPageComponent } from '../../component/login-page/login-page.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { LoginForgotComponent } from '../../component/login-forgot/login-forgot.component';
|
||||
// import { AppModule } from '../../app.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
LoginContentComponent,
|
||||
LoginPageComponent,
|
||||
LoginForgotComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
FontAwesomeModule,
|
||||
// AppModule,
|
||||
LoginControlRoutingModule
|
||||
]
|
||||
})
|
||||
export class LoginControlModule { }
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { MainDashboardComponent } from '../../component/main-dashboard/main-dashboard.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'dashboard', component: MainDashboardComponent },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'dashbord',
|
||||
// // loadChildren: () => import('./controls/profile-control/profile-control.module').then(m => m.ProfileControlModule)
|
||||
// },
|
||||
// {
|
||||
// path: 'report',
|
||||
// loadChildren: () => import('./controls/report-control/report-control.module').then(m => m.ReportControlModule)
|
||||
// },
|
||||
// { path: '', redirectTo: 'profile', pathMatch: 'full' }
|
||||
// ]
|
||||
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
{ path: '**', redirectTo: 'dashboard' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class MainControlRoutingModule { }
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { MainControlRoutingModule } from './main-control-routing.module';
|
||||
|
||||
|
||||
import { MainDashboardComponent } from '../../component/main-dashboard/main-dashboard.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
MainDashboardComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MainControlRoutingModule,
|
||||
// BrowserAnimationsModule
|
||||
]
|
||||
})
|
||||
export class MainControlModule { }
|
||||
76
templete/src/app/services/generalservice.ts
Normal file
76
templete/src/app/services/generalservice.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GeneralService {
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
// 🧱 Default header
|
||||
private getHttpOptions() {
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
return { headers };
|
||||
}
|
||||
|
||||
// 🧩 Helper: wrap body ให้มี request ครอบเสมอ
|
||||
private wrapRequestBody(body: any): any {
|
||||
// ถ้ามี request อยู่แล้ว จะไม่ซ้ำ
|
||||
if (body && body.request) {
|
||||
return body;
|
||||
}
|
||||
return { request: body ?? {} };
|
||||
}
|
||||
|
||||
// 📦 POST Request
|
||||
postRequest(uri: string, body: any): Observable<any> {
|
||||
const payload = this.wrapRequestBody(body);
|
||||
return this.http.post(uri, payload, this.getHttpOptions()).pipe(
|
||||
map((res: any) => res),
|
||||
catchError((error: any) => {
|
||||
console.error('❌ [POST Request Error]:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 📦 GET Request
|
||||
getRequest(uri: string): Observable<any> {
|
||||
return this.http.get(uri, this.getHttpOptions()).pipe(
|
||||
map((res: any) => res),
|
||||
catchError((error: any) => {
|
||||
console.error('❌ [GET Request Error]:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 📦 PUT Request
|
||||
putRequest(uri: string, body: any): Observable<any> {
|
||||
const payload = this.wrapRequestBody(body);
|
||||
return this.http.put(uri, payload, this.getHttpOptions()).pipe(
|
||||
map((res: any) => res),
|
||||
catchError((error: any) => {
|
||||
console.error('❌ [PUT Request Error]:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 📦 DELETE Request
|
||||
deleteRequest(uri: string, body?: any): Observable<any> {
|
||||
const payload = this.wrapRequestBody(body);
|
||||
return this.http.delete(uri, { ...this.getHttpOptions(), body: payload }).pipe(
|
||||
map((res: any) => res),
|
||||
catchError((error: any) => {
|
||||
console.error('❌ [DELETE Request Error]:', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
18
templete/src/index.html
Normal file
18
templete/src/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AccountingNgNuttakit</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Kanit:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"> -->
|
||||
<!-- <link rel="stylesheet" href="node_modules/font-awesome/css/all.min.css"> -->
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
7
templete/src/main.ts
Normal file
7
templete/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { platformBrowser } from '@angular/platform-browser';
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
platformBrowser().bootstrapModule(AppModule, {
|
||||
ngZoneEventCoalescing: true,
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
179
templete/src/styles.css
Normal file
179
templete/src/styles.css
Normal file
@@ -0,0 +1,179 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Global base styles for the app. Keep lightweight and self-contained so
|
||||
the login component can reliably fill the viewport without producing
|
||||
an outer page scrollbar. */
|
||||
|
||||
/* Make sure the page and app root occupy full height so 100vh aligns */
|
||||
html, body, app-root {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
/* เริ่มต้น: สำหรับ Desktop */
|
||||
.login-mobile {
|
||||
width: 415px;
|
||||
}
|
||||
|
||||
/* ถ้าเป็น Mobile (<=768px) ให้ลบ width ออก */
|
||||
@media (max-width: 768px) {
|
||||
.login-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* sensible default box model */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Kanit", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Prevent the browser from showing a scroll bar for the page itself;
|
||||
the login card will scroll internally if needed. */
|
||||
}
|
||||
|
||||
/* Simple utilities used by nested components in this workspace */
|
||||
.content-box {
|
||||
border: 2px solid black;
|
||||
padding: 10px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.comp-box {
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
/* Use flex centering so nested components (like the login widget)
|
||||
are centered without forcing the document to scroll. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* If the project uses Bootstrap, the Bootstrap utilities will still apply.
|
||||
These local utility rules only ensure a consistent appearance if Bootstrap
|
||||
isn't available. */
|
||||
|
||||
|
||||
.kanit-thin {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-extralight {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-light {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-regular {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-medium {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-semibold {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-bold {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-extrabold {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-black {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kanit-thin-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-extralight-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-light-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-regular-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-medium-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-semibold-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-bold-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-extrabold-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kanit-black-italic {
|
||||
font-family: "Kanit", sans-serif;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
15
templete/tsconfig.app.json
Normal file
15
templete/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
27
templete/tsconfig.json
Normal file
27
templete/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
15
templete/tsconfig.spec.json
Normal file
15
templete/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
10
templete/vite.config.ts
Normal file
10
templete/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { angular } from '@analogjs/vite-plugin-angular';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [angular()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['accounting.nuttakit.work'], // 👈 เพิ่มโดเมนของคุณ
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user