Compare commits

...

64 Commits

Author SHA1 Message Date
x2Skyz
43ed6c0c55 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m8s
2025-11-23 18:16:02 +07:00
x2Skyz
5f68d3fa51 debug 2025-11-23 18:15:54 +07:00
f68f76373c workflow: fix typo on docker rm
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 5m46s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 13:22:04 +07:00
19a5245536 Workflow: set +e-e to docker rm container
Some checks failed
Build Docker Image / Build Docker Image (push) Has been cancelled
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 13:19:53 +07:00
811d7b14b8 Dockerfile: fix path for nginx.conf
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 5m53s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-23 12:53:38 +07:00
b1f9c48bd8 Dockerfile: added nginx.conf
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 5m54s
2025-11-23 12:39:57 +07:00
a34714388f nginx.conf: added
Some checks failed
Build Docker Image / Build Docker Image (push) Has been cancelled
2025-11-23 12:37:49 +07:00
x2Skyz
809e2e16bb -commit
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m18s
2025-11-23 12:23:28 +07:00
x2Skyz
3ebfd37400 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m24s
2025-11-21 19:35:53 +07:00
x2Skyz
f0336a5ead . 2025-11-21 19:35:46 +07:00
HAPFEX
8cedef205a Workflow: disable api url patching
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m21s
2025-11-21 18:49:30 +07:00
x2Skyz
45259f7b8d -accouting Wep
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 8m54s
2025-11-21 15:01:21 +07:00
1c7729ab99 workflow: change temporary api port to 8080
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m50s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 11:50:14 +07:00
x2Skyz
10aac6060b - add project
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m12s
2025-11-21 10:24:49 +07:00
651a120e2b workflow: fix path and typo once again
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m24s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 09:58:36 +07:00
14e7223974 workflow: fix path for sed
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m43s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 09:21:01 +07:00
ff95446e07 workflow: temporary change api url when build image
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m30s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 08:14:08 +07:00
3bb798c39e workflow: temporary change api url to wg
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-21 08:11:35 +07:00
x2Skyz
9db43971ce -
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m3s
2025-11-20 13:31:19 +07:00
1d2d593eee -upadte รอ ผูก
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 5m53s
2025-11-20 12:29:49 +07:00
5a16152a46 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend
Some checks failed
Build Docker Image / Build Docker Image (push) Has been cancelled
2025-11-20 12:25:57 +07:00
3e0dae41f0 -number 2025-11-20 12:25:07 +07:00
ebb1f2e0d6 workflow: testing build docker image
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 5m53s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 10:38:00 +07:00
a7906c6a34 workflow: locate dist folder
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 5m52s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 10:01:53 +07:00
1406d36d72 workflow: install @angular/cli as global
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m49s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 09:39:07 +07:00
87571f8332 workflow: --include=dev to npm install due to --dev is deprecated
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 4m51s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 09:23:03 +07:00
856b6b41a8 workflow: added --force --legacy-peer-deps --dev to npm install
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 4m59s
the issue was dev tools wasn't installed if --dev is not passed.

Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 09:16:53 +07:00
9a05e78fc5 workflow: npm install use --legacy-peer-deps instead of --force
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m13s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 09:04:29 +07:00
de73c8d68f workflow: fix path for apt sed'in, verify why angular is not installed
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 6m46s
angular issue is about the version itself, but verification is needed by front-end dev too. cannot figure it out yet why isn't installing on ubuntu noble container.

ubuntu noble is using /etc/apt/source.list.d, unlike debian.

Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 07:07:57 +07:00
cd6e0595a9 workflow: remove install docker, sudo sed and test ng build release
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 5m58s
since docker is already exist inside the container, no need to reinstall.

Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 06:36:59 +07:00
39457af479 workflow: typo, try again
Some checks failed
Build Docker Image / Build Docker Image (push) Has been cancelled
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 06:31:17 +07:00
0c05d9eadf workflow: change package mirror to kku, install and test docker-in-docker
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 24s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 06:29:35 +07:00
1847bf5d65 workflow: npm install --force
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m14s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 06:06:31 +07:00
290f5cae6a workflows: testing npm install
Some checks failed
Build Docker Image / Build Docker Image (push) Failing after 2m56s
Signed-off-by: supphakitd <67319010028@technictrang.ac.th>
2025-11-20 05:59:14 +07:00
6ef1afb7f3 Dockerfile: added nginx based
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 11s
2025-11-20 05:49:29 +07:00
538ee11053 workflow: fix yaml format
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 26s
2025-11-20 05:46:37 +07:00
5b3bc79199 workflow: added verify project tree
Some checks failed
Build Docker Image / Preparing Dependecies (push) Failing after 1s
2025-11-20 05:42:43 +07:00
15308ababa -login guard
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 4s
-caching
-budget
2025-11-19 18:30:35 +07:00
213fd197ef -จัดการ flow ของ program ใหม่
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s
2025-11-19 11:08:30 +07:00
6da86b74a9 แก้ไข
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s
2025-11-17 18:24:42 +07:00
21007f7bbc latest merge
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s
2025-11-17 17:45:33 +07:00
a475265764 - catching
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s
2025-11-17 17:25:51 +07:00
2e07e728dd ....
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 4s
2025-11-17 17:19:18 +07:00
304ed9294c -สร้างโครงสร้างไฟล์ ใหม่ ใน ttc base project
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s
2025-11-17 17:14:08 +07:00
9b0edccde5 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend 2025-11-17 08:43:39 +07:00
x2Skyz
6e96c686a2 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s
2025-11-16 22:51:11 +07:00
x2Skyz
c4c19e96b6 -แก้ไขโครงสร้าง dashboard 2025-11-16 22:51:03 +07:00
1f3c33b78c -env 2025-11-16 22:06:23 +07:00
5ce012b558 workflow: test p.1
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 1m25s
2025-11-16 21:51:06 +07:00
4b3e52ff43 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend 2025-11-16 21:50:13 +07:00
8f9159a330 workflow: test p.1 2025-11-16 21:48:42 +07:00
x2Skyz
ccab40852c -แก้ไขระบบ trow api และ ยิง 2025-11-16 21:45:57 +07:00
60662d88d4 workflow: initial workflow placeholder file 2025-11-16 21:33:05 +07:00
ee72ef6676 -scroll เมื่อ ชุดข้อมูลมากกว่า 5 2025-11-14 12:29:59 +07:00
7b441c3600 -ระ บบ pie chart และคำนวณ สี 2025-11-14 10:10:55 +07:00
139167be8a - 2025-11-13 20:23:55 +07:00
80edb10361 - 2025-11-13 19:00:31 +07:00
3cc4a4a632 -เชื่อมโยง api search กับ frontend
-ปรับปรุงระบบ state
-เพิ่ม ระบบ pipe dtmtodatetime
2025-11-13 18:00:51 +07:00
f27389da29 -ตั้งชื่อ caching ผิด 2025-11-13 16:25:13 +07:00
1664be0c8b dropdown 2025-11-13 15:37:50 +07:00
b3fa94f904 -interface
-service

-เพิ่มเทคนิค การส่ง ผ่านข้อมูล
2025-11-13 14:53:37 +07:00
37ca45701b -เพิ่มการรองรับ interfaces/dashboard.interface.ts 2025-11-13 14:20:15 +07:00
78ce686f97 feat: เพิ่มระบบ caching
- เพิ่ม caching interceptor และ service

- เพิ่ม configสำหรับ caching
2025-11-13 13:30:02 +07:00
f25488370a -เพิ่ม guard jwt token
-เพิ่ม  jwt-decoded
-เพิ่ม  ระบบ first setup ดึงข้อมูล  accounting
2025-11-13 11:45:03 +07:00
151 changed files with 31117 additions and 486 deletions

View File

@@ -0,0 +1,35 @@
name: Build Docker Image
run-name: Build Docker Image
on: [push]
jobs:
Build Docker Image:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Install system dependencies
run: |
# change mirror to kku.ac.th for faster package downloading
sudo sed -i 's@http://archive.ubuntu.com@http://mirror.kku.ac.th@g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt update && sudo apt install nodejs npm -y
- name: Install project dependencies
run: |
cd accounting-ng-nuttakit
npm install --force --legacy-peer-deps --include=dev
npm install -g @angular/cli
- name: Build webapp
run: |
cd accounting-ng-nuttakit
# temporary change api url for development
#sed -i 's@https://api.nuttakit.work@http://10.9.0.0:8080@g' src/environments/environment.ts
ng build
- name: Build docker image
run: |
set +e
docker rm $(docker stop $(docker ps -a -q --filter ancestor=accounting-frontend:latest --format="{{.ID}}"))
set -e
docker image rm -f accounting-frontend:latest
docker build . -t accounting-frontend:latest

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
ADD accounting-ng-nuttakit/dist/accounting-ng-nuttakit/browser /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -37,12 +37,10 @@
}
],
"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": {
@@ -86,7 +84,7 @@
"builder": "@angular/build:dev-server",
"options": {
"host": "0.0.0.0",
"allowedHosts": ["accounting.nuttakit.work", "localhost"]
"allowedHosts": ["accounting.nuttakit.work", "localhost", "meal-demand-virtual-referrals.trycloudflare.com"]
},
"configurations": {
"production": {
@@ -116,12 +114,10 @@
}
],
"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"
]
}
}

View File

@@ -58,9 +58,9 @@
"@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",
"chart.js": "^4.5.1",
"dotenv": "^17.2.3",
"jwt-decode": "^4.0.0",
"ng2-charts": "^6.0.1",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
@@ -73,6 +73,7 @@
"@angular/cli": "^20.3.9",
"@angular/compiler-cli": "^20.3.10",
"@capacitor/cli": "latest",
"@types/jasmine": "~5.1.0",
"cross-env": "^10.1.0",
"electron": "^39.0.0",
"electron-builder": "^26.0.12",
@@ -84,7 +85,6 @@
"karma-jasmine-html-reporter": "~2.1.0",
"ngx-toastr": "^19.1.0",
"postcss": "^8.5.3",
"typescript": "~5.9.3",
"@types/jasmine": "~5.1.0"
"typescript": "~5.9.3"
}
}

View File

@@ -8,7 +8,7 @@ const routes: Routes = [
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) },
{ path: 'c', component: LicensePrivacyTermsComponent},
{ path: 'license', component: LicensePrivacyTermsComponent},
{
path: 'main',
@@ -22,9 +22,18 @@ const routes: Routes = [
(m) => m.MainControlModule
),
},
// {
// path: 'report',
// loadChildren: () =>
// import('./controls/report-control/report-control.module').then(
// (m) => m.ReportControlModule
// ),
// },
],
},
// {path: 'license' , component: LicensePrivacyTermsComponent}
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: '**', redirectTo: 'login' }

View File

@@ -1 +1,2 @@
<router-outlet></router-outlet>

View File

@@ -21,7 +21,11 @@ import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/
// import { LoginPageComponent } from './component/login-page/login-page.component';
// import { LoginContentComponent } from './content/login-content/login-content.component';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { LoginRegisterComponent } from './component/login-register/login-register.component';
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
@NgModule({
declarations: [
@@ -30,6 +34,9 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
SidebarContentComponent,
SidebarComponent,
LicensePrivacyTermsComponent,
// LoginRegisterComponent,
// AccDateFormatPipe
// DtmtodatetimePipe,
// MainDashboardContentComponent,
// MainDashboardComponent,
// LoginForgotComponent,
@@ -51,6 +58,9 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
HttpClientModule,
FontAwesomeModule
],
exports: [
// AccDateFormatPipe
],
providers: [provideCharts(withDefaultRegisterables())],
bootstrap: [AppComponent]
})

View File

@@ -8,12 +8,11 @@
--radius: 8px;
--shadow: 0 10px 30px rgba(11,26,43,0.08);
--glass: rgba(255,255,255,0.6);
--success-color: #10b981; /* Green for success/confirm */
}
/* Page layout */
/* Page layout (unchanged) */
.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;
@@ -24,8 +23,7 @@
color: var(--text);
}
/* Card */
/* Card (unchanged) */
.login-widget .card{
width: 380px;
max-width: calc(100% - 40px);
@@ -37,16 +35,14 @@
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 */
/* Modal/backdrop styles (unchanged) */
.login-backdrop{
position: fixed;
inset: 0; /* top:0; right:0; bottom:0; left:0; */
inset: 0;
background: rgba(0,0,0,0.38);
display: flex;
align-items: center;
@@ -54,43 +50,19 @@
z-index: 1040;
padding: 24px;
}
.login-modal{ width: 480px; max-width: 480px; }
.modal-card{
border-radius: 12px;
padding: 0; /* card children control internal padding */
padding: 0;
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 area (unchanged) */
.brand{
text-align: center;
padding: 18px;
padding-bottom: 4px;
border-bottom: 1px solid #eef2f5;
}
@@ -116,16 +88,13 @@ button.primary{
/* 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;
padding: 6px 22px 22px 22px;
}
/* Field label wrapper */
/* Field label wrapper (unchanged) */
.field{
display: flex;
flex-direction: column;
@@ -135,11 +104,11 @@ button.primary{
font-size: 13px;
color: var(--muted);
}
/* Inputs */
/* Inputs (class 'input-field' added to HTML) */
input[type="email"],
input[type="password"],
input[type="text"]{
input[type="text"],
.input-field { /* เพิ่ม class input-field เพื่อให้สไตล์ถูกใช้กับ input ที่กำหนด */
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
@@ -163,31 +132,30 @@ input:focus{
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;
flex-direction: row-reverse;
}
/* Custom class for justify-end when using flex-row-reverse */
.actions.justify-end-custom {
justify-content: flex-start;
}
/* Custom class for lift hover effect (used for 'เปิด Modal' button) */
.hover-lift:hover {
transform: translateY(-2.5px);
transition: transform .2s ease;
}
/* PRIMARY BUTTON - แก้ไขสีข้อความให้เป็นสีดำ */
button.primary{
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
color: #000000;
color: #000000; /* ⬅️ แก้ไขเป็นสีดำตามคำขอ */
border: none;
padding: 10px 14px;
border-radius: 6px;
@@ -197,6 +165,7 @@ button.primary{
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);
@@ -207,45 +176,26 @@ button.primary:active{
button.primary:disabled{
opacity: 0.55;
cursor: not-allowed;
color: #000000; /* ข้อความ disabled ก็เป็นสีดำ */
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;
/* Secondary Button Style (สำหรับปุ่ม 'เปิด Modal', 'ส่งอีกครั้ง') */
.primary.secondary-button {
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;
border: 1px solid var(--primary);
box-shadow: none;
transition: background-color .14s ease;
}
.biometric svg{ display: block; opacity: .95; }
.biometric:hover{
background: rgba(0,120,212,0.04);
.primary.secondary-button:hover {
background: rgba(0, 120, 212, 0.05);
transform: none;
box-shadow: none;
}
/* Help link */
.help-link{
margin-left: auto;
font-size: 13px;
color: var(--primary);
text-decoration: none;
}
.help-link:hover{ text-decoration: underline; }
/* Footer */
/* Footer (unchanged) */
.footer{
display: flex;
justify-content: center;
@@ -262,13 +212,9 @@ button.primary:disabled{
text-decoration: none;
font-weight: 600;
}
.footer a:hover{ text-decoration: underline; }
.divider{ color: #d0d6db; }
/* Focus styles for keyboard users */
:focus{
outline: none;
}
/* Focus styles (unchanged) */
:focus{ outline: none; }
:focus-visible{
outline: 3px solid rgba(0,120,212,0.12);
outline-offset: 2px;
@@ -277,14 +223,13 @@ button.primary:disabled{
/* Small screens */
@media (max-width:420px){
.login-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
.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; }
}

View File

@@ -1,52 +1,38 @@
<div class="login-backdrop">
<div class="login-modal d-flex align-items-center justify-content-center ">
<div class="min-h-screen flex items-center justify-center p-4 sm:p-6 bg-linear-to-br from-red-50 via-white to-red-100 relative overflow-hidden">
<div class="login-modal d-flex align-items-center justify-content-center bg-white rounded-2xl">
<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>
<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">
<form [formGroup]="forgotFrm" class="form">
<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 />
<span class="label-text">อีเมล์</span>
<input type="email" formControlName="email" class="input-field" 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/>
<input type="text" formControlName="otp" class="input-field" autocomplete="one-time-code" 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">
<div class="actions justify-end-custom mt-4">
<div class="flex-row hover-lift">
<button class="primary secondary-button" type="button" (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 type="submit" class="primary" disabled>
กำลังส่ง...
</button>
} @else {
@@ -57,7 +43,7 @@
</div>
} @else if(isSendOtp === true) {
<div class="flex justify-end gap-2">
<button type="button" class="primary" (click)="onSubmin()">
<button type="button" class="primary secondary-button" (click)="onSubmin()">
{{ 'ส่งอีกครั้ง' }}
</button>
<button type="submit" class="primary" (click)="onVerifySubmit()">
@@ -66,37 +52,66 @@
</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 class="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true" [formGroup]="forgotFrm">
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity" aria-hidden="true"></div>
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div class="relative transform overflow-hidden rounded-2xl bg-white text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-md md:max-w-lg">
<div class="bg-red-900 h-2 w-full"></div>
<div class="px-8 pt-6 pb-8">
<div class="sm:flex sm:items-start mb-6">
<div class="mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-red-900">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
</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="กรอกยืนยันรหัสผ่านใหม่">
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 class="text-xl font-bold leading-6 text-gray-900" id="modal-title">เปลี่ยนรหัสผ่าน</h3>
<p class="text-sm text-gray-500 mt-1">กรุณากรอกรหัสผ่านใหม่ของคุณด้านล่าง</p>
</div>
</div>
<div class="space-y-5">
<div>
<label for="newPassword" class="block text-sm font-semibold text-gray-700 mb-2">รหัสผ่านใหม่</label>
<input type="password" id="newPassword" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all shadow-sm" formControlName="newPassword" placeholder="········">
</div>
<div>
<label for="confirmPassword" class="block text-sm font-semibold text-gray-700 mb-2">ยืนยันรหัสผ่านใหม่</label>
<input type="password" id="confirmPassword" class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all shadow-sm" 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 class="flex items-center text-red-600 text-sm mt-2 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>รหัสผ่านไม่ตรงกัน</span>
</div>
}
</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">
ปิด
</div>
</div>
<div class="bg-gray-50 px-6 py-4 sm:flex sm:flex-row-reverse sm:px-8 rounded-b-2xl">
<button type="button" class="inline-flex w-full justify-center rounded-xl bg-red-900 px-5 py-3 text-sm font-semibold text-white shadow-sm hover:bg-red-950 sm:ml-3 sm:w-auto transition-colors" (click)="onSetNewPassword()">
ยืนยันการเปลี่ยนแปลง
</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 type="button" class="mt-3 inline-flex w-full justify-center rounded-xl bg-white px-5 py-3 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors" (click)="isModalOpen = false">
ยกเลิก
</button>
</div>
</div>
</div>
</div>
}

View File

@@ -12,8 +12,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;
@@ -37,8 +35,6 @@
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;
}
@@ -46,7 +42,7 @@
/* Modal/backdrop styles */
.login-backdrop{
position: fixed;
inset: 0; /* top:0; right:0; bottom:0; left:0; */
inset: 0;
background: rgba(0,0,0,0.38);
display: flex;
align-items: center;
@@ -64,33 +60,10 @@
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: 18px; /* Use padding from modal-card .brand */
padding-bottom: 4px;
border-bottom: 1px solid #eef2f5;
}
@@ -116,12 +89,11 @@ button.primary{
/* Form area */
.form{
/* keep compact spacing inside the card */
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 6px 0 2px;
padding: 6px 22px 2px 22px; /* Adjusted padding to match card padding */
}
/* Field label wrapper */
@@ -185,8 +157,9 @@ input:focus{
margin-top: 4px;
}
button.primary{
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
/* ⭐️ แก้ไขตรงนี้: เปลี่ยนสีข้อความเป็นสีดำตามคำขอ */
color: #000000;
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
border: none;
padding: 10px 14px;
border-radius: 6px;
@@ -196,6 +169,7 @@ button.primary{
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);
@@ -206,6 +180,7 @@ button.primary:active{
button.primary:disabled{
opacity: 0.55;
cursor: not-allowed;
color: #000000; /* ข้อความ Disabled ก็ยังเป็นสีดำ */
box-shadow: none;
}
@@ -230,6 +205,7 @@ button.primary:disabled{
font-weight: 600;
font-size: 13px;
}
.biometric svg{ display: block; opacity: .95; }
.biometric:hover{
background: rgba(0,120,212,0.04);
@@ -276,14 +252,14 @@ button.primary:disabled{
/* Small screens */
@media (max-width:420px){
.login-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
.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; }
}

View File

@@ -1,55 +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="min-h-screen flex items-center justify-center p-4 sm:p-6 bg-linear-to-br from-red-50 via-white to-red-100 relative overflow-hidden">
<div class="login-modal d-flex align-items-center justify-content-center bg-white rounded-2xl">
<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>
<img src="/logo.png" alt="Company logo" class="logo"/>
<h1 id="signin-title" class="kanit-bold">เข้าสู่ระบบ</h1>
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของท่าน</p>
</div>
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form px-3 pb-3">
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form">
<label class="field">
<span class="label-text">อีเมล์</span>
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required />
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required class="input-field" />
</label>
<label class="field">
<label class="field mt-3">
<span class="label-text">รหัสผ่าน</span>
<input type="password" formControlName="password" autocomplete="current-password" required />
<input type="password" formControlName="password" autocomplete="current-password" required class="input-field" />
</label>
<div class="actions">
<div class="actions d-flex justify-content-between align-items-center mt-4">
<label class="stay-signed">
<input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span>
</label>
<!-- <fa-icon [icon]="faCoffee" /> -->
<button type="submit" class="primary" [disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
<button type="submit" class="primary login-button"
[disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
เข้าสู่ระบบ
</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">
<div class="alt-options mt-4 text-center">
<button type="button" class="biometric btn-icon" (click)="useBiometric()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" class="icon-svg">
<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>
<a class="help-link mt-2" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
</div>
<div class="footer">
<div class="footer mt-5 text-center">
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
<span class="divider"></span>
<span class="divider mx-2"></span>
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
</div>
</form>
</div>
</div>
</div>

View File

@@ -81,6 +81,7 @@ export class LoginPageComponent implements OnInit {
}
createAccount(): void {
this.router.navigate(['/login/register']);
this.message = 'Create account flow not implemented.';
}

View File

@@ -0,0 +1,17 @@
/* Custom animation for blobs */
@keyframes blob {
0% { transform: translate(0px, 0px) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
100% { transform: translate(0px, 0px) scale(1); }
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}

View File

@@ -0,0 +1,214 @@
<body class="bg-gray-50">
<div class="min-h-screen w-full flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-8 bg-linear-to-br from-red-50 via-white to-red-100 relative overflow-hidden">
<!-- Decorative Blobs -->
<div class="absolute top-0 left-0 w-72 h-72 bg-red-300 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob"></div>
<div class="absolute top-0 right-0 w-72 h-72 bg-orange-300 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-rose-300 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-4000"></div>
<!-- Card Container -->
<div class="bg-white/80 backdrop-blur-lg px-6 py-8 sm:p-10 rounded-3xl shadow-2xl w-full max-w-lg border border-white/50 relative z-10 mx-auto flex flex-col min-h-[500px]">
<!-- Header & Stepper Progress -->
<div class="mb-6">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold text-red-950 tracking-tight">สร้างบัญชีใหม่</h1>
<p class="text-gray-500 mt-1 text-sm">ขั้นตอนที่ {{ currentStep }} จาก 3</p>
</div>
<!-- Progress Bar -->
<div class="relative h-2 bg-gray-200 rounded-full overflow-hidden">
<div class="absolute top-0 left-0 h-full bg-red-900 transition-all duration-500 ease-out rounded-full"
[style.width.%]="(currentStep / 3) * 100"></div>
</div>
<!-- Step Labels (Optional visual aid) -->
<div class="flex justify-between text-xs text-gray-400 mt-2 font-medium px-1">
<span [class.text-red-900]="currentStep >= 1">ข้อมูลส่วนตัว</span>
<span [class.text-red-900]="currentStep >= 2">ตั้งรหัสผ่าน</span>
<span [class.text-red-900]="currentStep >= 3">ยืนยัน</span>
</div>
</div>
<!-- Form Bindings -->
<form [formGroup]="registerFrm" (ngSubmit)="onSubmit()" class="flex-1 flex flex-col justify-between">
<!-- STEP 1: Personal Info -->
@if (currentStep === 1) {
<div class="space-y-5 step-content">
<div>
<label for="username" class="block text-sm font-semibold text-gray-700 mb-1.5">ชื่อจริง</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" /></svg>
</div>
<input type="text" id="username" formControlName="username"
class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-red-900 focus:border-transparent outline-none transition-all shadow-sm placeholder-gray-400"
[class.border-red-500]="registerFrm.get('username')?.invalid && (registerFrm.get('username')?.dirty || registerFrm.get('username')?.touched)"
placeholder="สมชาย">
</div>
@if (registerFrm.get('username')?.invalid && (registerFrm.get('username')?.dirty || registerFrm.get('username')?.touched)) {
<div class="text-red-600 text-xs mt-1.5 ml-1">กรุณาระบุชื่อจริง</div>
}
</div>
<div>
<label for="userlastname" class="block text-sm font-semibold text-gray-700 mb-1.5">นามสกุล</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" /></svg>
</div>
<input type="text" id="userlastname" formControlName="userlastname"
class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-red-900 focus:border-transparent outline-none transition-all shadow-sm placeholder-gray-400"
[class.border-red-500]="registerFrm.get('userlastname')?.invalid && (registerFrm.get('userlastname')?.dirty || registerFrm.get('userlastname')?.touched)"
placeholder="ใจดี">
</div>
@if (registerFrm.get('userlastname')?.invalid && (registerFrm.get('userlastname')?.dirty || registerFrm.get('userlastname')?.touched)) {
<div class="text-red-600 text-xs mt-1.5 ml-1">กรุณาระบุนามสกุล</div>
}
</div>
</div>
}
<!-- STEP 2: Account Info -->
@if (currentStep === 2) {
<div class="space-y-5 step-content">
<div>
<label for="email" class="block text-sm font-semibold text-gray-700 mb-1.5">อีเมล</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" /></svg>
</div>
<input type="email" id="email" formControlName="email"
class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-red-900 focus:border-transparent outline-none transition-all shadow-sm placeholder-gray-400"
[class.border-red-500]="registerFrm.get('email')?.invalid && (registerFrm.get('email')?.dirty || registerFrm.get('email')?.touched)"
placeholder="you@example.com">
</div>
@if (registerFrm.get('email')?.invalid && (registerFrm.get('email')?.dirty || registerFrm.get('email')?.touched)) {
<div class="text-red-600 text-xs mt-1.5 ml-1">กรุณาระบุอีเมลที่ถูกต้อง</div>
}
</div>
<div>
<label for="password" class="block text-sm font-semibold text-gray-700 mb-1.5">รหัสผ่าน</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" /></svg>
</div>
<input type="password" id="password" formControlName="password"
class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-red-900 focus:border-transparent outline-none transition-all shadow-sm placeholder-gray-400"
[class.border-red-500]="registerFrm.get('password')?.invalid && (registerFrm.get('password')?.dirty || registerFrm.get('password')?.touched)"
placeholder="••••••••">
</div>
@if (registerFrm.get('password')?.invalid && (registerFrm.get('password')?.dirty || registerFrm.get('password')?.touched)) {
<div class="text-red-600 text-xs mt-1.5 ml-1">กรุณาระบุรหัสผ่าน</div>
}
</div>
<div>
<label for="confirmPassword" class="block text-sm font-semibold text-gray-700 mb-1.5">ยืนยันรหัสผ่าน</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" /></svg>
</div>
<input type="password" id="confirmPassword" formControlName="confirmPassword"
class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:bg-white focus:ring-2 focus:ring-red-900 focus:border-transparent outline-none transition-all shadow-sm placeholder-gray-400"
[class.border-red-500]="(registerFrm.get('confirmPassword')?.invalid || registerFrm.get('password')?.value !== registerFrm.get('confirmPassword')?.value) && (registerFrm.get('confirmPassword')?.dirty || registerFrm.get('confirmPassword')?.touched)"
placeholder="••••••••">
</div>
@if (registerFrm.get('confirmPassword')?.dirty && registerFrm.get('password')?.value !== registerFrm.get('confirmPassword')?.value) {
<div class="text-red-600 text-xs mt-1.5 ml-1">รหัสผ่านไม่ตรงกัน</div>
}
</div>
</div>
}
<!-- STEP 3: Confirmation -->
@if (currentStep === 3) {
<div class="step-content flex flex-col justify-center items-center h-full space-y-6 text-center">
<div class="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10 text-red-900">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">เกือบเสร็จแล้ว!</h3>
<p class="text-gray-500 text-sm mt-1">กรุณายอมรับเงื่อนไขเพื่อสร้างบัญชีของคุณ</p>
</div>
<div class="bg-gray-50 p-4 rounded-xl w-full text-left border border-gray-100">
<p class="text-sm text-gray-600 font-medium mb-2">สรุปข้อมูล:</p>
<div class="text-sm text-gray-800 flex items-center gap-2">
<svg class="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
{{ registerFrm.get('username')?.value }} {{ registerFrm.get('userlastname')?.value }}
</div>
<div class="text-sm text-gray-800 flex items-center gap-2 mt-1">
<svg class="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
{{ registerFrm.get('email')?.value }}
</div>
</div>
<div class="w-full">
<div class="flex items-center justify-center">
<input type="checkbox" id="terms" formControlName="terms"
class="h-5 w-5 text-red-900 border-gray-300 rounded focus:ring-red-900 cursor-pointer transition-all">
<label for="terms" class="ml-2 block text-sm text-gray-600 cursor-pointer select-none">
ฉันยอมรับ <a href="/license" class="font-semibold text-red-900 hover:text-red-700 underline decoration-2 decoration-transparent hover:decoration-red-700 transition-all">ข้อตกลงและเงื่อนไข</a>
</label>
</div>
@if (registerFrm.get('terms')?.invalid && (registerFrm.get('terms')?.dirty || registerFrm.get('terms')?.touched)) {
<div class="text-red-600 text-xs mt-2">กรุณายอมรับเงื่อนไข</div>
}
</div>
</div>
}
<!-- Navigation Buttons -->
<div class="pt-6 mt-auto flex gap-3">
<!-- Back Button (Hide on step 1) -->
@if (currentStep > 1) {
<button type="button" (click)="currentStep = currentStep - 1"
class="w-1/3 py-3.5 px-4 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold rounded-xl transition-colors">
ย้อนกลับ
</button>
}
<!-- Next Button (Step 1 & 2) -->
@if (currentStep < 3) {
<button type="button" (click)="currentStep = currentStep + 1"
[disabled]="(currentStep === 1 && (registerFrm.get('username')?.invalid || registerFrm.get('userlastname')?.invalid)) ||
(currentStep === 2 && (registerFrm.get('email')?.invalid || registerFrm.get('password')?.invalid || registerFrm.get('confirmPassword')?.invalid || registerFrm.get('password')?.value !== registerFrm.get('confirmPassword')?.value))"
class="flex-1 py-3.5 px-4 bg-red-900 hover:bg-red-950 text-white font-bold rounded-xl shadow-lg shadow-red-900/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
ถัดไป
</button>
}
<!-- Submit Button (Step 3 Only) -->
@if (currentStep === 3) {
<button type="submit"
[disabled]="registerFrm.invalid"
class="flex-1 py-3.5 px-4 bg-red-900 hover:bg-red-950 text-white font-bold rounded-xl shadow-lg shadow-red-900/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
สมัครสมาชิก
</button>
}
</div>
</form>
<!-- Footer Link -->
@if (currentStep === 1) {
<p class="mt-6 text-center text-sm text-gray-500">
เป็นสมาชิกอยู่แล้วใช่ไหม?
<a href="#" class="font-bold text-red-900 hover:text-red-950 hover:underline transition-all">
เข้าสู่ระบบ
</a>
</p>
}
</div>
</div>
</body>

View File

@@ -0,0 +1,46 @@
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-register',
standalone: false,
templateUrl: './login-register.component.html',
styleUrl: './login-register.component.css',
})
export class LoginRegisterComponent implements OnInit {
@Output() registeredEventSubmit = new EventEmitter<any>();
registerFrm!: FormGroup;
currentStep = 1;
constructor(){}
ngOnInit(): void {
this.setupFormControl();
}
setupFormControl(): void {
this.registerFrm = new FormGroup({
username: new FormControl('',[Validators.required, Validators.maxLength(100)]),
userlastname: new FormControl('',[Validators.required, Validators.maxLength(100)]),
email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
password: new FormControl('',[Validators.required, Validators.maxLength(50)]),
confirmPassword: new FormControl('',[Validators.required, Validators.maxLength(50)]),
terms: new FormControl(false, [Validators.requiredTrue])
});
}
onSubmit(){
let data = {
username: this.registerFrm.get('username')?.value,
userlastname: this.registerFrm.get('userlastname')?.value,
email: this.registerFrm.get('email')?.value,
password: this.registerFrm.get('password')?.value
}
this.RegisteredEventSubmit(data);
}
RegisteredEventSubmit(event: any){
this.registeredEventSubmit.emit(event);
}
}

View File

@@ -281,6 +281,23 @@
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.quick-log__form select,
.quick-log__form textarea {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 0.75rem 1rem;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.quick-log__form select:focus,
.quick-log__form textarea:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
outline: none;
}
.quick-log__form input:focus,
.quick-log__form textarea:focus {
border-color: #0ea5e9;
@@ -627,3 +644,20 @@
text-align: left;
}
}
.quick-log__form select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.ledger-table.is-scrollable {
max-height: 25rem;
overflow-y: auto;
padding-right: 0.5rem;
}

View File

@@ -13,7 +13,36 @@
</div>
</header>
<section class="dashboard__periods">
<!-- Summary Cards -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-8">
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition">
<div class="text-gray-500 text-sm">รายรับทั้งหมด</div>
<div class="text-3xl font-bold text-green-600 mt-1">
{{ myActSumData.summary.totalIncome || 0 | number:'1.0-2' }} บาท
</div>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition">
<div class="text-gray-500 text-sm">รายจ่ายทั้งหมด</div>
<div class="text-3xl font-bold text-red-600 mt-1">
{{ myActSumData.summary.totalExpense || 0 | number:'1.0-2' }} บาท
</div>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition">
<div class="text-gray-500 text-sm">คงเหลือ</div>
<div
class="text-3xl font-bold mt-1"
[ngClass]="myActSumData.summary.netProfit >= 0 ? 'text-blue-600' : 'text-red-600'"
>
{{ myActSumData.summary.netProfit | number:'1.0-2' }} บาท
</div>
</div>
</div>
<!-- <section class="dashboard__periods">
<article class="period-card" *ngFor="let summary of periodSummaries">
<header class="period-card__header">
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
@@ -39,9 +68,9 @@
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
</footer>
</article>
</section>
</section> -->
<section class="dashboard__stats">
<!-- <section class="dashboard__stats">
<article class="stat-card" *ngFor="let card of kpiCards">
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
<div class="stat-card__body">
@@ -50,47 +79,131 @@
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
</div>
</article>
</section>
</section> -->
<section class="ledger-grid">
<article class="panel quick-log">
<div class="ledger-grid" [formGroup]="saveFrm">
<form class="panel quick-log" [formGroup]="saveFrm">
<div class="panel__header">
<div>
<h2>บันทึกรายการแบบรวดเร็ว</h2>
<p>จดรายรับรายจ่ายภายในไม่กี่คลิก</p>
</div>
</div>
<form class="quick-log__form">
<!-- เปลี่ยน form ด้านในเป็น div แทน เพื่อไม่ให้ form ซ้อน form -->
<div class="quick-log__form">
<!-- 1. ส่วนเลือกประเภท (รายรับ/รายจ่าย) -->
<label>
<span>ประเภท</span>
<div class="quick-log__toggle">
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'i' }" (click)="mode = 'i'">รายรับ</button>
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'">รายจ่าย</button>
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'i' }" (click)="mode = 'i'; saveFrm.get('actcat')?.reset('')">รายรับ</button>
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'; saveFrm.get('actcat')?.reset('')">รายจ่าย</button>
</div>
</label>
<label>
<span>วันที่</span>
<!-- <input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/> -->
<input type="datetime-local"/>
</label>
<div class="quick-log__grid">
<!-- 2. ส่วนวันที่ -->
<div class="mb-2">
<label>
<span>หมวดหมู่</span>
<input type="text" placeholder="เลือกหมวดหมู่" />
</label>
<label>
<span>ยอดเงิน (฿)</span>
<input type="number" placeholder="0.00" />
<span>วันที่ <span class="text-red-500">*</span></span>
<input type="datetime-local" formControlName="actacpdtm"
[class.border-red-500]="saveFrm.get('actacpdtm')?.invalid && (saveFrm.get('actacpdtm')?.value || saveFrm.get('actacpdtm')?.touched)"/>
</label>
<!-- Validate วันที่ -->
@if (saveFrm.get('actacpdtm')?.invalid && (saveFrm.get('actacpdtm')?.value || saveFrm.get('actacpdtm')?.touched)) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>กรุณาระบุวันที่</span>
</div>
}
</div>
<div class="quick-log__grid">
<!-- 3. ส่วนหมวดหมู่ -->
<div class="flex flex-col">
<label class="h-full">
<span>หมวดหมู่ <span class="text-red-500">*</span></span>
@if(mode == 'i'){
<select formControlName="actcat"
class="w-full h-auto px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white"
[class.border-red-500]="saveFrm.get('actcat')?.invalid && (saveFrm.get('actcat')?.dirty || saveFrm.get('actcat')?.touched)">
<option value="" disabled>ไม่เลือก</option>
@for (item of myDropAct.income; track item.dtlcod) {
<option [value]="item.dtlcod">{{ item.dtlnam }}</option>
}
</select>
}@else if(mode == 'e'){
<select formControlName="actcat"
class="w-full h-auto px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white"
[class.border-red-500]="saveFrm.get('actcat')?.invalid && (saveFrm.get('actcat')?.dirty || saveFrm.get('actcat')?.touched)">
<option value="" disabled>ไม่เลือก</option>
@for (item of myDropAct.expense; track item.dtlcod) {
<option [value]="item.dtlcod">{{ item.dtlnam }}</option>
}
</select>
}
</label>
<!-- Validate หมวดหมู่ -->
@if (saveFrm.get('actcat')?.invalid && (saveFrm.get('actcat')?.dirty || saveFrm.get('actcat')?.touched)) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>กรุณาเลือกหมวดหมู่</span>
</div>
}
</div>
<!-- 4. ส่วนยอดเงิน -->
<div class="flex flex-col">
<label>
<span>ยอดเงิน (฿) <span class="text-red-500">*</span></span>
<input type="number" formControlName="actqty" placeholder="0.00"
[class.border-red-500]="saveFrm.get('actqty')?.invalid && (saveFrm.get('actqty')?.dirty || saveFrm.get('actqty')?.touched)"/>
</label>
<!-- Validate ยอดเงิน -->
@if (saveFrm.get('actqty')?.invalid && (saveFrm.get('actqty')?.dirty || saveFrm.get('actqty')?.touched)) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>กรุณาระบุยอดเงิน</span>
</div>
}
</div>
</div>
<!-- 5. ส่วนบันทึกเพิ่มเติม -->
<div class="mb-2">
<label>
<span>บันทึกเพิ่มเติม</span>
<textarea rows="3" placeholder="รายละเอียดการรับ/จ่าย"></textarea>
<textarea rows="3" formControlName="actcmt" placeholder="รายละเอียดการรับ/จ่าย"
[class.border-red-500]="saveFrm.get('actcmt')?.invalid"></textarea>
</label>
<button type="button" class="btn btn--primary">บันทึกรายการ</button>
</form>
</article>
<!-- Validate ความยาวตัวอักษร (ถ้ามี) -->
@if (saveFrm.get('actcmt')?.hasError('maxlength')) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>พิมพ์ได้สูงสุด 200 ตัวอักษร</span>
</div>
}
</div>
<button type="button" class="btn btn--primary" (click)="onSaveSubmit()">บันทึกรายการ</button>
</div>
</form>
<article class="panel ledger-panel">
<div class="panel__header">
@@ -100,32 +213,61 @@
</div>
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
</div>
<div class="ledger-table">
<div class="ledger-table" [class.is-scrollable]="myActData.length > 5">
<div class="ledger-row ledger-head">
<span>รายการ</span>
<span>หมวดหมู่</span>
<span>ยอดเงิน</span>
<span>บันทึก</span>
</div>
<div class="ledger-row" *ngFor="let idx of ledgerEntries; let i = index;">
<!-- @for (idx of myActData; track i; let i = $index) {
<div class="ledger-row">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.type == 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.type == 'i' ? 'รับ' : 'จ่าย' }}
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.type === 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.title }}</p>
<p class="ledger-date">{{ idx.date }}</p>
</div>
</div>
<span class="ledger-category">{{ idx.category }}</span>
<span class="ledger-amount" [ngClass]="idx.type == 'i' ? 'is-credit' : 'is-debit'">
<span class="ledger-amount" [ngClass]="idx.type === 'i' ? 'is-credit' : 'is-debit'">
{{ idx.amount }}
</span>
<span class="ledger-note">{{ idx.note }}</span>
</div>
} -->
@for (idx of myActData; track idx.actseq; let i = $index) {
<div class="ledger-row">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.acttypnam }}</p>
<p class="ledger-date">{{ idx.actacpdtm ?? '' | dtmtodatetime}}</p>
</div>
</div>
<span class="ledger-category">{{ idx.actcatnam }}</span>
<span class="ledger-amount" [ngClass]="idx.acttyp === 'i' ? 'is-credit' : 'is-debit'">
{{ idx.actqty }}
</span>
<span class="ledger-note">{{ idx.actcmt }}</span>
</div>
}
</div>
</article>
</section>
</div>
<section class="dashboard__grid">
<!-- <article class="panel panel--main">
@@ -153,25 +295,26 @@
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
</div>
<div class="pie-panel__content">
<div class="pie-chart" [style.background]="expenseGradient">
<div class="pie-chart" [style.background]="ActSumDataGradient">
<div class="pie-chart__center">
<p>รวมเดือนนี้</p>
<strong>฿732K</strong>
<strong>{{myActSumData.summary.totalExpense}}</strong>
</div>
</div>
<ul class="pie-legend">
<li class="pie-legend__item" *ngFor="let part of expenseBreakdown">
<span class="swatch" [style.background]="part.color"></span>
<li class="pie-legend__item" *ngFor="let idx of myActSumData.pie.expense">
<span class="swatch" [style.background]="idx.color"></span>
<div>
<p class="pie-legend__label">{{ part.label }}</p>
<p class="pie-legend__value">{{ part.value }}%</p>
<p class="pie-legend__label">{{ idx.label }}</p>
<p class="pie-legend__value">{{ idx.percent }}%</p>
<p class="pie-legend__value">{{ idx.value }} บาท</p>
</div>
</li>
</ul>
</div>
</article>
<!-- ตัวเลขซ้อนทับกัน -->
<article class="panel panel--side">
<!-- <article class="panel panel--side">
<div class="panel__header">
<div>
<h2>สรุปสภาพคล่อง</h2>
@@ -188,9 +331,39 @@
</div>
</div>
</div>
</article>
</article> -->
<article class="panel alerts-panel">
<article class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm">
<div class="flex justify-between items-center mb-4">
<div>
<h2 class="text-lg font-bold text-gray-800">สรุปสภาพคล่อง</h2>
<p class="text-xs text-gray-400">อัปเดตล่าสุดเมื่อสักครู่</p>
</div>
</div>
<div class="space-y-3">
<div *ngFor="let ratio of quickRatios"
class="flex justify-between items-center p-2 hover:bg-gray-50 rounded-lg transition">
<p class="text-gray-600 text-sm font-medium flex-1 truncate">
{{ ratio.label }}
</p>
<span class="text-base font-semibold" [ngClass]="ratio.colorClass">
<ng-container *ngIf="isNumber(ratio.value); else textVal">
{{ ratio.value | number:'1.2-2' }}
<span *ngIf="ratio.label !== 'อัตรากำไร'" class="text-xs font-normal text-gray-400">บาท</span>
</ng-container>
<ng-template #textVal>
{{ ratio.value }}
</ng-template>
</span>
</div>
</div>
</article>
<!-- <article class="panel alerts-panel">
<div class="panel__header">
<div>
<h2>การแจ้งเตือนสำคัญ</h2>
@@ -204,9 +377,9 @@
</div>
<span class="alert__tag">{{ alert.tag }}</span>
</div>
</article>
</article> -->
<article class="panel tasks-panel">
<!-- <article class="panel tasks-panel">
<div class="panel__header">
<div>
<h2>รายการยอดค้างจ่าย</h2>
@@ -223,7 +396,7 @@
<span class="task__badge">{{ task.priority }}</span>
</li>
</ul>
</article>
</article> -->
</section>
</section>

View File

@@ -1,6 +1,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { GeneralService } from '../../services/generalservice';
import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData, QuickRatio} from '../../interfaces/dashboard.interface'
import { DashboardStateService } from '../../services/state/dashboard-state.service';
@Component({
selector: 'app-main-dashboard',
@@ -9,15 +11,39 @@ import { GeneralService } from '../../services/generalservice';
styleUrl: './main-dashboard.component.css'
})
export class MainDashboardComponent implements OnInit {
@Output() saveEventSubmit = new EventEmitter<any>();
mode: string = 'i';
isModalOpen: boolean = false;
isSubmitting: boolean = false;
arrearsForm!: FormGroup;
saveFrm!: FormGroup;
myActData: IActData[] = [];
quickRatios: QuickRatio[] = [];
// myDropAct: IStateDrop[] = [];
myDropAct: IStateDrop = { income: [], expense: [] };
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: 0,
profitRate: '',
adjustedProfitRate: '',
period: ''
},
pie: {
income: [],
expense: []
}
};
ActSumDataGradient: any
readonly ownerName = 'Nuttakit';
ownerName = localStorage.getItem('username') || 'ชนกนันต์';
constructor(
private dashboardStateService: DashboardStateService
){}
readonly kpiCards = [
{
@@ -50,21 +76,28 @@ export class MainDashboardComponent implements OnInit {
}
];
readonly revenueTrend = [
{ label: 'ม.ค.', value: 52 },
{ label: 'ก.พ.', value: 61 },
{ label: 'มี.ค.', value: 73 },
{ label: 'เม.ย.', value: 68 },
{ label: 'พ.ค.', value: 82 },
{ label: 'มิ.ย.', value: 77 }
];
// readonly revenueTrend = [
// { label: 'ม.ค.', value: 52 },
// { label: 'ก.พ.', value: 61 },
// { label: 'มี.ค.', value: 73 },
// { label: 'เม.ย.', value: 68 },
// { label: 'พ.ค.', value: 82 },
// { label: 'มิ.ย.', value: 77 }
// ];
readonly quickRatios = [
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
{ label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
{ label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
];
// readonly quickRatios = [
// { label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
// { label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
// { label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
// ];
// ฟังก์ชันนี้ควรเรียกหลังจากได้รับข้อมูล myActSumData แล้ว (เช่นใน subscribe หรือ ngOnChanges)
// เพิ่มใน Class Component
isNumber(val: any): boolean {
return typeof val === 'number';
}
readonly periodSummaries = [
{
label: 'รายปี',
@@ -131,40 +164,40 @@ export class MainDashboardComponent implements OnInit {
}
];
readonly ledgerEntries = [
{
type: 'i',
title: 'ค่าบริการที่ปรึกษา',
category: 'บริการ',
amount: '+฿85,000',
date: 'วันนี้ · 10:15',
note: 'โครงการ Warehouse Automation'
},
{
type: 'e',
title: 'ค่าเช่าออฟฟิศ',
category: 'ค่าใช้จ่ายคงที่',
amount: '-฿48,000',
date: 'วันนี้ · 09:00',
note: 'สำนักงานพระราม 9'
},
{
type: 'i',
title: 'รับเงินมัดจำ',
category: 'สัญญาใหม่',
amount: '+฿120,000',
date: 'เมื่อวาน',
note: 'ลูกค้า Urbane CoWorking'
},
{
type: 'e',
title: 'ค่าวัตถุดิบ',
category: 'ต้นทุนโครงการ',
amount: '-฿27,500',
date: '12 มิ.ย.',
note: 'สั่งผ่าน Blue Supply'
}
];
// readonly ledgerEntries = [
// {
// type: 'i',
// title: 'ค่าบริการที่ปรึกษา',
// category: 'บริการ',
// amount: '+฿85,000',
// date: 'วันนี้ · 10:15',
// note: 'โครงการ Warehouse Automation'
// },
// {
// type: 'e',
// title: 'ค่าเช่าออฟฟิศ',
// category: 'ค่าใช้จ่ายคงที่',
// amount: '-฿48,000',
// date: 'วันนี้ · 09:00',
// note: 'สำนักงานพระราม 9'
// },
// {
// type: 'i',
// title: 'รับเงินมัดจำ',
// category: 'สัญญาใหม่',
// amount: '+฿120,000',
// date: 'เมื่อวาน',
// note: 'ลูกค้า Urbane CoWorking'
// },
// {
// type: 'e',
// title: 'ค่าวัตถุดิบ',
// category: 'ต้นทุนโครงการ',
// amount: '-฿27,500',
// date: '12 มิ.ย.',
// note: 'สั่งผ่าน Blue Supply'
// }
// ];
readonly expenseBreakdown = [
{ label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' },
@@ -174,12 +207,30 @@ export class MainDashboardComponent implements OnInit {
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
];
readonly expenseGradient = this.buildExpenseGradient();
ngOnInit(): void {
this.setupFormControl();
this.dashboardStateService.getStateResult().subscribe(data => {
if (data) {
this.myDropAct = data;
}
});
// ผลลับท์ ของ รายการ
this.dashboardStateService.getStateAccountResult().subscribe(data => {
if (data) {
this.myActData = data;
}
});
// ผลลัพการ คำนวณ ของ ปัญชี ต่างๆ
this.dashboardStateService.getStateSumResult().subscribe(data => {
if (data) {
this.myActSumData = data;
this.ActSumDataGradient = this.buildExpenseGradient()
this.updateQuickRatios();
}
});
}
setupFormControl(){
this.arrearsForm = new FormGroup({
// email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
@@ -190,15 +241,66 @@ export class MainDashboardComponent implements OnInit {
});
this.saveFrm = new FormGroup({
actacpdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
actacpdtm: new FormControl('',[Validators.required]),
actqty: new FormControl('',[Validators.required]),
actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]),
actcmt: new FormControl('',[Validators.maxLength(200)])
});
}
onSaveSubmit(){
updateQuickRatios() {
const summary = this.myActSumData.summary;
// แปลงค่า netProfit เป็นตัวเลขเพื่อเช็คเงื่อนไข (รองรับทั้ง string และ number)
const netProfitVal = parseFloat(String(summary.netProfit));
const profitRateVal = parseFloat(summary.profitRate.replace('%', '')); // ลบ % ออกก่อนเช็ค
this.quickRatios = [
{
label: 'รายรับรวม',
value: summary.totalIncome,
colorClass: 'text-green-600' // รายรับสีเขียวเสมอ (หรือจะใช้สีดำ text-gray-700 ก็ได้)
},
{
label: 'รายจ่ายรวม',
value: summary.totalExpense,
colorClass: 'text-red-500' // รายจ่ายสีแดงอ่อน หรือสีปกติ
},
{
label: 'คงเหลือสุทธิ', // หรือ กำไรสุทธิ
value: netProfitVal, // ส่งเป็นตัวเลขไปให้ Pipe format
// ถ้า >= 0 สีน้ำเงิน, ถ้าติดลบ สีแดง
colorClass: netProfitVal >= 0 ? 'text-blue-600' : 'text-red-600 font-bold'
},
{
label: 'อัตรากำไร',
value: summary.profitRate,
// เช็ค % ถ้าติดลบให้แดง
colorClass: profitRateVal >= 0 ? 'text-blue-600' : 'text-red-600'
},
{
label: 'ระยะเวลา',
value: summary.period,
colorClass: 'text-gray-500'
}
];
}
onSaveSubmit(){
const rawDate = this.saveFrm.get('actacpdtm')?.value; // ค่าดิบ: "2025-11-21T14:23"
let arysave = {
actacpdtm: rawDate ? rawDate.replace(/[-T:]/g, '') : '',
actqty: this.saveFrm.get('actqty')?.value,
actcat: this.saveFrm.get('actcat')?.value,
actcmt: this.saveFrm.get('actcmt')?.value,
acttyp: this.mode
}
this.SaveEventSubmit(arysave);
}
SaveEventSubmit(event: any){
this.saveEventSubmit.emit(event);
}
onArrearsSubmit(){
@@ -206,15 +308,19 @@ export class MainDashboardComponent implements OnInit {
}
private buildExpenseGradient(): string {
if (!this.myActSumData?.pie?.expense?.length) return '';
let current = 0;
const segments = this.expenseBreakdown
const segments = this.myActSumData.pie.expense
.map(part => {
const start = current;
const end = current + part.value;
const percent = parseFloat(part.percent); // แปลงจาก string → number
const end = current + percent;
current = end;
return `${part.color} ${start}% ${end}%`;
})
.join(', ');
return `conic-gradient(${segments})`;
}
}

View File

@@ -0,0 +1,441 @@
:host {
display: block;
padding: 2rem clamp(1rem, 4vw, 3rem);
background: #f8fafc;
min-height: 100%;
}
.report {
max-width: 1280px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.report__header {
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
align-items: flex-end;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.8rem;
color: #94a3b8;
margin: 0 0 0.25rem;
}
.report__header h1 {
margin: 0 0 0.25rem;
font-size: clamp(1.8rem, 4vw, 2.4rem);
color: #0f172a;
}
.muted {
margin: 0;
color: #94a3b8;
font-size: 0.95rem;
}
.report__actions {
display: inline-flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.btn {
border: none;
border-radius: 999px;
padding: 0.65rem 1.4rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn--primary {
background: linear-gradient(135deg, #22d3ee, #0ea5e9);
color: #0f172a;
box-shadow: 0 15px 30px rgba(14, 165, 233, 0.25);
}
.btn--ghost {
background: #fff;
color: #0f172a;
border: 1px solid #cbd5f5;
}
.btn--compact {
padding: 0.45rem 1.1rem;
font-size: 0.9rem;
}
.btn:hover {
transform: translateY(-1px);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.summary-card {
position: relative;
background: #fff;
border-radius: 20px;
padding: 1.25rem;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
}
.summary-card__label {
margin: 0;
color: #64748b;
font-size: 0.9rem;
}
.summary-card h2 {
margin: 0.4rem 0;
font-size: 1.6rem;
color: #0f172a;
}
.summary-card__detail {
margin: 0;
color: #94a3b8;
font-size: 0.9rem;
}
.summary-card__tone {
position: absolute;
inset: 0;
border-radius: 20px;
pointer-events: none;
opacity: 0.15;
}
.tone-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); }
.tone-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); }
.tone-indigo { background: linear-gradient(135deg, #c4b5fd, #818cf8); }
.tone-slate { background: linear-gradient(135deg, #cbd5f5, #94a3b8); }
.report__content {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
gap: 1.5rem;
}
.panel {
background: #fff;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 0 15px 45px rgba(15, 23, 42, 0.08);
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.panel__header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.panel__header h2 {
margin: 0;
}
.panel__header p {
margin: 0;
color: #94a3b8;
font-size: 0.9rem;
}
.table {
border: 1px solid #e2e8f0;
border-radius: 18px;
overflow: hidden;
}
.table__head,
.table__row {
display: grid;
grid-template-columns: 1.2fr 1fr 1.6fr 1fr 0.8fr;
padding: 0.85rem 1rem;
gap: 1rem;
align-items: center;
}
.table__head {
background: #f1f5f9;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #64748b;
}
.table__row:nth-child(even) {
background: rgba(15, 23, 42, 0.015);
}
.table__row strong {
display: block;
}
.table__row small {
display: block;
font-size: 0.85rem;
}
.mono {
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
font-size: 0.9rem;
}
.amount-col {
text-align: right;
font-weight: 600;
}
.income {
color: #16a34a;
}
.expense {
color: #dc2626;
}
.pie-panel__content {
display: flex;
gap: 1.5rem;
align-items: center;
justify-content: center;
}
.pie-chart {
width: 200px;
height: 200px;
border-radius: 50%;
position: relative;
box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
}
.pie-chart__center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 110px;
height: 110px;
border-radius: 50%;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
}
.pie-chart__center p {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
}
.pie-chart__center strong {
color: #0f172a;
}
.pie-legend {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.pie-legend li {
display: flex;
align-items: center;
gap: 0.6rem;
}
.swatch {
width: 14px;
height: 14px;
border-radius: 4px;
}
.legend-label {
margin: 0;
font-weight: 600;
}
.legend-value {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
}
.preview-modal {
position: fixed;
inset: 0;
z-index: 120;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.preview-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(4px);
}
.preview-modal__content {
position: relative;
background: #fff;
border-radius: 24px;
padding: 1.5rem;
width: min(1100px, 100%);
max-height: 90vh;
overflow: auto;
box-shadow: 0 25px 60px rgba(15, 23, 42, 0.35);
}
.preview-modal__header {
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.preview-modal__actions {
display: inline-flex;
gap: 0.5rem;
}
.preview-sheet {
margin-top: 1.5rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 1.5rem;
font-size: 0.95rem;
}
.preview-sheet__header {
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.preview-totals {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.preview-totals p {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
}
.preview-totals strong {
display: block;
color: #0f172a;
}
.preview-pie {
margin: 1.5rem 0;
display: flex;
gap: 1.5rem;
align-items: center;
}
.mini-pie {
width: 140px;
height: 140px;
border-radius: 50%;
box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
}
.preview-pie ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.preview-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.preview-table th,
.preview-table td {
padding: 0.65rem 0.75rem;
border: 1px solid #e2e8f0;
text-align: left;
}
.preview-table th {
background: #f1f5f9;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: #64748b;
}
.preview-table td:last-child {
text-align: right;
font-weight: 600;
}
@media (max-width: 900px) {
.report__content {
grid-template-columns: 1fr;
}
.table__head,
.table__row {
grid-template-columns: repeat(2, minmax(0, 1fr));
text-align: left;
}
.amount-col {
text-align: left;
}
.pie-panel__content {
flex-direction: column;
}
}
@media (max-width: 640px) {
:host {
padding: 1.5rem 1rem 2rem;
}
.report__actions {
width: 100%;
justify-content: flex-start;
}
.preview-modal {
padding: 1rem;
}
}

View File

@@ -0,0 +1,142 @@
<section class="report">
<header class="report__header">
<div>
<p class="eyebrow">สรุปรายงาน</p>
<h1>รายงานรายรับรายจ่าย</h1>
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
</div>
<div class="report__actions">
<button class="btn btn--ghost">ส่งออกเป็น Excel</button>
<button class="btn btn--primary" (click)="openPreview()">ปริ้นรายงาน</button>
</div>
</header>
<section class="summary-grid">
<article class="summary-card" *ngFor="let card of summaryCards">
<p class="summary-card__label">{{ card.label }}</p>
<h2>{{ card.value }}</h2>
<p class="summary-card__detail">{{ card.detail }}</p>
<span class="summary-card__tone" [ngClass]="'tone-' + card.tone"></span>
</article>
</section>
<section class="report__content">
<article class="panel">
<div class="panel__header">
<div>
<h2>สมุดรายวัน</h2>
<p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p>
</div>
<button class="btn btn--compact btn--ghost">กรองข้อมูล</button>
</div>
<div class="table">
<div class="table__head">
<span>วันที่</span>
<span>เลขที่เอกสาร</span>
<span>หัวข้อ</span>
<span>หมวดหมู่</span>
<span class="amount-col">ยอดเงิน</span>
</div>
<div class="table__row" *ngFor="let record of formattedRecords">
<span>{{ record.date }}</span>
<span class="mono">{{ record.doc }}</span>
<span>
<strong>{{ record.topic }}</strong>
<small class="muted">{{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }}</small>
</span>
<span>{{ record.category }}</span>
<span class="amount-col" [ngClass]="record.tone">{{ record.displayAmount }}</span>
</div>
</div>
</article>
<article class="panel pie-panel">
<div class="panel__header">
<div>
<h2>สัดส่วนค่าใช้จ่าย</h2>
<p>เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้</p>
</div>
</div>
<div class="pie-panel__content">
<div class="pie-chart" [style.background]="expenseGradient">
<div class="pie-chart__center">
<p>รวมรายจ่าย</p>
<strong>฿732K</strong>
</div>
</div>
<ul class="pie-legend">
<li *ngFor="let part of expenseBreakdown">
<span class="swatch" [style.background]="part.color"></span>
<div>
<p class="legend-label">{{ part.label }}</p>
<p class="legend-value">{{ part.value }}%</p>
</div>
</li>
</ul>
</div>
</article>
</section>
</section>
<section class="preview-modal" *ngIf="printPreviewOpen">
<div class="preview-modal__backdrop" (click)="closePreview()"></div>
<div class="preview-modal__content">
<header class="preview-modal__header">
<div>
<p class="eyebrow">Print Preview</p>
<h2>รายงานรายรับรายจ่าย</h2>
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
</div>
<div class="preview-modal__actions">
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
<button class="btn btn--primary">พิมพ์</button>
</div>
</header>
<div class="preview-sheet">
<div class="preview-sheet__header">
<div>
<h3>Accounting Summary</h3>
<p>Prepared on {{ reportRange.end }}</p>
</div>
<div class="preview-totals">
<div *ngFor="let total of previewTotals">
<p>{{ total.label }}</p>
<strong>{{ total.value }}</strong>
</div>
</div>
</div>
<div class="preview-pie">
<div class="mini-pie" [style.background]="expenseGradient"></div>
<ul>
<li *ngFor="let part of expenseBreakdown">
<span class="swatch" [style.background]="part.color"></span>
<span>{{ part.label }} · {{ part.value }}%</span>
</li>
</ul>
</div>
<table class="preview-table">
<thead>
<tr>
<th>วันที่</th>
<th>เลขที่</th>
<th>หัวข้อ</th>
<th>หมวดหมู่</th>
<th>ยอดเงิน</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let record of formattedRecords">
<td>{{ record.date }}</td>
<td>{{ record.doc }}</td>
<td>{{ record.topic }}</td>
<td>{{ record.category }}</td>
<td [ngClass]="record.tone">{{ record.displayAmount }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>

View File

@@ -0,0 +1,138 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-main-report',
templateUrl: './main-report.component.html',
standalone: false,
styleUrls: ['./main-report.component.css']
})
export class MainReportComponent {
readonly reportRange = {
start: '1 มิถุนายน 2567',
end: '30 มิถุนายน 2567'
};
readonly summaryCards = [
{ label: 'รายรับรวม', value: '฿1,284,500', detail: '+12.4% MoM', tone: 'mint' },
{ label: 'รายจ่ายรวม', value: '฿732,800', detail: '-4.1% MoM', tone: 'amber' },
{ label: 'กำไรสุทธิ', value: '฿551,700', detail: 'Margin 42.9%', tone: 'indigo' },
{ label: 'บันทึกรายการ', value: '86 รายการ', detail: '32 รายรับ · 54 รายจ่าย', tone: 'slate' }
];
readonly ledgerRecords = [
{
date: '01 มิ.ย. 2567',
doc: 'RCPT-9101',
type: 'income',
topic: 'ค่าบริการที่ปรึกษา',
category: 'บริการ',
amount: 145000
},
{
date: '02 มิ.ย. 2567',
doc: 'EXP-4407',
type: 'expense',
topic: 'ค่าวัสดุโครงการ A',
category: 'ต้นทุนโครงการ',
amount: -38900
},
{
date: '06 มิ.ย. 2567',
doc: 'RCPT-9110',
type: 'income',
topic: 'รับเงินมัดจำโครงการ',
category: 'สัญญาใหม่',
amount: 220000
},
{
date: '09 มิ.ย. 2567',
doc: 'EXP-4412',
type: 'expense',
topic: 'เงินเดือนพนักงาน',
category: 'บุคลากร',
amount: -180000
},
{
date: '12 มิ.ย. 2567',
doc: 'EXP-4416',
type: 'expense',
topic: 'ค่าเช่าออฟฟิศ',
category: 'ค่าใช้จ่ายคงที่',
amount: -48000
},
{
date: '19 มิ.ย. 2567',
doc: 'RCPT-9122',
type: 'income',
topic: 'ค่าสัญญาบริการรายปี',
category: 'บริการ',
amount: 325000
},
{
date: '23 มิ.ย. 2567',
doc: 'EXP-4425',
type: 'expense',
topic: 'ค่าโฆษณาออนไลน์',
category: 'การตลาด',
amount: -72000
},
{
date: '28 มิ.ย. 2567',
doc: 'RCPT-9133',
type: 'income',
topic: 'รายรับจากคู่ค้าใหม่',
category: 'พันธมิตร',
amount: 210500
}
];
readonly expenseBreakdown = [
{ label: 'ต้นทุนโครงการ', value: 34, color: '#10b981' },
{ label: 'บุคลากร', value: 26, color: '#6366f1' },
{ label: 'การตลาด', value: 18, color: '#f97316' },
{ label: 'ค่าใช้จ่ายคงที่', value: 14, color: '#0ea5e9' },
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
];
readonly previewTotals = [
{ label: 'รายรับรวม', value: '฿1,284,500' },
{ label: 'รายจ่ายรวม', value: '฿732,800' },
{ label: 'กำไรสุทธิ', value: '฿551,700' }
];
printPreviewOpen = false;
get expenseGradient(): string {
let current = 0;
const segments = this.expenseBreakdown
.map(slice => {
const start = current;
const end = current + slice.value;
current = end;
return `${slice.color} ${start}% ${end}%`;
})
.join(', ');
return `conic-gradient(${segments})`;
}
get formattedRecords() {
return this.ledgerRecords.map(record => ({
...record,
displayAmount: this.formatCurrency(record.amount),
tone: record.type === 'income' ? 'income' : 'expense'
}));
}
openPreview(): void {
this.printPreviewOpen = true;
}
closePreview(): void {
this.printPreviewOpen = false;
}
private formatCurrency(amount: number): string {
const formatter = new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB', maximumFractionDigits: 0 });
return formatter.format(amount);
}
}

View File

@@ -10,7 +10,7 @@
<div class="flex items-center gap-3 p-5">
<h3 *ngIf="isOpen" class="text-2xl font-bold transition-all duration-300">
Global Sidebar
<img src="logo.png" alt="">
</h3>
</div>
@@ -24,13 +24,13 @@
<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
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
(click)="navigate('/main/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> -->
<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"

View File

@@ -0,0 +1,12 @@
export const CACHEABLE_URLS = {
GET: [
// Add GET URIs here that you want to cache
// e.g., '/api/data'
],
POST: [
'/api/web/accountingSetup',
'/api/nigga'
// Add POST URIs here that you want to cache
// e.g., '/api/search'
]
};

View File

@@ -5,7 +5,7 @@
} @else if(mode == "forgot-password"){
<app-login-forgot (otpEventSubmit)="onOtpSendSubmit($event)" (otpVerifyEventSubmit)="onVerifySubmit($event)"></app-login-forgot>
}
<!-- @else {
} -->
@else if(mode == "register"){
<app-login-register (registeredEventSubmit)="onRegisterSubmit($event)"></app-login-register>
}
</div>

View File

@@ -14,7 +14,7 @@ import { finalize } from 'rxjs/operators';
export class LoginContentComponent implements OnInit {
@ViewChild(LoginForgotComponent) loginForgotComponent!: LoginForgotComponent;
@ViewChild(LoginPageComponent) loginPageComponent!: LoginPageComponent;
mode: 'forgot-password' | 'default' = 'default';
mode: 'forgot-password' | 'register' | 'default' = 'default';
constructor(
private generalService: GeneralService,
@@ -27,6 +27,8 @@ export class LoginContentComponent implements OnInit {
if (param === 'forgot-password') {
this.mode = 'forgot-password';
}else if(param === 'register'){
this.mode = 'register';
} else {
// this.router.navigate(['/login']); // This can cause navigation loops
this.mode = 'default';
@@ -57,6 +59,7 @@ export class LoginContentComponent implements OnInit {
if (result.code === '200' && result.data?.token) {
this.generalService.trowApi(result);
localStorage.setItem('access_token', result.data.token);
localStorage.setItem('username', result.data.usrthinam);
this.router.navigate(['main/dashboard']);
} else {
const errorMessage = result.message_th || result.message || 'Sign-in failed.';
@@ -71,11 +74,38 @@ export class LoginContentComponent implements OnInit {
if (this.loginPageComponent) {
this.loginPageComponent.message = errorMessage;
}
this.generalService.trowApi(error.error || { message_th: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' });
this.generalService.trowApi(error);
}
});
}
onRegisterSubmit(value: any){
const uri = '/api/login/register';
const request = {
firstname: value.username,
lastname: value.userlastname,
password: value.password,
email: value.email,
organization: 'accpj'
};
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
}
else {
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
this.router.navigate(['/login']);
}
});
}
onOtpSendSubmit(value: any){
let uri = '/api/login/otp/send';

View File

@@ -1 +1 @@
<app-main-dashboard></app-main-dashboard>
<app-main-dashboard (saveEventSubmit)="OnSaveSubmit($event)"></app-main-dashboard>

View File

@@ -1,7 +1,10 @@
import { DashboardStateService } from './../../services/state/dashboard-state.service';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ChartConfiguration, ChartOptions } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { GeneralService } from '../../services/generalservice';
import { IDropAct, IStateDrop, IActData, IActSumData } from '../../interfaces/dashboard.interface';
@Component({
selector: 'app-main-dashboard-content',
@@ -11,85 +14,169 @@ import { GeneralService } from '../../services/generalservice';
})
export class MainDashboardContentComponent implements OnInit {
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
public lineChartData: ChartConfiguration<'line'>['data'] = {
labels: [],
datasets: [
{
data: [],
label: 'Revenue',
fill: true,
tension: 0.5,
borderColor: 'rgba(75,192,192,1)',
backgroundColor: 'rgba(75,192,192,0.2)'
}
]
};
public lineChartOptions: ChartOptions<'line'> = {
responsive: true,
scales: {
y: {
beginAtZero: true
}
myDropAct!: IStateDrop;
myActData: IActData[] = [];
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: 0,
profitRate: '',
adjustedProfitRate: '',
period: ''
},
plugins: {
legend: {
display: true,
},
title: {
display: true,
text: 'Revenue Summary - Last 6 Months'
}
pie: {
income: [],
expense: []
}
};
constructor(private generalService: GeneralService) {}
constructor(
private generalService: GeneralService,
private dashboardStateService: DashboardStateService
) {}
ngOnInit(): void {
this.fetchChartData();
let token = localStorage.getItem('access_token')
this.OnSearchAct(token, true);
this.OnSetupDashboard(token, true);
this.OnSearchSum(token, true);
}
fetchChartData(): void {
// NOTE: Using a placeholder endpoint as the actual one was not provided.
const uri = '/api/dashboard/summary-last-6-months';
this.generalService.getRequest(uri).subscribe({
OnSearchAct(value: any, setupFirst: boolean): void {
const uri = '/api/web/accountingSearch';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200' && result.data) {
this.processChartData(result.data);
} else {
console.warn('Could not fetch chart data:', result.message_th);
// Optionally, display placeholder data or an error message
this.setupPlaceholderData();
if (result.code === '200') {
this.generalService.trowApi(result);
this.myActData = result.data;
this.dashboardStateService.setStateAccountResult(this.myActData);
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
console.error('Error fetching chart data:', error);
// Display placeholder data on error to show the graph structure
this.setupPlaceholderData();
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
processChartData(data: any[]): void {
const labels = data.map(item => item.month);
const revenues = data.map(item => item.revenue);
this.lineChartData.labels = labels;
this.lineChartData.datasets[0].data = revenues;
this.chart?.update();
OnSaveSubmit(value: any){
const uri = '/api/web/accountingAdd';
let request = value
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
// this.myActData = result.data;
// this.dashboardStateService.setStateAccountResult(this.myActData);
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
this.ngOnInit();
}
});
}
setupPlaceholderData(): void {
// This function is called if the API fails, to show a sample graph.
const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
const revenues = [1200, 1900, 3000, 5000, 2300, 3200]; // Sample data
this.lineChartData.labels = labels;
this.lineChartData.datasets[0].data = revenues;
this.chart?.update();
OnSetupDashboard(value: any, setupFirst: boolean): void {
const uri = '/api/web/accountingSetup';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
this.myDropAct = result.data
this.dashboardStateService.setStateResult(this.myDropAct)
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
OnSearchSum(value: any, setupFirst: boolean): void {
const uri = '/api/web/accountingSum';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
this.myActSumData = result.data
this.dashboardStateService.setStateSumResult(this.myActSumData);
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
// fetchChartData(): void {
// // NOTE: Using a placeholder endpoint as the actual one was not provided.
// const uri = '/api/dashboard/summary-last-6-months';
// this.generalService.getRequest(uri).subscribe({
// next: (result: any) => {
// if (result.code === '200' && result.data) {
// this.processChartData(result.data);
// } else {
// console.warn('Could not fetch chart data:', result.message_th);
// // Optionally, display placeholder data or an error message
// this.setupPlaceholderData();
// }
// },
// error: (error: any) => {
// console.error('Error fetching chart data:', error);
// // Display placeholder data on error to show the graph structure
// this.setupPlaceholderData();
// }
// });
// }
// processChartData(data: any[]): void {
// const labels = data.map(item => item.month);
// const revenues = data.map(item => item.revenue);
// this.lineChartData.labels = labels;
// this.lineChartData.datasets[0].data = revenues;
// this.chart?.update();
// }
// setupPlaceholderData(): void {
// // This function is called if the API fails, to show a sample graph.
// const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
// const revenues = [1200, 1900, 3000, 5000, 2300, 3200]; // Sample data
// this.lineChartData.labels = labels;
// this.lineChartData.datasets[0].data = revenues;
// this.chart?.update();
// }
}

View File

@@ -6,13 +6,15 @@ import { LoginPageComponent } from '../../component/login-page/login-page.compon
import { ReactiveFormsModule } from '@angular/forms';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { LoginForgotComponent } from '../../component/login-forgot/login-forgot.component';
import { LoginRegisterComponent } from '../../component/login-register/login-register.component';
// import { AppModule } from '../../app.module';
@NgModule({
declarations: [
LoginContentComponent,
LoginPageComponent,
LoginForgotComponent
LoginForgotComponent,
LoginRegisterComponent
],
imports: [
CommonModule,

View File

@@ -1,12 +1,14 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component';
import { MainReportComponent } from '../../component/main-report/main-report.component';
// import { MainReportComponent } from '../../component/main-report/main-report.component';
const routes: Routes = [
{ path: 'dashboard', component: MainDashboardContentComponent },
{ path: 'report', component: MainReportComponent },
// children: [
// {
// path: 'dashboard',

View File

@@ -8,6 +8,8 @@ import { ReactiveFormsModule } from '@angular/forms';
import { MainDashboardComponent } from '../../component/main-dashboard/main-dashboard.component';
import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component';
import { AccDateFormatPipe } from '../../pipe/dtmtodatetime.pipe';
import { MainReportComponent } from '../../component/main-report/main-report.component';
// import { MainReportComponent } from '../../component/main-report/main-report.component';
@@ -16,7 +18,9 @@ import { MainDashboardContentComponent } from '../../content/main-dashboard-cont
@NgModule({
declarations: [
MainDashboardComponent,
MainDashboardContentComponent
MainDashboardContentComponent,
MainReportComponent,
AccDateFormatPipe
// MainReportComponent
],
imports: [
@@ -24,6 +28,9 @@ import { MainDashboardContentComponent } from '../../content/main-dashboard-cont
MainControlRoutingModule,
ReactiveFormsModule
// BrowserAnimationsModule
],
exports: [
AccDateFormatPipe
]
})
export class MainControlModule { }

View File

@@ -0,0 +1,100 @@
export interface IStateDrop {
income: IDropAct[];
expense: IDropAct[];
}
export interface IDropAct {
dtlnam?: string,
dtlcod?: string
}
export interface IActData {
actseq?: number,
actnum?: number,
acttyp?: string,
acttypnam?: string,
actcatnam?: string,
actqty?: number,
actcmt?: string,
actacpdtm?: string
}
export interface IStateResultResponse {
data: IStateDrop;
}
export interface IStateResultResponse {
data: IStateDrop;
}
export interface IActSumData {
summary: IActSummary;
pie: IActPie;
}
export interface IActSummary {
totalIncome: string;
totalExpense: string;
netProfit: number;
profitRate: string;
adjustedProfitRate: string;
period: string;
}
export interface IActPie {
expense: IActCategory[];
income: IActCategory[];
}
export interface IActCategory {
label: string;
value: number;
percent: string;
color: string;
}
export interface QuickRatio {
label: string;
value: string | number;
colorClass: string; // ตัวเก็บชื่อ class สี
}
// ข้อมูลสินค้าหลัก
// export interface IProduct {
// id: string;
// name: string;
// price: number;
// category: string;
// inStock: boolean;
// description?: string; // optional
// imageUrl?: string;
// tags: string[];
// createdAt: Date;
// updatedAt: Date;
// }
// // ข้อมูลสินค้าแบบย่อ (ใช้ในรายการ)
// export interface IProductSummary {
// id: string;
// name: string;
// price: number;
// imageUrl?: string;
// inStock: boolean;
// }
// // ข้อมูลสำหรับฟอร์ม
// export interface IProductForm {
// name: string;
// price: number;
// category: string;
// description?: string;
// inStock: boolean;
// }
// // ข้อมูลการจัดหมวดหมู่
// export interface IProductCategory {
// id: string;
// name: string;
// parentId?: string;
// productCount: number;
// }

View File

@@ -0,0 +1,24 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'dtmtodatetime',
standalone: false
})
export class AccDateFormatPipe implements PipeTransform {
transform(value: string | number): string {
if (value === null || value === undefined) return '';
const str = value.toString();
if (str.length !== 12) return str;
const yyyy = str.slice(0, 4);
const mm = str.slice(4, 6);
const dd = str.slice(6, 8);
const hh = str.slice(8, 10);
const min = str.slice(10, 12);
return `${dd}/${mm}/${yyyy} ${hh}:${min}`;
}
}

View File

@@ -1,12 +1,30 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { jwtDecode } from 'jwt-decode';
export const authGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
try {
const decodedToken: any = jwtDecode(accessToken);
const currentTime = Date.now() / 1000;
if (decodedToken.exp < currentTime) {
// Token expired
localStorage.removeItem('access_token');
router.navigate(['/login']);
return false;
}
return true;
} catch (error) {
// Error decoding token
localStorage.removeItem('access_token');
router.navigate(['/login']);
return false;
}
} else {
router.navigate(['/login']);
return false;

View File

@@ -0,0 +1,52 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CachingService } from './caching.service';
import { CACHEABLE_URLS } from '../config/caching.config';
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: CachingService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isCacheable(req)) {
return next.handle(req);
}
const cachedResponse = this.cache.get(this.getCacheKey(req));
if (cachedResponse) {
return of(cachedResponse.clone());
}
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.put(this.getCacheKey(req), event.clone());
}
})
);
}
private isCacheable(req: HttpRequest<any>): boolean {
if (req.method === 'GET') {
return CACHEABLE_URLS.GET.some(url => req.urlWithParams.includes(url));
}
if (req.method === 'POST') {
return CACHEABLE_URLS.POST.some(url => req.urlWithParams.includes(url));
}
return false;
}
private getCacheKey(req: HttpRequest<any>): string {
if (req.method === 'POST') {
return req.urlWithParams + JSON.stringify(req.body);
}
return req.urlWithParams;
}
}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class CachingService {
private cache = new Map<string, [Date, HttpResponse<any>]>();
private cacheDurationInMs = 600000; // 5 minutes
constructor() { }
get(key: string): HttpResponse<any> | null {
const tuple = this.cache.get(key);
if (!tuple) {
return null;
}
const expires = tuple[0];
const httpResponse = tuple[1];
// Don't observe expired keys
const now = new Date();
if (expires && expires.getTime() < now.getTime()) {
this.cache.delete(key);
return null;
}
return httpResponse;
}
put(key: string, value: HttpResponse<any>): void {
const expires = new Date();
expires.setMilliseconds(expires.getMilliseconds() + this.cacheDurationInMs);
this.cache.set(key, [expires, value]);
}
clear(): void {
this.cache.clear();
}
}

View File

@@ -54,8 +54,14 @@ export class GeneralService {
return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe(
map((res: any) => res),
catchError((error: any) => {
const response = error?.error;
console.error('❌ [POST Request Error]:', error);
return throwError(() => error);
return throwError(() => ({
status: error.status,
code: response?.code ?? '500',
message: response?.message ?? 'Internal Server Error',
message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์'
}));
})
);
}
@@ -66,9 +72,14 @@ export class GeneralService {
return this.http.get(fullUrl, this.getHttpOptions()).pipe(
map((res: any) => res),
catchError((error: any) => {
const response = error?.error;
console.error('❌ [GET Request Error]:', error);
return throwError(() => error);
})
return throwError(() => ({
status: error.status,
code: response?.code ?? '500',
message: response?.message ?? 'Internal Server Error',
message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์'
})); })
);
}

View File

@@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { IDropAct, IStateDrop, IActData, IActSumData } from '../../interfaces/dashboard.interface';
@Injectable({
providedIn: 'root'
})
export class DashboardStateService {
// ประกาศ BehaviorSubject ด้วย Interface
private dashboardState = new BehaviorSubject<IStateDrop | null>(null);
private accounttingState = new BehaviorSubject<IActData[] | null>(null);
private actsumState = new BehaviorSubject<IActSumData | null>(null);
// ส่ง Observable ไปให้ components subscribe
getStateResult(): Observable<IStateDrop | null> {
return this.dashboardState.asObservable();
}
// เซ็ท state
setStateResult(dashboard: IStateDrop): void {
this.dashboardState.next(dashboard);
}
setStateAccountResult(dashboard: IActData[]): void {
this.accounttingState.next(dashboard);
}
setStateSumResult(sumResult: IActSumData): void {
this.actsumState.next(sumResult);
}
// เคลียร์ state
clearState(): void {
this.dashboardState.next(null);
}
getStateAccountResult(): Observable<IActData[] | null> {
return this.accounttingState.asObservable();
}
getStateSumResult(): Observable<IActSumData | null> {
return this.actsumState.asObservable();
}
// ดึงค่า current state (ไม่ใช่ observable)
// getCurrentState(): IDropAct | null {
// return this.dashboardState.value;
// }
}

View File

@@ -1,4 +1,7 @@
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:8000'
apiBaseUrl: 'https://bread-leader-move-created.trycloudflare.com'
// apiBaseUrl: 'http://localhost:8000'
};

View File

@@ -1,4 +1,4 @@
export const environment = {
production: false,
production: true,
apiBaseUrl: 'https://api.nuttakit.work'
};

View File

@@ -12,6 +12,10 @@
z-index: 999999 !important;
}
input::placeholder{
color: #9aa3ad;
}
/* Make sure the page and app root occupy full height so 100vh aligns */
html, body, app-root {
height: 100%;

View 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

43
ng-ttc-frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# 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
/.angular
# Node
/node_moduless
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

View File

@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

33
ng-ttc-frontend/.vscode/launch.json vendored Normal file
View 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
ng-ttc-frontend/.vscode/tasks.json vendored Normal file
View 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
ng-ttc-frontend/README.md Normal file
View File

@@ -0,0 +1,59 @@
# NgTtcFrontend
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.0.7.
## 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.

View File

@@ -0,0 +1,130 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ng-ttc-frontend": {
"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/build:application",
"options": {
"outputPath": "dist/ng-ttc-frontend",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"src/styles.css"
],
"scripts": [
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.development.ts",
"with": "src/environments/environment.ts"
}
],
"optimization": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "1MB",
"maximumError": "2MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kB",
"maximumError": "10kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"host": "0.0.0.0",
"allowedHosts": ["localhost"]
},
"configurations": {
"production": {
"buildTarget": "ng-ttc-frontend:build:production"
},
"development": {
"buildTarget": "ng-ttc-frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"src/styles.css"
],
"scripts": [
]
}
}
}
}
},
"cli": {
"analytics": false
}
}

21419
ng-ttc-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
{
"name": "ng-ttc-frontend",
"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/ng-ttc-frontend/browser/**/*",
"electron/**/*",
"!node_modules/@capacitor/android/**/*"
],
"win": {
"target": [
"nsis",
"msi"
],
"icon": "public/favicon.ico"
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowElevation": true,
"runAfterFinish": false
}
},
"private": true,
"dependencies": {
"@angular/animations": "^20.3.10",
"@angular/common": "^20.3.10",
"@angular/compiler": "^20.3.10",
"@angular/core": "^20.3.10",
"@angular/forms": "^20.3.10",
"@angular/platform-browser": "^20.3.10",
"@angular/platform-browser-dynamic": "^20.3.10",
"@angular/router": "^20.3.10",
"@capacitor/android": "^7.4.4",
"@capacitor/angular": "^2.0.3",
"@capacitor/core": "latest",
"@fortawesome/angular-fontawesome": "^3.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.17",
"chart.js": "^4.5.1",
"dotenv": "^17.2.3",
"jwt-decode": "^4.0.0",
"ng2-charts": "^6.0.1",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.17",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^20.3.10",
"@angular/build": "^20.3.9",
"@angular/cli": "^20.3.9",
"@angular/compiler-cli": "^20.3.10",
"@capacitor/cli": "latest",
"@types/jasmine": "~5.1.0",
"cross-env": "^10.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",
"ngx-toastr": "^19.1.0",
"postcss": "^8.5.6",
"typescript": "~5.9.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,49 @@
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';
import { authGuard } from './services/auth.guard';
import { loginGuard } from './services/login.guard';
const routes: Routes = [
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule), canActivate: [loginGuard] },
{ path: 'license', component: LicensePrivacyTermsComponent},
{
path: 'main',
component: SidebarContentComponent,
canActivate: [
authGuard
],
children: [
{
path: '',
loadChildren: () =>
import('./controls/main-control/main-control.module').then(
(m) => m.MainControlModule
),
},
// {
// path: 'report',
// loadChildren: () =>
// import('./controls/report-control/report-control.module').then(
// (m) => m.ReportControlModule
// ),
// },
],
},
// {path: 'license' , component: LicensePrivacyTermsComponent}
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: '**', redirectTo: 'login' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@@ -0,0 +1 @@
<router-outlet />

View 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 = 'ng-ttc-frontend';
}

View File

@@ -0,0 +1,77 @@
import { NgModule, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { AppRoutingModule } from './app-routing.module';
// import { RouterModule } from '@angular/router';
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, HTTP_INTERCEPTORS } 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';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { CachingInterceptor } from './services/caching.interceptor';
import { MainProject } from './component/main-project/main-project';
import { MainProjectContent } from './content/main-project-content/main-project-content';
import { MainProjectAdd } from './component/main-project-add/main-project-add';
// import { BudgetAproval } from './component/budget-aproval/budget-aproval';
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
@NgModule({
declarations: [
AppComponent,
// LayoutComponent,
SidebarContentComponent,
SidebarComponent,
LicensePrivacyTermsComponent,
// MainProjectAdd,
// MainProject,
// MainProjectContent,
// BudgetAproval,
// AccDateFormatPipe
// DtmtodatetimePipe,
// MainDashboardContentComponent,
// MainDashboardComponent,
// LoginForgotComponent,
// LoginPageComponent,
// LoginPageComponentComponent,
],
imports: [
BrowserModule,
CommonModule,
ToastrModule.forRoot({
positionClass:'toast-top-right',
preventDuplicates: true,
maxOpened: 3,
autoDismiss: true
}),
// ReactiveFormsModule,
BrowserAnimationsModule,
AppRoutingModule,
HttpClientModule,
FontAwesomeModule
],
exports: [
// AccDateFormatPipe
],
providers: [
provideCharts(withDefaultRegisterables()),
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,307 @@
<div class="p-6 space-y-10">
<!-- Header -->
<div class="flex justify-between items-center mb-4 ">
<h2 class="text-xl font-semibold">
รายการงบประมาณของโครงการ: ระบบจัดการน้ำดื่ม
<!-- {{ project?.name }} -->
</h2>
<div class="text-gray-600 text-sm">
แสดงข้อมูล {{ budgetItems.length }} รายการ
</div>
</div>
<!-- Add New Budget Item -->
<!-- <form [formGroup]="addItemForm" class="bg-gray-50 border rounded-xl p-4 mb-6">
<h3 class="font-semibold mb-3 text-gray-700">เพิ่มรายการงบประมาณ</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
หมวดงบ
<div>
<label class="text-sm text-gray-600">หมวดงบประมาณ</label>
<select
formControlName="category"
class="w-full px-4 py-2 border rounded-xl bg-white mt-1
focus:ring-2 focus:ring-blue-200 focus:border-blue-300" >
<option value="">-- เลือกหมวดงบ --</option>
@for (item of budgetCategoriesDrop.expense; track item.dtlcod) {
<option [value]="item.dtlcod">{{ item.dtlnam }}</option>
}
</select>
error
<div
*ngIf="addItemForm.controls['category'].invalid && addItemForm.controls['category'].touched"
class="text-red-500 text-xs mt-1"
>
กรุณาเลือกหมวดงบ
</div>
</div>
รายการ
<div>
<label class="text-sm text-gray-600">ชื่อรายการ</label>
<select
formControlName="name"
class="w-full px-4 py-2 border rounded-xl bg-white mt-1
focus:ring-2 focus:ring-blue-200 focus:border-blue-300"
>
<option value="">-- เลือกรายการ --</option>
<option *ngFor="let it of masterItems" [value]="it.name">
{{ it.name }}
</option>
</select>
<div
*ngIf="addItemForm.controls['name'].invalid && addItemForm.controls['name'].touched"
class="text-red-500 text-xs mt-1"
>
กรุณาเลือกรายการ
</div>
</div>
จำนวน
<div>
<label class="text-sm text-gray-600">จำนวน</label>
<input
type="number"
formControlName="qty"
class="w-full px-4 py-2 border rounded-xl bg-white mt-1
focus:ring-2 focus:ring-blue-200 focus:border-blue-300"
/>
<div
*ngIf="addItemForm.controls['qty'].invalid && addItemForm.controls['qty'].touched"
class="text-red-500 text-xs mt-1"
>
จำนวนต้องมากกว่า 0
</div>
</div>
ราคา
<div>
<label class="text-sm text-gray-600">ราคา</label>
<input
type="number"
formControlName="price"
class="w-full px-4 py-2 border rounded-xl bg-white mt-1
focus:ring-2 focus:ring-blue-200 focus:border-blue-300"/>
<div
*ngIf="addItemForm.controls['price'].invalid && addItemForm.controls['price'].touched"
class="text-red-500 text-xs mt-1"
>
ราคาต้องมากกว่า 0
</div>
</div>
</div>
Add button
<button
type="button"
(click)="addBudgetItem()"
class="mt-4 bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-xl shadow"
>
เพิ่มเข้าตาราง
</button>
</form> -->
<div class="bg-white rounded-lg shadow-md border border-gray-200 ">
<!-- หัวพับได้ -->
<div class="flex justify-between items-center p-4 border-b bg-gray-50 rounded-t-lg" (click)="toggleFormCollapse()">
<h3 class="text-base font-semibold text-gray-700">เพิ่มรายการงบประมาณสำหรับโครงการ</h3>
<button class="text-gray-400 hover:text-gray-600 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"
[ngClass]="{'rotate-180': !isFormExpanded}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<!-- เนื้อหาพับได้ -->
@if(!isFormExpanded){
<div class="p-4 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="borrower_id" class="block text-sm font-medium text-gray-700 mb-1">หมวดงบประมาณ</label>
<select name="" id="" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 shadow-sm">
<option value="">-- เลือกหมวดงบ --</option>
@for (item of budgetCategoriesDrop.expense; track item.dtlcod) {
<option [value]="item.dtlcod">{{ item.dtlnam }}</option>
}
</select>
</div>
<div>
<label for="first_name" class="block text-sm font-medium text-gray-700 mb-1">จำนวนเงิน</label>
<input type="text" id="first_name" name="first_name" type="number" class="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 shadow-sm" placeholder="">
</div>
</div>
<div class="flex justify-end pt-4 space-x-2">
<!-- <button class="flex items-center px-4 py-2 text-sm font-medium text-white bg-gray-500 rounded-md shadow-sm hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
รีเซ็ต
</button> -->
<!-- <button class="flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md shadow-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
ค้นหา
</button> -->
<!-- <button class="flex items-center px-4 py-2 text-sm font-medium text-white bg-green-500 rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0011.414 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3 3v-6" />
</svg>
ส่งออก Excel
</button> -->
<button class="flex items-center px-4 py-2 text-sm font-medium text-white bg-green-500 rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0011.414 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3 3v-6" />
</svg>
เพิ่มเข้าสู่ตาราง
</button>
</div>
</div>
}
</div>
<!-- Table -->
<!-- <div class="overflow-x-auto bg-white shadow rounded-xl border">
<table class="min-w-full border-collapse text-sm">
<thead class="bg-gray-100 border-b">
<tr class="text-gray-700">
<th class="py-3 px-4">ลำดับ</th>
<th class="py-3 px-4">สถานะ</th>
<th class="py-3 px-4">รหัส</th>
<th class="py-3 px-4">ชื่อรายการ</th>
<th class="py-3 px-4 text-center">จำนวน</th>
<th class="py-3 px-4 text-right">ราคา/หน่วย</th>
<th class="py-3 px-4 text-right">ยอดชำระ</th>
<th class="py-3 px-4 text-center">ดำเนินการ</th>
</tr>
</thead>
<tbody>
<tr
*ngFor="let item of budgetItems; let i = index"
class="border-b hover:bg-gray-50 transition"
>
<td class="py-3 px-4">{{ i + 1 }}</td>
<td class="py-3 px-4 text-center">
<span class="text-green-500 text-xl">●</span>
</td>
<td class="py-3 px-4 font-medium">{{ item.code }}</td>
<td class="py-3 px-4 font-semibold">{{ item.name }}</td>
<td class="py-3 px-4 text-center">
<input
type="number"
class="w-20 px-3 py-2 border rounded-lg text-center"
/>
</td>
<td class="py-3 px-4 text-right">
{{ item.price | number:'1.0-2' }}
</td>
<td class="py-3 px-4 text-right font-semibold">
{{ item.qty * item.price | number:'1.0-2' }}
</td>
<td class="py-3 px-4 text-center space-x-2">
<button
class="bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-lg shadow"
>
🗑
</button>
<button
class="bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded-lg shadow"
>
👁
</button>
</td>
</tr>
</tbody>
</table>
</div> -->
<div class="overflow-x-auto bg-white border border-gray-200 rounded-2xl shadow-sm">
<table class="min-w-full text-left border-collapse">
<thead class="bg-gray-100 border-b border-gray-200 text-gray-700 text-sm">
<tr>
<th class="py-3 px-4 font-semibold">ลำดับ</th>
<th class="py-3 px-4 font-semibold">ชื่องบประมาณ</th>
<th class="py-3 px-4 font-semibold text-center">จำนวนเงิน</th>
<th class="py-3 px-4 font-semibold text-right pr-11">ดำเนินการ</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let p of projects; let i = index"
class="border-b border-gray-100 hover:bg-blue-50/20 transition">
<td class="py-4 px-4 text-gray-700">{{ i + 1 }}</td>
<td class="py-4 px-4 font-medium text-gray-700"> {{ p.code }}</td>
<td class="py-4 px-4 text-center">
<input type="number" class="w-40 px-3 py-2 border border-gray-300 rounded-lg text-center hover:shadow-2xl">
</td>
<td class="py-4 px-4 text-right space-x-2 whitespace-nowrap pr-8">
<button
class="bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-lg shadow"
>
🗑
</button>
<button
class="bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded-lg shadow"
>
🖊
</button>
</td>
</tr>
</tbody>
</table>
<!-- endtable -->
</div>
<!-- Summary -->
<div class="text-right mt-4 text-lg font-semibold">
ยอดรวมทั้งหมด:
<span class="text-blue-600">
{{ getTotalAmount() | number:'1.0-2' }} บาท
</span>
</div>
</div>

View File

@@ -0,0 +1,120 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { GeneralService } from '../../services/generalservice';
import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData } from '../../interfaces/dashboard.interface'
import { DashboardStateService } from '../../services/state/dashboard-state.service';
import { Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-budget-aproval',
standalone: false,
templateUrl: './budget-aproval.html',
styleUrl: './budget-aproval.css',
})
export class BudgetAproval {
projectCode: any;
project: any;
addItemForm!: FormGroup;
isFormExpanded: boolean = false;
budgetItems = [
{ code: 'ITEM001', name: 'เอกซ์เรย์', qty: 1, price: 1000 },
{ code: 'ITEM002', name: 'กรอกคำสั่งซื้อ', qty: 1, price: 1500 },
{ code: 'ITEM003', name: 'ตรวจพื้นฐาน', qty: 1, price: 1000 },
];
projects = [
{ code: 'เงินรายได้', name: 'ระบบจัดการน้ำดื่ม', owner: 'นาย A', budget: 20000, status: 'WAIT', acp: 0, bdgnam: 'ยังไมจัดสรร' },
{ code: 'งบดำเนินงาน', name: 'ปรับปรุงอาคาร B', owner: 'นางสาว B', budget: 45000, status: 'WAIT', acp: 0, bdgnam: 'ยังไมจัดสรร'},
{ code: 'ค่ากิจกรรมพัฒนาคุณภาพผู้เรียน', name: 'ซื้อคอมพิวเตอร์', owner: 'นาย C', budget: 30000, status: 'APPROVED', acp: 20000, bdgnam: 'งบดำเนินงาน' }
];
budgetCategoriesDrop = {
expense: [
{ dtlcod: 'BDG001', dtlnam: 'เงินรายได้' },
{ dtlcod: 'BDG002', dtlnam: 'งบดำเนินงาน ปวส.' },
{ dtlcod: 'BDG003', dtlnam: 'โครงการส่งเสริมพัฒนาทักษะวิชาชีพทักษะพื้นฐาน' },
{ dtlcod: 'BDG004', dtlnam: 'ค่ากิจกรรมพัฒนาคุณภาพผู้เรียน' },
{ dtlcod: 'BDG005', dtlnam: 'อุดหนุนส่งเสริมและพัฒนาผู้เรียนองค์การวิชาชีพแห่งประเทศไทย (อวท.)' },
{ dtlcod: 'BDG006', dtlnam: 'งบดำเนินงาน ระยะสั้น' },
{ dtlcod: 'BDG007', dtlnam: 'โครงการบูรณาการการพัฒนาทักษะทางวิชาชีพกับการเสริมสร้างคุณลักษณะอันพึงประสงค์ (FIX IT)' },
{ dtlcod: 'BDG008', dtlnam: 'โครงการพัฒนาทักษะและสมรรถนะวิชาชีพกำลังคน (Up-skill, Re-skill)' },
{ dtlcod: 'BDG009', dtlnam: 'งบดำเนินงาน ปวช.' },
{ dtlcod: 'BDG010', dtlnam: 'โครงการขยายและยกระดับการจัดอาชีวศึกษาระบบทวิภาคีคุณภาพสูง' },
{ dtlcod: 'BDG011', dtlnam: 'ปวช.(สอจ)' },
{ dtlcod: 'BDG012', dtlnam: 'ค่าจัดการเรียนการสอน' },
{ dtlcod: 'BDG013', dtlnam: 'งบดำเนินงาน 170000' },
{ dtlcod: 'BDG014', dtlnam: 'โครงการพัฒนาทักษะและศักยภาพภาพการจัดการเรียนการสอนอาชีวศึกษา' },
{ dtlcod: 'BDG015', dtlnam: 'ศึกษาธิการ' },
{ dtlcod: 'BDG016', dtlnam: 'Up-skill' },
{ dtlcod: 'BDG017', dtlnam: 'ติดตามผู้สำเร็จ' },
{ dtlcod: 'BDG018', dtlnam: 'ทวิภาคี' },
{ dtlcod: 'BDG019', dtlnam: 'พันธุกรรม' },
{ dtlcod: 'BDG020', dtlnam: 'ปวส(สาธารณูประโภค)' },
{ dtlcod: 'BDG021', dtlnam: 'ปวส(ค่าสาธารณูปโภค)' },
{ dtlcod: 'BDG022', dtlnam: 'งบดำเนินงาน (ค่าสาธารณูปโภค)' },
{ dtlcod: 'BDG023', dtlnam: 'ค่าหนังสือเรียน' },
{ dtlcod: 'BDG024', dtlnam: 'โครงการอาชีวะต้านยาเสพติด' },
{ dtlcod: 'BDG025', dtlnam: 'ค่าตอบแทนพนักงานราชการ' },
{ dtlcod: 'BDG026', dtlnam: 'ค่าอุปกรณ์การเรียน' },
{ dtlcod: 'BDG027', dtlnam: 'โครงการยกระดับและพัฒนาขีดความสามารถด้านภาษาและทักษะดิจิทัลเพื่อพัฒนากำลังคนให้มีสมรรถนะสูง' },
{ dtlcod: 'BDG028', dtlnam: 'โครงการอนุรักษ์พันธุกรรมพืชอันเนื่องมาจากพระราชดำริ' },
{ dtlcod: 'BDG029', dtlnam: 'ปวช.(สอจ.)' },
{ dtlcod: 'BDG030', dtlnam: 'โครงการพัฒนาศักยภาพผู้เรียนอาชีวศึกษาในการเป็นผู้ประกอบการ (บ่มเพาะ)' },
{ dtlcod: 'BDG031', dtlnam: 'โครงการพัฒนาและยกระดับการติดตามผู้สำเร็จการศึกษาอาชีวศึกษา' },
{ dtlcod: 'BDG032', dtlnam: 'โครงการเสริมสร้างคุณธรรม จริยธรรมและธรรมาภิบาลในสถานศึกษา' },
{ dtlcod: 'BDG033', dtlnam: 'โครงการจัดการอาชีวศึกษาเพื่อสนองพระราชดำริ' },
{ dtlcod: 'BDG034', dtlnam: 'เงินรายได้ ป.ตรี' },
{ dtlcod: 'BDG035', dtlnam: 'งบดำเนินงาน 235200' }
]
};
ngOnInit(): void {
this.setupForm();
}
setupForm() {
this.addItemForm = new FormGroup({
category: new FormControl('', [Validators.required]),
name: new FormControl('', [Validators.required]),
qty: new FormControl(1, [Validators.required, Validators.min(1)]),
price: new FormControl(0, [Validators.required, Validators.min(1)])
});
}
toggleFormCollapse(): void {
this.isFormExpanded = !this.isFormExpanded;
}
addBudgetItem() {
if (this.addItemForm.invalid) {
this.addItemForm.markAllAsTouched();
return;
}
const formValue = this.addItemForm.value;
this.budgetItems.push({
code: 'NEW' + (this.budgetItems.length + 1).toString().padStart(3, '0'),
name: formValue.name,
qty: formValue.qty,
price: formValue.price,
// category: formValue.category
});
// reset form
this.addItemForm.reset({
category: '',
name: '',
qty: 1,
price: 0
});
}
getTotalAmount() {
return this.budgetItems.reduce((sum, item) => sum + item.qty * item.price, 0);
}
}

View File

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

View File

@@ -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&#64;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>

View File

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

View File

@@ -0,0 +1,235 @@
: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);
--success-color: #10b981; /* Green for success/confirm */
}
/* Page layout (unchanged) */
.login-widget {
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 (unchanged) */
.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;
max-height: calc(100vh - 56px);
overflow: auto;
}
/* Modal/backdrop styles (unchanged) */
.login-backdrop{
position: fixed;
inset: 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;
overflow: hidden;
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
}
/* Brand area (unchanged) */
.brand{
text-align: center;
padding: 18px;
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{
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 6px 22px 22px 22px;
}
/* Field label wrapper (unchanged) */
.field{
display: flex;
flex-direction: column;
gap: 6px;
}
.field .label-text{
font-size: 13px;
color: var(--muted);
}
/* Inputs (class 'input-field' added to HTML) */
input[type="email"],
input[type="password"],
input[type="text"],
.input-field { /* เพิ่ม class input-field เพื่อให้สไตล์ถูกใช้กับ input ที่กำหนด */
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);
}
/* Actions row */
.actions{
display: flex;
align-items: center;
gap: 12px;
margin-top: 4px;
flex-direction: row-reverse;
}
/* Custom class for justify-end when using flex-row-reverse */
.actions.justify-end-custom {
justify-content: flex-start;
}
/* Custom class for lift hover effect (used for 'เปิด Modal' button) */
.hover-lift:hover {
transform: translateY(-2.5px);
transition: transform .2s ease;
}
/* PRIMARY BUTTON - แก้ไขสีข้อความให้เป็นสีดำ */
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;
color: #000000; /* ข้อความ disabled ก็เป็นสีดำ */
box-shadow: none;
}
/* Secondary Button Style (สำหรับปุ่ม 'เปิด Modal', 'ส่งอีกครั้ง') */
.primary.secondary-button {
background: transparent;
color: var(--primary);
border: 1px solid var(--primary);
box-shadow: none;
transition: background-color .14s ease;
}
.primary.secondary-button:hover {
background: rgba(0, 120, 212, 0.05);
transform: none;
box-shadow: none;
}
/* Footer (unchanged) */
.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;
}
/* Focus styles (unchanged) */
: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-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
.login-widget .card{
padding: 18px;
width: 100%;
}
.brand h1{ font-size: 18px; }
.biometric span, .primary{ font-size: 13px; }
}

View File

@@ -0,0 +1,86 @@
<div class="login-backdrop">
<div class="login-modal d-flex align-items-center justify-content-center bg-white rounded-2xl">
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
<div class="brand">
<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">
<label class="field">
<span class="label-text">อีเมล์</span>
<input type="email" formControlName="email" class="input-field" 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="text" formControlName="otp" class="input-field" autocomplete="one-time-code" placeholder="123456" alt required/>
</label>
}
<div class="actions justify-end-custom mt-4">
<div class="flex-row hover-lift">
<button class="primary secondary-button" type="button" (click)="isModalOpen = true">
เปิด Modal
</button>
</div>
@if (isSendOtp === false) {
<div class="flex justify-end">
@if (isLoading === true) {
<button type="submit" class="primary" 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 secondary-button" (click)="onSubmin()">
{{ 'ส่งอีกครั้ง' }}
</button>
<button type="submit" class="primary" (click)="onVerifySubmit()">
{{ 'รีเซ็ตรหัสผ่าน' }}
</button>
</div>
}
</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-xl max-w-sm w-fit modal-reset-password">
<h2 class="text-xl font-bold mb-4 text-center">เปลี่ยนรหัสผ่าน</h2>
<hr class="w-full h-px bg-gray-200 border-0 my-3">
<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 text-xs mt-1 block">รหัสผ่านไม่ตรงกัน</span>
}
</div>
<div class="flex justify-end gap-2 mt-5">
<button class="bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors hover:bg-gray-500" (click)="isModalOpen = false">
ปิด
</button>
<button class="bg-green-600 text-white px-4 py-2 rounded-lg font-semibold shadow-md transition-all duration-300 ease-in-out hover:bg-green-700 hover:shadow-lg" (click)="onSetNewPassword()">
ยืนยัน
</button>
</div>
</div>
</div>
}
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,265 @@
: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 {
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;
max-height: calc(100vh - 56px);
overflow: auto;
}
/* Modal/backdrop styles */
.login-backdrop{
position: fixed;
inset: 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);
}
/* Brand area */
.brand{
text-align: center;
padding: 18px; /* Use padding from modal-card .brand */
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{
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 6px 22px 2px 22px; /* Adjusted padding to match card padding */
}
/* 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{
/* ⭐️ แก้ไขตรงนี้: เปลี่ยนสีข้อความเป็นสีดำตามคำขอ */
color: #000000;
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
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;
color: #000000; /* ข้อความ Disabled ก็ยังเป็นสีดำ */
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-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
.login-widget .card{
padding: 18px;
width: 100%;
}
.brand h1{ font-size: 18px; }
.biometric span, .primary{ font-size: 13px; }
}

View File

@@ -0,0 +1,55 @@
<div class="login-backdrop">
<div class="login-modal d-flex align-items-center justify-content-center bg-white rounded-2xl">
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
<div class="brand">
<img src="/logo.png" alt="Company logo" class="logo"/>
<h1 id="signin-title" class="kanit-bold">เข้าสู่ระบบ</h1>
<p class="subtitle">บัญชีโปรแกรมจัดการแผนงานงบประมาณของท่าน</p>
</div>
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form">
<label class="field">
<span class="label-text">อีเมล์</span>
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required class="input-field" />
</label>
<label class="field mt-3">
<span class="label-text">รหัสผ่าน</span>
<input type="password" formControlName="password" autocomplete="current-password" required class="input-field" />
</label>
<div class="actions d-flex justify-content-between align-items-center mt-4">
<label class="stay-signed">
<input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span>
</label>
<button type="submit" class="primary login-button"
[disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
เข้าสู่ระบบ
</button>
</div>
<div class="alt-options mt-4 text-center">
<button type="button" class="biometric btn-icon" (click)="useBiometric()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" class="icon-svg">
<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 mt-2" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
</div>
<div class="footer mt-5 text-center">
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
<span class="divider mx-2"></span>
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,90 @@
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<any>();
faCoffee = faCoffee;
loginForm!: FormGroup;
busy = false;
message = '';
constructor(
private router: Router
) {}
ngOnInit(): void {
this.setupFormControl();
}
setupFormControl(): void {
this.loginForm = new FormGroup({
username: new FormControl('',[Validators.required, Validators.maxLength(100)]),
password: new FormControl( '', [Validators.required, Validators.maxLength(50)]),
remember: new FormControl(false)
});
}
signIn(): void {
if (this.loginForm.invalid) return;
this.signedIn.emit(this.loginForm.value);
}
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;
}
}
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']);
}
}

View File

@@ -0,0 +1,646 @@
:host {
display: block;
padding: 2rem clamp(1.25rem, 4vw, 3rem) 3rem;
background: radial-gradient(120% 120% at 0% 0%, #f6f8ff 0%, #eef5ff 55%, #ffffff 100%);
min-height: 100%;
}
.dashboard {
display: flex;
flex-direction: column;
gap: 2rem;
max-width: 1280px;
margin: 0 auto;
}
.dashboard__hero {
background: #0f172a;
color: #f8fafc;
padding: 2rem;
border-radius: 28px;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: space-between;
align-items: center;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.3);
}
.hero__text h1 {
margin: 0 0 0.5rem;
font-size: clamp(1.8rem, 3vw, 2.5rem);
}
.hero__subtitle {
margin: 0;
color: rgba(248, 250, 252, 0.7);
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.8rem;
color: rgba(248, 250, 252, 0.8);
margin: 0 0 0.5rem;
}
.hero__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.btn {
border: none;
border-radius: 999px;
padding: 0.65rem 1.5rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn--primary {
background: linear-gradient(135deg, #22d3ee, #0ea5e9);
color: #0f172a;
box-shadow: 0 15px 30px rgba(14, 165, 233, 0.35);
}
.btn--ghost {
background: rgba(248, 250, 252, 0.12);
color: #f8fafc;
border: 1px solid rgba(248, 250, 252, 0.2);
}
.btn--compact {
padding: 0.45rem 1.15rem;
font-size: 0.9rem;
}
.btn:focus-visible {
outline: 3px solid rgba(14, 165, 233, 0.4);
outline-offset: 3px;
}
.btn:hover {
transform: translateY(-1px);
}
.dashboard__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 1rem;
}
.dashboard__periods {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
}
.period-card {
background: rgba(15, 23, 42, 0.85);
color: #f8fafc;
border-radius: 22px;
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.4);
}
.period-card__header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: rgba(248, 250, 252, 0.75);
}
.period-card__badge {
padding: 0.2rem 0.75rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.period-card__badge--year { background: rgba(248, 250, 252, 0.14); }
.period-card__badge--month { background: rgba(125, 211, 252, 0.25); }
.period-card__badge--week { background: rgba(110, 231, 183, 0.2); }
.period-card__values {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
}
.muted {
margin: 0;
font-size: 0.8rem;
color: rgba(248, 250, 252, 0.65);
}
.income,
.expense,
.net {
margin: 0.1rem 0 0;
font-size: 1.2rem;
font-weight: 600;
}
.income { color: #34d399; }
.expense { color: #fbbf24; }
.net { color: #38bdf8; }
.trend-chip {
background: rgba(248, 250, 252, 0.12);
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
align-self: flex-start;
}
.stat-card {
background: #ffffff;
border-radius: 20px;
padding: 1.25rem;
display: flex;
gap: 1rem;
align-items: center;
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.08);
}
.stat-card__icon {
width: 52px;
height: 52px;
border-radius: 16px;
flex-shrink: 0;
background: #e2e8f0;
}
.accent-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); }
.accent-lavender { background: linear-gradient(135deg, #ddd6fe, #a78bfa); }
.accent-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); }
.accent-teal { background: linear-gradient(135deg, #99f6e4, #14b8a6); }
.stat-card__label {
margin: 0;
color: #475569;
font-size: 0.9rem;
}
.stat-card__value {
font-size: 1.5rem;
font-weight: 700;
color: #0f172a;
}
.stat-card__trend {
margin: 0;
color: #64748b;
font-size: 0.85rem;
}
.dashboard__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.panel {
background: #ffffff;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 0 12px 35px rgba(15, 23, 42, 0.08);
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.panel--main {
grid-column: span 2;
min-height: 280px;
}
.panel--side {
grid-column: span 1;
}
@media (max-width: 900px) {
.panel--main {
grid-column: span 1;
}
}
.panel__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.panel__header h2 {
margin: 0;
font-size: 1.2rem;
}
.panel__header p {
margin: 0;
color: #94a3b8;
font-size: 0.9rem;
}
.ledger-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.quick-log__form {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.quick-log__form label {
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.9rem;
color: #475569;
}
.quick-log__form input,
.quick-log__form textarea {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 0.75rem 1rem;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.quick-log__form input:focus,
.quick-log__form textarea:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
outline: none;
}
.quick-log__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.quick-log__toggle {
display: inline-flex;
gap: 0.4rem;
background: #f1f5f9;
border-radius: 999px;
padding: 0.25rem;
}
.toggle-btn {
border: none;
background: transparent;
border-radius: 999px;
padding: 0.4rem 1.1rem;
font-weight: 600;
color: #475569;
cursor: pointer;
}
.toggle-btn.is-active {
background: #0ea5e9;
color: #f8fafc;
}
.ledger-table {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.pie-panel__content {
display: grid;
grid-template-columns: minmax(220px, 1fr) 1fr;
gap: 2rem;
align-items: center;
}
.pie-chart {
width: 220px;
height: 220px;
border-radius: 50%;
position: relative;
margin: 0 auto;
box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
}
.pie-chart__center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120px;
height: 120px;
border-radius: 50%;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.1);
}
.pie-chart__center p {
margin: 0;
font-size: 0.85rem;
color: #94a3b8;
}
.pie-chart__center strong {
font-size: 1.2rem;
color: #0f172a;
}
.pie-legend {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.pie-legend__item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0;
}
.swatch {
width: 14px;
height: 14px;
border-radius: 4px;
}
.pie-legend__label {
margin: 0;
font-weight: 600;
color: #0f172a;
}
.pie-legend__value {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
}
.ledger-row {
display: grid;
grid-template-columns: 2fr 1fr 0.8fr 1.2fr;
gap: 1rem;
align-items: center;
padding: 0.75rem 0.4rem;
border-bottom: 1px solid #e2e8f0;
}
.ledger-head {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: #94a3b8;
}
.ledger-main {
display: flex;
align-items: center;
gap: 0.75rem;
}
.pill {
padding: 0.2rem 0.7rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
.pill--income {
background: rgba(16, 185, 129, 0.12);
color: #059669;
}
.pill--expense {
background: rgba(248, 113, 113, 0.15);
color: #dc2626;
}
.ledger-title {
margin: 0;
font-weight: 600;
}
.ledger-date {
margin: 0;
font-size: 0.85rem;
color: #94a3b8;
}
.ledger-category {
font-weight: 500;
color: #475569;
}
.ledger-amount {
font-weight: 700;
}
.ledger-note {
color: #64748b;
font-size: 0.9rem;
}
.trend-chart {
display: flex;
gap: 1rem;
align-items: flex-end;
min-height: 180px;
}
.trend-chart__bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.trend-chart__value {
width: 100%;
border-radius: 16px 16px 6px 6px;
background: linear-gradient(180deg, rgba(14, 165, 233, 0.8) 0%, rgba(56, 189, 248, 0.4) 100%);
}
.trend-chart__label {
font-size: 0.85rem;
color: #475569;
}
.ratio-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.ratio {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 18px;
padding: 0.85rem 1.2rem;
font-weight: 600;
}
.ratio--positive {
background: rgba(16, 185, 129, 0.12);
color: #047857;
}
.ratio--neutral {
background: rgba(59, 130, 246, 0.12);
color: #1d4ed8;
}
.ratio--warning {
background: rgba(251, 191, 36, 0.15);
color: #b45309;
}
.alerts-panel .alert {
display: flex;
justify-content: space-between;
align-items: center;
background: #f8fafc;
border-radius: 18px;
padding: 1rem 1.2rem;
gap: 1rem;
}
.alert__title {
margin: 0 0 0.3rem;
font-weight: 600;
color: #0f172a;
}
.alert__detail {
margin: 0;
color: #64748b;
font-size: 0.9rem;
}
.alert__tag {
padding: 0.4rem 0.9rem;
border-radius: 999px;
background: #e0f2fe;
color: #0369a1;
font-size: 0.85rem;
font-weight: 600;
}
.task-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.task {
background: #f8fafc;
border-radius: 18px;
padding: 1rem 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.task__title {
margin: 0 0 0.25rem;
font-weight: 600;
}
.task__due {
margin: 0;
color: #94a3b8;
font-size: 0.9rem;
}
.task__badge {
padding: 0.35rem 0.8rem;
border-radius: 12px;
background: #e2e8f0;
font-weight: 600;
}
.is-credit {
color: #10b981;
font-weight: 600;
}
.is-debit {
color: #ef4444;
font-weight: 600;
}
@media (max-width: 640px) {
.dashboard__hero,
.panel {
padding: 1.25rem;
}
.quick-log__grid {
grid-template-columns: 1fr;
}
.pie-panel__content {
grid-template-columns: 1fr;
}
.pie-chart {
width: 180px;
height: 180px;
}
.ledger-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ledger-row span:nth-child(3),
.ledger-row span:nth-child(4) {
text-align: left;
}
}
.quick-log__form select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.ledger-table.is-scrollable {
max-height: 25rem;
overflow-y: auto;
padding-right: 0.5rem;
}

View File

@@ -0,0 +1,380 @@
<section class="dashboard">
<!-- <header class="dashboard__hero">
<div class="hero__text">
<p class="eyebrow">ภาพรวมบัญชี</p>
<h1>ยินดีต้อนรับกลับ, {{ ownerName }}</h1>
<p class="hero__subtitle">
จดบันทึกรายรับรายจ่าย และดูสรุปต่อปี เดือน สัปดาห์ ได้ในหน้าเดียว
</p>
</div>
<div class="hero__actions">
<button class="btn btn--primary">สร้างรายงานด่วน</button>
<button class="btn btn--ghost">อัปโหลดใบเสร็จ</button>
</div>
</header> -->
<!-- <section class="dashboard__periods">
<article class="period-card" *ngFor="let summary of periodSummaries">
<header class="period-card__header">
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
{{ summary.label }}
</span>
<p>{{ summary.note }}</p>
</header>
<div class="period-card__values">
<div>
<p class="muted">รายรับ</p>
<p class="income">{{ summary.income }}</p>
</div>
<div>
<p class="muted">รายจ่าย</p>
<p class="expense">{{ summary.expense }}</p>
</div>
<div>
<p class="muted">คงเหลือสุทธิ</p>
<p class="net">{{ summary.net }}</p>
</div>
</div>
<footer>
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
</footer>
</article>
</section> -->
<!-- <section class="dashboard__stats">
<article class="stat-card" *ngFor="let card of kpiCards">
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
<div class="stat-card__body">
<p class="stat-card__label">{{ card.label }}</p>
<div class="stat-card__value">{{ card.value }}</div>
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
</div>
</article>
</section> -->
<!--
<section class="ledger-grid">
<article class="panel quick-log">
<div class="panel__header">
<div>
<h2>บันทึกรายการแบบรวดเร็ว</h2>
<p>จดรายรับรายจ่ายภายในไม่กี่คลิก</p>
</div>
</div>
<form class="quick-log__form">
<label>
<span>ประเภท</span>
<div class="quick-log__toggle">
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'i' }" (click)="mode = 'i'">รายรับ</button>
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'">รายจ่าย</button>
</div>
</label>
<label>
<span>วันที่</span>
<input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/>
<input type="datetime-local"/>
</label>
<div class="quick-log__grid">
<label>
<span>หมวดหมู่</span>
@if(mode == 'i'){
<select class="w-full h-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white">
<option value="">ไม่เลือก</option>
@for (item of myDropAct.income; track item.dtlcod) {
<option [value]="item.dtlcod">
{{ item.dtlnam }}
</option>
}
</select>
}@else if(mode == 'e'){
<select class="w-full h-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white">
<option value="">ไม่เลือก</option>
@for (item of myDropAct.expense; track item.dtlcod) {
<option [value]="item.dtlcod">
{{ item.dtlnam }}
</option>
}
</select>
}
</label>
<label>
<span>ยอดเงิน (฿)</span>
<input type="number" placeholder="0.00" />
</label>
</div>
<label>
<span>บันทึกเพิ่มเติม</span>
<textarea rows="3" placeholder="รายละเอียดการรับ/จ่าย"></textarea>
</label>
<button type="button" class="btn btn--primary">บันทึกรายการ</button>
</form>
</article>
<article class="panel ledger-panel">
<div class="panel__header">
<div>
<h2>สมุดบันทึกล่าสุด</h2>
<p>แยกสีระหว่างรายรับและรายจ่าย</p>
</div>
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
</div>
<div class="ledger-table" [class.is-scrollable]="myActData.length > 5">
<div class="ledger-row ledger-head">
<span>รายการ</span>
<span>หมวดหมู่</span>
<span>ยอดเงิน</span>
<span>บันทึก</span>
</div>
@for (idx of myActData; track i; let i = $index) {
<div class="ledger-row">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.type === 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.title }}</p>
<p class="ledger-date">{{ idx.date }}</p>
</div>
</div>
<span class="ledger-category">{{ idx.category }}</span>
<span class="ledger-amount" [ngClass]="idx.type === 'i' ? 'is-credit' : 'is-debit'">
{{ idx.amount }}
</span>
<span class="ledger-note">{{ idx.note }}</span>
</div>
}
@for (idx of myActData; track idx.actseq; let i = $index) {
<div class="ledger-row">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.acttypnam }}</p>
<p class="ledger-date">{{ idx.actacpdtm ?? '' | dtmtodatetime}}</p>
</div>
</div>
<span class="ledger-category">{{ idx.actcatnam }}</span>
<span class="ledger-amount" [ngClass]="idx.acttyp === 'i' ? 'is-credit' : 'is-debit'">
{{ idx.actqty }}
</span>
<span class="ledger-note">{{ idx.actcmt }}</span>
</div>
}
</div>
</article>
</section> -->
<section class="dashboard__grid">
<!-- <article class="panel panel--main">
<div class="panel__header">
<div>
<h2>แนวโน้มรายรับ</h2>
<p>สรุป 6 เดือนล่าสุด</p>
</div>
<button class="btn btn--ghost btn--compact">ดาวน์โหลดข้อมูล</button>
</div>
<div class="trend-chart">
<div class="trend-chart__bar" *ngFor="let point of revenueTrend">
<span class="trend-chart__value" [style.height.%]="point.value"></span>
<span class="trend-chart__label">{{ point.label }}</span>
</div>
</div>
</article> -->
<article class="panel panel--main pie-panel">
<div class="panel__header">
<div>
<h2>สัดส่วนค่าใช้จ่าย</h2>
<p>ดูหมวดไหนใช้เงินมากที่สุด</p>
</div>
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
</div>
<div class="pie-panel__content">
<div class="pie-chart" [style.background]="ActSumDataGradient">
<div class="pie-chart__center">
<p>รวมเดือนนี้</p>
<strong>{{myActSumData.summary.totalExpense}}</strong>
</div>
</div>
<ul class="pie-legend">
<li class="pie-legend__item" *ngFor="let idx of myActSumData.pie.expense">
<span class="swatch" [style.background]="idx.color"></span>
<div>
<p class="pie-legend__label">{{ idx.label }}</p>
<p class="pie-legend__value">{{ idx.percent }}%</p>
<p class="pie-legend__value">{{ idx.value }} บาท</p>
</div>
</li>
</ul>
</div>
</article>
<!-- ตัวเลขซ้อนทับกัน -->
<article class="panel panel--side">
<div class="panel__header">
<div>
<h2>สรุปสภาพคล่อง</h2>
<p>อัปเดตล่าสุด 5 นาทีที่แล้ว</p>
</div>
</div>
<div class="ratio-list">
<div class="ratio" *ngFor="let ratio of quickRatios" [ngClass]="'ratio--' + ratio.status">
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.5rem;">
<p style="margin:0;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
{{ ratio.label }}
</p>
<span style="margin-left:0.5rem;flex:0 0 auto">{{ ratio.value }}</span>
</div>
</div>
</div>
</article>
<!-- <article class="panel alerts-panel">
<div class="panel__header">
<div>
<h2>การแจ้งเตือนสำคัญ</h2>
<p>จัดลำดับงานค้างก่อนครบกำหนด</p>
</div>
</div>
<div class="alert" *ngFor="let alert of alerts">
<div>
<p class="alert__title">{{ alert.title }}</p>
<p class="alert__detail">{{ alert.detail }}</p>
</div>
<span class="alert__tag">{{ alert.tag }}</span>
</div>
</article> -->
<!-- <article class="panel tasks-panel">
<div class="panel__header">
<div>
<h2>รายการยอดค้างจ่าย</h2>
<p>ช่วยเตือนความจำให้</p>
</div>
<button class="btn btn--primary btn--compact" (click)="isModalOpen = true">เพิ่มงาน</button>
</div>
<ul class="task-list">
<li class="task" *ngFor="let task of tasks">
<div>
<p class="task__title">{{ task.title }}</p>
<p class="task__due">{{ task.due }}</p>
</div>
<span class="task__badge">{{ task.priority }}</span>
</li>
</ul>
</article> -->
</section>
</section>
@if(isModalOpen == true){
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 backdrop-blur-sm transition-all duration-300 ease-in-out" role="dialog" aria-modal="true" [formGroup]="arrearsForm">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-auto overflow-hidden transform scale-100 transition-all duration-300 ease-out">
<!-- Header -->
<header class="flex items-center justify-between gap-4 px-6 py-5 border-b bg-linear-to-r from-rose-50 to-white">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-rose-600" viewBox="0 0 24 24" fill="none" aria-hidden>
<path d="M12 2v6M6 12h12M4 20h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<h2 class="text-lg font-semibold text-gray-900 m-0">เพิ่มยอดค้างชำระ</h2>
<p class="text-sm text-gray-500 m-0">บันทึกยอดที่ยังค้างชำระเพื่อการติดตาม</p>
</div>
</div>
<button type="button" (click)="isModalOpen = false" class="text-gray-400 hover:text-rose-600 p-2 rounded-md transition-colors duration-200" aria-label="ปิด">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none">
<path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</header>
<!-- Form -->
<form class="px-6 py-6 bg-white" (ngSubmit)="onArrearsSubmit()" autocomplete="off" novalidate>
<div class="grid grid-cols-1 gap-2">
<!-- จำนวนเงิน -->
<label class="block">
<span class="text-sm font-medium text-gray-700">จำนวนเงิน (฿)</span>
<div class="mt-1 relative">
<input
type="text"
inputmode="decimal"
id="amount"
formControlName="amount"
placeholder="0.00"
class="w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">THB</span>
</div>
@if(arrearsForm.get('amount')?.touched && arrearsForm.get('amount')?.invalid) {
<p class="mt-1 text-xs text-red-600">
กรุณากรอกจำนวนเงินที่ถูกต้อง
</p>
}
</label>
<label class="block">
<span class="text-sm font-medium text-gray-700">วันครบกำหนกจ่าย</span>
<div class="mt-1 relative">
<input type="datetime-local" formControlName="expdtm" class=" w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"/>
</div>
@if(arrearsForm.get('expdtm')?.touched && arrearsForm.get('expdtm')?.invalid) {
<p class="mt-1 text-xs text-red-600">
กรุณาระบุวันครบกำหนดชำระ
</p>
}
</label>
<!-- เหตุผล -->
<label class="block">
<span class="text-sm font-medium text-gray-700">เหตุผล</span>
<input
type="text"
id="reason"
formControlName="reason"
placeholder="เช่น บิลค้างชำระจากผู้ขาย"
class="mt-1 w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"
/>
@if(arrearsForm.get('reason')?.touched && arrearsForm.get('reason')?.invalid) {
<p class="mt-1 text-xs text-red-600">
กรุณากรอกเหตุผล
</p>
}
</label>
<!-- บันทึกเพิ่มเติม -->
<label class="block">
<span class="text-sm font-medium text-gray-700">บันทึกเพิ่มเติม (ไม่บังคับ)</span>
<textarea
rows="3"
formControlName="note"
placeholder="รายละเอียดเพิ่มเติม (เช่น เลขใบแจ้งหนี้ หรือ ผู้ติดต่อ)"
class="mt-1 w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 resize-none transition-all"
></textarea>
</label>
</div>
<!-- Footer -->
<footer class="flex items-center justify-end gap-3 pt-4 border-t mt-4">
<button type="button" (click)="isModalOpen = false" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300 transition-colors duration-200">
ยกเลิก
</button>
<button type="submit" class="rounded-2xl">
บันทึก
</button>
</footer>
</form>
</div>
</div>
}

View File

@@ -0,0 +1,264 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { GeneralService } from '../../services/generalservice';
import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData } from '../../interfaces/dashboard.interface'
import { DashboardStateService } from '../../services/state/dashboard-state.service';
@Component({
selector: 'app-main-dashboard',
standalone: false,
templateUrl: './main-dashboard.component.html',
styleUrl: './main-dashboard.component.css'
})
export class MainDashboardComponent implements OnInit {
mode: string = 'i';
isModalOpen: boolean = false;
isSubmitting: boolean = false;
arrearsForm!: FormGroup;
saveFrm!: FormGroup;
myActData: IActData[] = [];
// myDropAct: IStateDrop[] = [];
myDropAct: IStateDrop = { income: [], expense: [] };
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: '',
profitRate: '',
adjustedProfitRate: '',
period: ''
},
pie: {
income: [],
expense: []
}
};
ActSumDataGradient: any
readonly ownerName = 'Nuttakit';
constructor(
private dashboardStateService: DashboardStateService
){}
readonly kpiCards = [
{
label: 'รายรับรวม',
value: '฿1.28M',
trend: '+12.4%',
context: 'เทียบกับเดือนก่อน',
accent: 'mint'
},
{
label: 'รายจ่ายรวม',
value: '฿732K',
trend: '-4.1%',
context: 'จัดการได้ดีขึ้น',
accent: 'lavender'
},
{
label: 'ยอดค้างชำระ',
value: '฿184K',
trend: '-2 ใบแจ้งหนี้',
context: 'รอติดตาม',
accent: 'amber'
},
{
label: 'อัตรากำไร',
value: '37.8%',
trend: '+1.9 จุด',
context: 'ระยะ 30 วัน',
accent: 'teal'
}
];
// readonly revenueTrend = [
// { label: 'ม.ค.', value: 52 },
// { label: 'ก.พ.', value: 61 },
// { label: 'มี.ค.', value: 73 },
// { label: 'เม.ย.', value: 68 },
// { label: 'พ.ค.', value: 82 },
// { label: 'มิ.ย.', value: 77 }
// ];
readonly quickRatios = [
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
{ label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
{ label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
];
readonly periodSummaries = [
{
label: 'รายปี',
note: 'ปี 2567',
income: '฿9.6M',
expense: '฿5.1M',
net: '+฿4.5M',
trend: '+18%',
badge: 'year'
},
{
label: 'รายเดือน',
note: 'มิถุนายน 2567',
income: '฿1.28M',
expense: '฿732K',
net: '+฿548K',
trend: '+6%',
badge: 'month'
},
{
label: 'รายสัปดาห์',
note: 'สัปดาห์ที่ 24',
income: '฿312K',
expense: '฿188K',
net: '+฿124K',
trend: '+2%',
badge: 'week'
}
];
readonly alerts = [
{
title: 'ใบแจ้งหนี้ #INV-083 จะครบกำหนด',
detail: 'ลูกค้า Metro Engineering',
tag: 'ภายใน 3 วัน'
},
{
title: 'มีเอกสารที่ต้องอนุมัติ 2 รายการ',
detail: 'เบิกค่าใช้จ่ายฝ่ายการตลาด',
tag: 'รออนุมัติ'
},
{
title: 'พบรายการใช้จ่ายผิดปกติ',
detail: 'ค่าใช้จ่ายเดินทางสูงกว่าค่าเฉลี่ย 28%',
tag: 'ตรวจสอบ'
}
];
readonly tasks = [
{
title: 'กระทบยอดธนาคาร เดือน มิ.ย.',
due: 'วันนี้ 16:00',
priority: 'สูง'
},
{
title: 'เตรียมรายงาน VAT',
due: 'พรุ่งนี้ 10:30',
priority: 'กลาง'
},
{
title: 'ออกใบเสนอราคา โครงการใหม่',
due: 'ศุกร์ 14:00',
priority: 'ต่ำ'
}
];
// readonly ledgerEntries = [
// {
// type: 'i',
// title: 'ค่าบริการที่ปรึกษา',
// category: 'บริการ',
// amount: '+฿85,000',
// date: 'วันนี้ · 10:15',
// note: 'โครงการ Warehouse Automation'
// },
// {
// type: 'e',
// title: 'ค่าเช่าออฟฟิศ',
// category: 'ค่าใช้จ่ายคงที่',
// amount: '-฿48,000',
// date: 'วันนี้ · 09:00',
// note: 'สำนักงานพระราม 9'
// },
// {
// type: 'i',
// title: 'รับเงินมัดจำ',
// category: 'สัญญาใหม่',
// amount: '+฿120,000',
// date: 'เมื่อวาน',
// note: 'ลูกค้า Urbane CoWorking'
// },
// {
// type: 'e',
// title: 'ค่าวัตถุดิบ',
// category: 'ต้นทุนโครงการ',
// amount: '-฿27,500',
// date: '12 มิ.ย.',
// note: 'สั่งผ่าน Blue Supply'
// }
// ];
readonly expenseBreakdown = [
{ label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' },
{ label: 'การตลาด', value: 18, color: '#f97316' },
{ label: 'ต้นทุนโครงการ', value: 27, color: '#10b981' },
{ label: 'บุคลากร', value: 15, color: '#a855f7' },
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
];
ngOnInit(): void {
this.setupFormControl();
this.dashboardStateService.getStateResult().subscribe(data => {
if (data) {
this.myDropAct = data;
}
});
// ผลลับท์ ของ รายการ
this.dashboardStateService.getStateAccountResult().subscribe(data => {
if (data) {
this.myActData = data;
}
});
// ผลลัพการ คำนวณ ของ ปัญชี ต่างๆ
this.dashboardStateService.getStateSumResult().subscribe(data => {
if (data) {
this.myActSumData = data;
this.ActSumDataGradient = this.buildExpenseGradient()
}
});
}
setupFormControl(){
this.arrearsForm = new FormGroup({
// email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
amount: new FormControl('',[Validators.required, Validators.maxLength(20)]),
expdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
note: new FormControl('',[Validators.maxLength(200)]),
reason: new FormControl('',[Validators.required, Validators.maxLength(200)])
});
this.saveFrm = new FormGroup({
actacpdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
actqty: new FormControl('',[Validators.required]),
actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]),
actcmt: new FormControl('',[Validators.maxLength(200)])
});
}
onSaveSubmit(){
}
onArrearsSubmit(){
}
private buildExpenseGradient(): string {
if (!this.myActSumData?.pie?.expense?.length) return '';
let current = 0;
const segments = this.myActSumData.pie.expense
.map(part => {
const start = current;
const percent = parseFloat(part.percent); // แปลงจาก string → number
const end = current + percent;
current = end;
return `${part.color} ${start}% ${end}%`;
})
.join(', ');
return `conic-gradient(${segments})`;
}
}

View File

@@ -0,0 +1,232 @@
<div class="min-h-screen bg-gray-100 p-4">
<header class="flex justify-between items-center py-4 px-2">
<h1 class="text-xl font-semibold text-gray-800">หน้าหลัก ( landing )</h1>
<div class="flex space-x-4 text-2xl text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c0-.542.448-.99 1.042-.99.594 0 1.042.448 1.042.99 0 1.62-.99 2.534-1.99 3.438-1.042 1.042-2.042 2.042-2.042 3.042h4" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 21a9 9 0 110-18 9 9 0 010 18z" />
</svg> -->
<img src="interrogation.png" alt="" class="h-7 w-7 opacity-70">
<div class="h-7 w-7 rounded-full bg-gray-300">
<img src="user_0.png" alt="">
</div>
</div>
</header>
<main class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
<div class="md:col-span-2 ">
<div class="bg-white p-6 rounded-xl shadow-lg h-88">
<h2 class="text-lg font-medium text-gray-700 mb-4">เมนู</h2>
<div class="grid grid-cols-2 gap-4">
<button class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-150 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m4 2h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span class="text-gray-700">การเบิกงวดงบ</span>
</button>
<button class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-150 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<span class="text-gray-700">สัญญา / ข้อตกลง</span>
</button>
<button class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-150 cursor-pointer">
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.102a9 9 0 11-12.728 0m12.728 0a9 9 0 00-12.728 0M3 12a9 9 0 1118 0" />
</svg> -->
<img src="stamp_0.png" alt="" class="h-6 w-6 text-gray-500 opacity-50">
<span class="text-gray-700">การอนุมัติโครงการ</span>
</button>
<button class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-150 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="text-gray-700">เอกสารฟอร์มวิทยาลัย</span>
</button>
<button class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-150 cursor-pointer">
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.992 9.992 0 0112 21c-3.142 0-6.186-1.042-8.625-2.999M21 12a9.992 9.992 0 00-3-6.945M3 12a9.992 9.992 0 013-6.945M12 15a3 3 0 100-6 3 3 0 000 6z" />
</svg> -->
<img src="chart-simple.png" alt="" class="h-6 w-6 text-gray-500 opacity-50">
<span class="text-gray-700">จัดสรรงบประมาณ</span>
</button>
<button class="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-150 cursor-pointer" (click)="navigate('main/report')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
<span class="text-gray-700">วิเคราะห์ & รายงาน</span>
</button>
</div>
</div>
</div>
<div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-lg font-medium text-gray-700 mb-4">กล่องแจ้งเตือน</h2>
<div class="space-y-4">
<div class="flex items-start space-x-3 p-3 bg-red-50 rounded-lg border-l-4 border-red-500 hover:bg-red-100 transition duration-150 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mt-0.5 text-red-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<p class="text-sm font-medium text-gray-800">มี **โครงการ 3 รายการ** รออนุมัติ</p>
<p class="text-xs text-red-600 mt-0.5">ครบกำหนด: วันนี้, 15:00 น.</p>
</div>
</div>
<div class="flex items-start space-x-3 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-500 hover:bg-blue-100 transition duration-150 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mt-0.5 text-blue-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="text-sm font-medium text-gray-800">เอกสารฟอร์มวิทยาลัย **ฉบับใหม่** พร้อมดาวน์โหลดแล้ว</p>
<p class="text-xs text-gray-500 mt-0.5">ประกาศเมื่อ: 17 พ.ย. 2568</p>
</div>
</div>
<div class="flex items-start space-x-3 p-3 bg-green-50 rounded-lg border-l-4 border-green-500 hover:bg-green-100 transition duration-150 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mt-0.5 text-green-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.102a9 9 0 11-12.728 0m12.728 0a9 9 0 00-12.728 0" />
</svg>
<div>
<p class="text-sm font-medium text-gray-800">การเปิดงบประมาณ **ไตรมาส 4** ได้รับการอนุมัติแล้ว</p>
<p class="text-xs text-gray-500 mt-0.5">สำเร็จเมื่อ: 15 พ.ย. 2568</p>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- <section class="mt-6">
<div class="bg-white p-6 rounded-xl shadow-lg flex items-center space-x-6">
<div class="shrink-0">
<div class="h-24 w-24 rounded-full bg-white flex items-center justify-center overflow-hidden">
<img src="logo.png" alt="">
<span class="text-center text-white font-bold text-sm leading-tight">วิทยาลัยเทคนิคตรัง</span>
</div>
</div>
<div>
<h2 class="text-lg font-medium text-gray-700 mb-3">อัพเดตซอฟต์แวร์</h2>
<div class="space-y-2">
<div class="h-4 bg-red-100 rounded w-64"></div>
<div class="h-4 bg-red-100 rounded w-56"></div>
</div>
<table>
<tbody>
<td>sdsd</td>
<td>sdasds</td>
</tbody>
</table>
</div>
</div>
</section> -->
<section class="mt-6">
<div class="bg-white p-6 rounded-xl shadow-lg flex items-start space-x-6">
<div class="shrink-0">
<div class="h-24 w-24 rounded-full bg-white flex items-center justify-center overflow-hidden border border-gray-200">
<img src="logo.png" alt="Trang Technical College Logo" class="h-full w-full object-cover">
</div>
</div>
<div class="grow">
<h2 class="text-xl font-semibold text-gray-800 mb-3">อัพเดตซอฟต์แวร์ล่าสุด</h2>
<div class="flex text-sm font-medium text-gray-500 border-b pb-1 mb-2">
<span class="w-2/5 pr-2">รายการ</span>
<span class="w-1/5">ประเภท</span>
<span class="w-1/5 text-right">เวอร์ชัน</span>
<span class="w-1/5 pl-2">สถานะ</span>
</div>
<div class="flex items-center py-2 border-b last:border-b-0 hover:bg-gray-50 transition duration-100">
<div class="w-2/5 flex items-center space-x-3 pr-2">
<div>
<p class="text-sm font-medium text-gray-900">แก้ไขบั๊ก (Patch Fix)</p>
<p class="text-xs text-gray-500">18 พ.ย. 2568</p>
</div>
</div>
<span class="w-1/5">
<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800">
ปรับปรุง
</span>
</span>
<span class="w-1/5 text-sm font-medium text-right text-green-600">
1.0.1
</span>
<span class="w-1/5 text-sm text-gray-600 pl-2 truncate">
เสร็จสมบูรณ์
</span>
</div>
<div class="flex items-center py-2 border-b last:border-b-0 hover:bg-gray-50 transition duration-100">
<div class="w-2/5 flex items-center space-x-3 pr-2">
<div>
<p class="text-sm font-medium text-gray-900">อัปเดตระบบหลัก (Major Update)</p>
<p class="text-xs text-gray-500">25 ต.ค. 2568</p>
</div>
</div>
<span class="w-1/5">
<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-red-100 text-red-800">
คุณสมบัติใหม่
</span>
</span>
<span class="w-1/5 text-sm font-medium text-right text-red-600">
2.0.0
</span>
<span class="w-1/5 text-sm text-gray-600 pl-2 truncate">
รอตรวจสอบ
</span>
</div>
<div class="flex items-center py-2 last:border-b-0 hover:bg-gray-50 transition duration-100">
<div class="w-2/5 flex items-center space-x-3 pr-2">
<div>
<p class="text-sm font-medium text-gray-900">แก้ไขช่องโหว่ความปลอดภัย</p>
<p class="text-xs text-gray-500">5 พ.ย. 2568</p>
</div>
</div>
<span class="w-1/5">
<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800">
ปรับปรุง
</span>
</span>
<span class="w-1/5 text-sm font-medium text-right text-green-600">
1.0.0
</span>
<span class="w-1/5 text-sm text-gray-600 pl-2 truncate">
เสร็จสมบูรณ์
</span>
</div>
</div>
</div>
</section>
</div>

View File

@@ -0,0 +1,24 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-main-landing',
standalone: false,
templateUrl: './main-landing.component.html',
styleUrls: ['./main-landing.component.css']
})
export class MainLandingComponent implements OnInit {
constructor(
private router: Router
) {}
ngOnInit() {
}
navigate(path: string) {
this.router.navigate([path]);
}
}

View File

@@ -0,0 +1,648 @@
:host {
display: block;
padding: 2rem clamp(1.25rem, 4vw, 3rem) 3rem;
/* background: radial-gradient(120% 120% at 0% 0%, #f6f8ff 0%, #eef5ff 55%, #ffffff 100%); */
/* background: white; */
background-color: var(--color-gray-100);
min-height: 100%;
}
.dashboard {
display: flex;
flex-direction: column;
gap: 2rem;
max-width: 1280px;
margin: 0 auto;
}
.dashboard__hero {
background: #0f172a;
color: #f8fafc;
padding: 2rem;
border-radius: 28px;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: space-between;
align-items: center;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.3);
}
.hero__text h1 {
margin: 0 0 0.5rem;
font-size: clamp(1.8rem, 3vw, 2.5rem);
}
.hero__subtitle {
margin: 0;
color: rgba(248, 250, 252, 0.7);
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.8rem;
color: rgba(248, 250, 252, 0.8);
margin: 0 0 0.5rem;
}
.hero__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.btn {
border: none;
border-radius: 999px;
padding: 0.65rem 1.5rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn--primary {
background: linear-gradient(135deg, #22d3ee, #0ea5e9);
color: #0f172a;
box-shadow: 0 15px 30px rgba(14, 165, 233, 0.35);
}
.btn--ghost {
background: rgba(248, 250, 252, 0.12);
color: #f8fafc;
border: 1px solid rgba(248, 250, 252, 0.2);
}
.btn--compact {
padding: 0.45rem 1.15rem;
font-size: 0.9rem;
}
.btn:focus-visible {
outline: 3px solid rgba(14, 165, 233, 0.4);
outline-offset: 3px;
}
.btn:hover {
transform: translateY(-1px);
}
.dashboard__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 1rem;
}
.dashboard__periods {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
}
.period-card {
background: rgba(15, 23, 42, 0.85);
color: #f8fafc;
border-radius: 22px;
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.4);
}
.period-card__header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: rgba(248, 250, 252, 0.75);
}
.period-card__badge {
padding: 0.2rem 0.75rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.period-card__badge--year { background: rgba(248, 250, 252, 0.14); }
.period-card__badge--month { background: rgba(125, 211, 252, 0.25); }
.period-card__badge--week { background: rgba(110, 231, 183, 0.2); }
.period-card__values {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
}
.muted {
margin: 0;
font-size: 0.8rem;
color: rgba(248, 250, 252, 0.65);
}
.income,
.expense,
.net {
margin: 0.1rem 0 0;
font-size: 1.2rem;
font-weight: 600;
}
.income { color: #34d399; }
.expense { color: #fbbf24; }
.net { color: #38bdf8; }
.trend-chip {
background: rgba(248, 250, 252, 0.12);
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
align-self: flex-start;
}
.stat-card {
background: #ffffff;
border-radius: 20px;
padding: 1.25rem;
display: flex;
gap: 1rem;
align-items: center;
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.08);
}
.stat-card__icon {
width: 52px;
height: 52px;
border-radius: 16px;
flex-shrink: 0;
background: #e2e8f0;
}
.accent-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); }
.accent-lavender { background: linear-gradient(135deg, #ddd6fe, #a78bfa); }
.accent-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); }
.accent-teal { background: linear-gradient(135deg, #99f6e4, #14b8a6); }
.stat-card__label {
margin: 0;
color: #475569;
font-size: 0.9rem;
}
.stat-card__value {
font-size: 1.5rem;
font-weight: 700;
color: #0f172a;
}
.stat-card__trend {
margin: 0;
color: #64748b;
font-size: 0.85rem;
}
.dashboard__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.panel {
background: #ffffff;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 0 12px 35px rgba(15, 23, 42, 0.08);
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.panel--main {
grid-column: span 2;
min-height: 280px;
}
.panel--side {
grid-column: span 1;
}
@media (max-width: 900px) {
.panel--main {
grid-column: span 1;
}
}
.panel__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.panel__header h2 {
margin: 0;
font-size: 1.2rem;
}
.panel__header p {
margin: 0;
color: #94a3b8;
font-size: 0.9rem;
}
.ledger-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.quick-log__form {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.quick-log__form label {
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.9rem;
color: #475569;
}
.quick-log__form input,
.quick-log__form textarea {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 0.75rem 1rem;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.quick-log__form input:focus,
.quick-log__form textarea:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
outline: none;
}
.quick-log__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.quick-log__toggle {
display: inline-flex;
gap: 0.4rem;
background: #f1f5f9;
border-radius: 999px;
padding: 0.25rem;
}
.toggle-btn {
border: none;
background: transparent;
border-radius: 999px;
padding: 0.4rem 1.1rem;
font-weight: 600;
color: #475569;
cursor: pointer;
}
.toggle-btn.is-active {
background: #0ea5e9;
color: #f8fafc;
}
.ledger-table {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.pie-panel__content {
display: grid;
grid-template-columns: minmax(220px, 1fr) 1fr;
gap: 2rem;
align-items: center;
}
.pie-chart {
width: 220px;
height: 220px;
border-radius: 50%;
position: relative;
margin: 0 auto;
box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
}
.pie-chart__center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120px;
height: 120px;
border-radius: 50%;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.1);
}
.pie-chart__center p {
margin: 0;
font-size: 0.85rem;
color: #94a3b8;
}
.pie-chart__center strong {
font-size: 1.2rem;
color: #0f172a;
}
.pie-legend {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.pie-legend__item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0;
}
.swatch {
width: 14px;
height: 14px;
border-radius: 4px;
}
.pie-legend__label {
margin: 0;
font-weight: 600;
color: #0f172a;
}
.pie-legend__value {
margin: 0;
color: #94a3b8;
font-size: 0.85rem;
}
.ledger-row {
display: grid;
grid-template-columns: 2fr 1fr 0.8fr 1.2fr;
gap: 1rem;
align-items: center;
padding: 0.75rem 0.4rem;
border-bottom: 1px solid #e2e8f0;
}
.ledger-head {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: #94a3b8;
}
.ledger-main {
display: flex;
align-items: center;
gap: 0.75rem;
}
.pill {
padding: 0.2rem 0.7rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
}
.pill--income {
background: rgba(16, 185, 129, 0.12);
color: #059669;
}
.pill--expense {
background: rgba(248, 113, 113, 0.15);
color: #dc2626;
}
.ledger-title {
margin: 0;
font-weight: 600;
}
.ledger-date {
margin: 0;
font-size: 0.85rem;
color: #94a3b8;
}
.ledger-category {
font-weight: 500;
color: #475569;
}
.ledger-amount {
font-weight: 700;
}
.ledger-note {
color: #64748b;
font-size: 0.9rem;
}
.trend-chart {
display: flex;
gap: 1rem;
align-items: flex-end;
min-height: 180px;
}
.trend-chart__bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.trend-chart__value {
width: 100%;
border-radius: 16px 16px 6px 6px;
background: linear-gradient(180deg, rgba(14, 165, 233, 0.8) 0%, rgba(56, 189, 248, 0.4) 100%);
}
.trend-chart__label {
font-size: 0.85rem;
color: #475569;
}
.ratio-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.ratio {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 18px;
padding: 0.85rem 1.2rem;
font-weight: 600;
}
.ratio--positive {
background: rgba(16, 185, 129, 0.12);
color: #047857;
}
.ratio--neutral {
background: rgba(59, 130, 246, 0.12);
color: #1d4ed8;
}
.ratio--warning {
background: rgba(251, 191, 36, 0.15);
color: #b45309;
}
.alerts-panel .alert {
display: flex;
justify-content: space-between;
align-items: center;
background: #f8fafc;
border-radius: 18px;
padding: 1rem 1.2rem;
gap: 1rem;
}
.alert__title {
margin: 0 0 0.3rem;
font-weight: 600;
color: #0f172a;
}
.alert__detail {
margin: 0;
color: #64748b;
font-size: 0.9rem;
}
.alert__tag {
padding: 0.4rem 0.9rem;
border-radius: 999px;
background: #e0f2fe;
color: #0369a1;
font-size: 0.85rem;
font-weight: 600;
}
.task-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.task {
background: #f8fafc;
border-radius: 18px;
padding: 1rem 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.task__title {
margin: 0 0 0.25rem;
font-weight: 600;
}
.task__due {
margin: 0;
color: #94a3b8;
font-size: 0.9rem;
}
.task__badge {
padding: 0.35rem 0.8rem;
border-radius: 12px;
background: #e2e8f0;
font-weight: 600;
}
.is-credit {
color: #10b981;
font-weight: 600;
}
.is-debit {
color: #ef4444;
font-weight: 600;
}
@media (max-width: 640px) {
.dashboard__hero,
.panel {
padding: 1.25rem;
}
.quick-log__grid {
grid-template-columns: 1fr;
}
.pie-panel__content {
grid-template-columns: 1fr;
}
.pie-chart {
width: 180px;
height: 180px;
}
.ledger-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ledger-row span:nth-child(3),
.ledger-row span:nth-child(4) {
text-align: left;
}
}
.quick-log__form select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.ledger-table.is-scrollable {
max-height: 25rem;
overflow-y: auto;
padding-right: 0.5rem;
}

View File

@@ -0,0 +1,131 @@
<div class="w-full p-6">
<!-- Summary Cards -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-8">
<!--
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition">
<div class="text-gray-500 text-sm">งบทั้งหมด</div>
<div class="text-3xl font-bold text-gray-800 mt-1">
{{ totalBudget | number:'1.0-2' }} บาท
</div>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition">
<div class="text-gray-500 text-sm">งบที่อนุมัติแล้ว</div>
<div class="text-3xl font-bold text-green-600 mt-1">
{{ approvedBudget | number:'1.0-2' }} บาท
</div>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition">
<div class="text-gray-500 text-sm">งบคงเหลือ</div>
<div
class="text-3xl font-bold mt-1"
[ngClass]="remainingBudget >= 0 ? 'text-blue-600' : 'text-red-600'"
>
{{ remainingBudget | number:'1.0-2' }} บาท
</div>
</div> -->
</div>
<!-- Add Button -->
<div class="mb-4">
<button
class=" rounded-3xl bg-green-600 hover:bg-green-700 text-white px-5 py-2.5 text-sm font-medium shadow-sm flex items-center gap-2 transition"
>
เพิ่มโครงการใหม่
</button>
</div>
<!-- Table -->
<div class="overflow-x-auto bg-white border border-gray-200 rounded-2xl shadow-sm">
<table class="min-w-full text-left border-collapse">
<thead class="bg-red-900 border-b border-gray-200 text-white text-sm">
<tr>
<th class="py-3 px-4 font-semibold">ลำดับ</th>
<th class="py-3 px-4 font-semibold">รหัสโครงการ</th>
<th class="py-3 px-4 font-semibold">ชื่อโครงการ</th>
<th class="py-3 px-4 font-semibold">ผู้รับผิดชอบ</th>
<th class="py-3 px-4 font-semibold">งบที่ขออนุมัติ</th>
<th class="py-3 px-4 font-semibold">หมวดงบ</th>
<th class="py-3 px-4 font-semibold">จำนวนที่อนุมัติ</th>
<th class="py-3 px-4 font-semibold text-center">สถานะ</th>
<th class="py-3 px-4 font-semibold text-center">ดำเนินการ</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let p of projects; let i = index"class="border-b border-gray-100 hover:bg-blue-50/20 transition">
<td class="py-4 px-4 text-gray-700">{{ i + 1 }}</td>
<td class="py-4 px-4 font-medium text-gray-700"> {{ p.code }}</td>
<td class="py-4 px-4 text-gray-800 font-semibold leading-tight">{{ p.name }}</td>
<td class="py-4 px-4 text-gray-700">{{ p.owner }}</td>
<td class="py-4 px-4 text-blue-700 font-bold whitespace-nowrap"> {{ p.budget | number:'1.0-0' }} บาท</td>
<td class="py-4 px-4 w-64">
<!-- <select class="w-full px-4 py-2.5 border border-gray-300 rounded-xl bg-white
focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-300
text-sm transition">
<option value="">ไม่เลือก</option>
@for (item of budgetCategoriesDrop.expense; track item.dtlcod) {
<option [value]="item.dtlcod">
{{ item.dtlnam }}
</option>
}
</select> -->
{{ p.bdgnam }}
</td>
<td class="py-4 px-4 w-40">
<!-- <input type="number" class="w-full px-4 py-2.5 border border-gray-300 rounded-xl bg-white focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-300 text-sm transition"/> -->
{{ p.acp }} บาท
</td>
<td class="py-4 px-4 text-center">
<span
class="px-3 py-1.5 rounded-full text-xs font-semibold inline-flex items-center gap-1
shadow-sm border"
[ngClass]="{
'bg-yellow-50 text-yellow-700 border-yellow-200': p.status === 'WAIT',
'bg-green-50 text-green-700 border-green-200': p.status === 'APPROVED',
'bg-red-50 text-red-700 border-red-200': p.status === 'REJECTED'
}"
>
<ng-container *ngIf="p.status === 'APPROVED'">อนุมัติแล้ว</ng-container>
<ng-container *ngIf="p.status === 'WAIT'">รออนุมัติ</ng-container>
<ng-container *ngIf="p.status === 'REJECTED'">ไม่อนุมัติ</ng-container>
</span>
</td>
<td class="py-4 px-4 text-center space-x-2 whitespace-nowrap">
<button
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-xl text-sm
shadow-sm font-medium transition"
(click)="openBudgetDetail(p)">
จัดสรรงบประมาณ
</button>
</td>
</tr>
</tbody>
</table>
<!-- endtable -->
</div>
<!-- Pagination
<div class="flex justify-end items-center gap-3 mt-5">
<button class="px-4 py-2 border border-gray-300 rounded-xl bg-white hover:bg-gray-100">
1
</button>
<button class="px-4 py-2 border border-gray-300 rounded-xl bg-white hover:bg-gray-100">
2
</button>
</div> -->
</div>

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