Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07074c2519 | |||
| a06337d9b5 | |||
| d5026535d0 | |||
| 60be3f7890 | |||
| 278bfe80ec | |||
| d6b171a0a7 | |||
| 9a5c174fc5 | |||
| 1b6ef14c10 | |||
| 6c52de4762 | |||
| 374e25f7ee | |||
| 9136619628 | |||
|
|
f332f8b6e2 | ||
|
|
40e682e5d8 | ||
|
|
07d49d87cf | ||
|
|
f0f3392dbb | ||
|
|
07c19bd46a | ||
|
|
185227b12e | ||
|
|
11018ae49d | ||
|
|
4db7ddaba2 | ||
|
|
b8c6512f8a | ||
|
|
012d590f40 | ||
| 8e47a11ace | |||
| 87cac22db5 | |||
|
|
214ad672f0 | ||
|
|
e4e6dfc64a | ||
|
|
7204d54538 | ||
|
|
fe78631347 | ||
|
|
97fef61d8d | ||
| 14f54eddd3 | |||
| 591d89aa61 | |||
|
|
9e4af33493 | ||
|
|
368f2ef5e6 | ||
| 4c39683348 | |||
| 291485f7b7 | |||
| 4278a54726 | |||
| 252997510e | |||
| b5213b7d44 | |||
|
|
27c04e5f28 | ||
|
|
1b8241f30c | ||
| bcaf213d24 | |||
|
|
861f3f033b | ||
|
|
149b6128a7 | ||
|
|
6669399b7e | ||
|
|
0d43286d84 | ||
|
|
43ed6c0c55 | ||
|
|
5f68d3fa51 | ||
| f68f76373c | |||
| 19a5245536 | |||
| 811d7b14b8 | |||
| b1f9c48bd8 | |||
| a34714388f | |||
|
|
809e2e16bb | ||
|
|
3ebfd37400 | ||
|
|
f0336a5ead | ||
|
|
8cedef205a | ||
|
|
45259f7b8d | ||
| 1c7729ab99 | |||
|
|
10aac6060b | ||
| 651a120e2b | |||
| 14e7223974 | |||
| ff95446e07 | |||
| 3bb798c39e | |||
|
|
9db43971ce | ||
| 1d2d593eee | |||
| 5a16152a46 | |||
| 3e0dae41f0 | |||
| ebb1f2e0d6 | |||
| a7906c6a34 | |||
| 1406d36d72 | |||
| 87571f8332 | |||
| 856b6b41a8 | |||
| 9a05e78fc5 | |||
| de73c8d68f | |||
| cd6e0595a9 | |||
| 39457af479 | |||
| 0c05d9eadf | |||
| 1847bf5d65 | |||
| 290f5cae6a | |||
| 6ef1afb7f3 | |||
| 538ee11053 | |||
| 5b3bc79199 | |||
| 15308ababa | |||
| 213fd197ef |
@@ -1,9 +1,55 @@
|
|||||||
name: Build Docker Image
|
name: Build Docker Image
|
||||||
run-name: Build Docker Image
|
run-name: Build Docker Image
|
||||||
on: [push]
|
on: [push]
|
||||||
jobs:
|
|
||||||
Preparing Dependecies:
|
jobs:
|
||||||
steps:
|
Build Docker Image:
|
||||||
- run: |
|
runs-on: ubuntu-node
|
||||||
ls
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install project dependencies
|
||||||
|
run: |
|
||||||
|
(
|
||||||
|
cd accounting-ng-nuttakit
|
||||||
|
npm install --force --legacy-peer-deps --include=dev
|
||||||
|
npm install -g @angular/cli
|
||||||
|
)
|
||||||
|
(
|
||||||
|
cd ng-ttc-frontend
|
||||||
|
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
|
||||||
|
)
|
||||||
|
(
|
||||||
|
cd ng-ttc-frontend
|
||||||
|
# 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}}"))
|
||||||
|
docker rm $(docker stop $(docker ps -a -q --filter ancestor=ttc-frontend:latest --format="{{.ID}}"))
|
||||||
|
set -e
|
||||||
|
docker image rm -f accounting-frontend:latest
|
||||||
|
docker build . -t accounting-frontend:latest
|
||||||
|
docker image rm -f ttc-frontend:latest
|
||||||
|
docker build . -f Dockerfile-TTC -t ttc-frontend:latest
|
||||||
|
|
||||||
|
Restart Docker Compose:
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: Restart compose project
|
||||||
|
run: |
|
||||||
|
echo '(cd frontend-development-kickstarter && ddd && ddd && ddud)' > /hostpipe
|
||||||
|
|
||||||
|
|
||||||
10
Dockerfile
Normal 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;"]
|
||||||
10
Dockerfile-TTC
Normal 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 ng-ttc-frontend/dist/ng-ttc-frontend/browser /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -37,12 +37,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
|
||||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@@ -86,7 +84,7 @@
|
|||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"allowedHosts": ["accounting.nuttakit.work", "localhost"]
|
"allowedHosts": ["accounting.nuttakit.work", "localhost", "meal-demand-virtual-referrals.trycloudflare.com"]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -116,12 +114,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
|
||||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.16",
|
||||||
"bootstrap": "^5.3.8",
|
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/
|
|||||||
|
|
||||||
|
|
||||||
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
||||||
|
import { LoginRegisterComponent } from './component/login-register/login-register.component';
|
||||||
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
|
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
|
||||||
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
|
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
|||||||
SidebarContentComponent,
|
SidebarContentComponent,
|
||||||
SidebarComponent,
|
SidebarComponent,
|
||||||
LicensePrivacyTermsComponent,
|
LicensePrivacyTermsComponent,
|
||||||
|
// LoginRegisterComponent,
|
||||||
// AccDateFormatPipe
|
// AccDateFormatPipe
|
||||||
// DtmtodatetimePipe,
|
// DtmtodatetimePipe,
|
||||||
// MainDashboardContentComponent,
|
// MainDashboardContentComponent,
|
||||||
|
|||||||
@@ -8,12 +8,11 @@
|
|||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--shadow: 0 10px 30px rgba(11,26,43,0.08);
|
--shadow: 0 10px 30px rgba(11,26,43,0.08);
|
||||||
--glass: rgba(255,255,255,0.6);
|
--glass: rgba(255,255,255,0.6);
|
||||||
|
--success-color: #10b981; /* Green for success/confirm */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page layout */
|
/* Page layout (unchanged) */
|
||||||
.login-widget {
|
.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;
|
min-height: 100vh;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,8 +23,7 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Card (unchanged) */
|
||||||
/* Card */
|
|
||||||
.login-widget .card{
|
.login-widget .card{
|
||||||
width: 380px;
|
width: 380px;
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 40px);
|
||||||
@@ -37,16 +35,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
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);
|
max-height: calc(100vh - 56px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal/backdrop styles */
|
/* Modal/backdrop styles (unchanged) */
|
||||||
.login-backdrop{
|
.login-backdrop{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0; /* top:0; right:0; bottom:0; left:0; */
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.38);
|
background: rgba(0,0,0,0.38);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -54,43 +50,19 @@
|
|||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-modal{ width: 480px; max-width: 480px; }
|
.login-modal{ width: 480px; max-width: 480px; }
|
||||||
|
|
||||||
.modal-card{
|
.modal-card{
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 0; /* card children control internal padding */
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slightly larger brand area inside modal */
|
/* Brand area (unchanged) */
|
||||||
.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{
|
.brand{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 18px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid #eef2f5;
|
border-bottom: 1px solid #eef2f5;
|
||||||
}
|
}
|
||||||
@@ -116,16 +88,13 @@ button.primary{
|
|||||||
|
|
||||||
/* Form area */
|
/* Form area */
|
||||||
.form{
|
.form{
|
||||||
/* keep compact spacing inside the card */
|
|
||||||
/* width: 410px; */
|
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 0 2px;
|
padding: 6px 22px 22px 22px;
|
||||||
}
|
}
|
||||||
|
/* Field label wrapper (unchanged) */
|
||||||
/* Field label wrapper */
|
|
||||||
.field{
|
.field{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -135,11 +104,11 @@ button.primary{
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
/* Inputs (class 'input-field' added to HTML) */
|
||||||
/* Inputs */
|
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="text"]{
|
input[type="text"],
|
||||||
|
.input-field { /* เพิ่ม class input-field เพื่อให้สไตล์ถูกใช้กับ input ที่กำหนด */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -163,31 +132,30 @@ input:focus{
|
|||||||
transform: translateZ(0);
|
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 row */
|
||||||
.actions{
|
.actions{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 4px;
|
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{
|
button.primary{
|
||||||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
||||||
color: #000000;
|
color: #000000; /* ⬅️ แก้ไขเป็นสีดำตามคำขอ */
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -197,6 +165,7 @@ button.primary{
|
|||||||
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
||||||
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary:hover:not(:disabled){
|
button.primary:hover:not(:disabled){
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
||||||
@@ -207,45 +176,26 @@ button.primary:active{
|
|||||||
button.primary:disabled{
|
button.primary:disabled{
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
color: #000000; /* ข้อความ disabled ก็เป็นสีดำ */
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alternative options */
|
/* Secondary Button Style (สำหรับปุ่ม 'เปิด Modal', 'ส่งอีกครั้ง') */
|
||||||
.alt-options{
|
.primary.secondary-button {
|
||||||
display: flex;
|
background: transparent;
|
||||||
align-items: center;
|
color: var(--primary);
|
||||||
gap: 12px;
|
border: 1px solid var(--primary);
|
||||||
margin-top: 6px;
|
box-shadow: none;
|
||||||
flex-wrap: wrap;
|
transition: background-color .14s ease;
|
||||||
}
|
}
|
||||||
.biometric{
|
.primary.secondary-button:hover {
|
||||||
display: inline-flex;
|
background: rgba(0, 120, 212, 0.05);
|
||||||
align-items: center;
|
transform: none;
|
||||||
gap: 10px;
|
box-shadow: none;
|
||||||
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 (unchanged) */
|
||||||
.footer{
|
.footer{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -262,13 +212,9 @@ button.primary:disabled{
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.footer a:hover{ text-decoration: underline; }
|
|
||||||
.divider{ color: #d0d6db; }
|
|
||||||
|
|
||||||
/* Focus styles for keyboard users */
|
/* Focus styles (unchanged) */
|
||||||
:focus{
|
:focus{ outline: none; }
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
:focus-visible{
|
:focus-visible{
|
||||||
outline: 3px solid rgba(0,120,212,0.12);
|
outline: 3px solid rgba(0,120,212,0.12);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -277,14 +223,13 @@ button.primary:disabled{
|
|||||||
|
|
||||||
/* Small screens */
|
/* Small screens */
|
||||||
@media (max-width:420px){
|
@media (max-width:420px){
|
||||||
|
.login-backdrop{ padding: 12px; }
|
||||||
|
.login-modal{ max-width: 100%; }
|
||||||
|
.modal-card .brand{ padding: 12px; }
|
||||||
.login-widget .card{
|
.login-widget .card{
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.brand h1{ font-size: 18px; }
|
.brand h1{ font-size: 18px; }
|
||||||
.brand .subtitle{
|
|
||||||
font-family: "Kanit";
|
|
||||||
font-weight: 1000;
|
|
||||||
font-style: normal; }
|
|
||||||
.biometric span, .primary{ font-size: 13px; }
|
.biometric span, .primary{ font-size: 13px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,120 @@
|
|||||||
<div class="login-backdrop">
|
<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 ">
|
<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="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||||
<div class="brand">
|
|
||||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
|
||||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
|
||||||
<h1 id="signin-title" class='kanit-bold'>ลืมรหัสผ่าน</h1>
|
|
||||||
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
|
|
||||||
</div>
|
|
||||||
<form [formGroup]="forgotFrm" class="form px-3 pb-3 login-mobile">
|
|
||||||
<label class="field">
|
|
||||||
<span class="label-text ">อีเมล์</span>
|
|
||||||
<input type="email" formControlName="email" class="px-2 " id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
@if (isSendOtp === true) {
|
<div class="brand">
|
||||||
<label class="field">
|
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
||||||
<span class="label-text">รหัสยืนยัน OTP</span>
|
<h1 id="signin-title" class="kanit-bold">ลืมรหัสผ่าน</h1>
|
||||||
<input type="email" formControlName="otp" autocomplete="otp" placeholder="123456" alt required/>
|
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
|
||||||
</label>
|
|
||||||
}
|
|
||||||
<!-- <div class="justify-end flex"> -->
|
|
||||||
<!-- <label class="stay-signed">
|
|
||||||
<input type="checkbox" formControlName="remember" />
|
|
||||||
<span>จดจำรหัสผ่าน</span>
|
|
||||||
</label> -->
|
|
||||||
<div class="flex flex-row gap-2 mt-4 justify-end">
|
|
||||||
<div class="flex-row hover:-translate-y-2.5 transform transition-all">
|
|
||||||
<button class="bg-[linear-gradient(180deg,var(--primary)_0%,var(--primary-600)_100%)]
|
|
||||||
text-black
|
|
||||||
border-0
|
|
||||||
px-3.5 py-2.5
|
|
||||||
rounded-md
|
|
||||||
font-semibold
|
|
||||||
cursor-pointer
|
|
||||||
text-[14px]
|
|
||||||
shadow-[0_6px_18px_var(--color-blue-500)]
|
|
||||||
transition
|
|
||||||
ease-in-out
|
|
||||||
duration-100
|
|
||||||
hover:scale-[1.02]
|
|
||||||
active:opacity-90" (click)="isModalOpen = true">
|
|
||||||
เปิด Modal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@if (isSendOtp === false) {
|
|
||||||
<div class="flex justify-end">
|
|
||||||
@if (isLoading === true) {
|
|
||||||
<button type="submit" class="primary cursor-progress!" disabled>
|
|
||||||
กำลังส่ง...
|
|
||||||
</button>
|
|
||||||
} @else {
|
|
||||||
<button type="submit" class="primary" (click)="onSubmin()">
|
|
||||||
{{ 'ยืนยันส่ง OTP รีเซ็ตรหัสผ่าน' }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} @else if(isSendOtp === true) {
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button type="button" class="primary" (click)="onSubmin()">
|
|
||||||
{{ 'ส่งอีกครั้ง' }}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="primary" (click)="onVerifySubmit()">
|
|
||||||
{{ 'รีเซ็ตรหัสผ่าน' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<!-- <button mat-raised-button color="primary" [disabled]="isLoading">
|
|
||||||
{{ isLoading ? 'กำลังส่ง...' : 'ส่งรหัส OTP' }}
|
<form [formGroup]="forgotFrm" class="form">
|
||||||
</button> -->
|
<label class="field">
|
||||||
<!-- } -->
|
<span class="label-text">อีเมล์</span>
|
||||||
<!-- </div> -->
|
<input type="email" formControlName="email" class="input-field" id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
|
||||||
</form>
|
</label>
|
||||||
@if(isModalOpen){
|
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50" [formGroup]="forgotFrm">
|
@if (isSendOtp === true) {
|
||||||
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-fit">
|
<label class="field">
|
||||||
<h2 class="text-xl font-bold mb-4">เปลี่ยนรหัสผ่าน</h2>
|
<span class="label-text">รหัสยืนยัน OTP</span>
|
||||||
<hr class="w-full h-1 bg-gray-300 rounded-sm shadow-neutral-400 md:my-1">
|
<input type="text" formControlName="otp" class="input-field" autocomplete="one-time-code" placeholder="123456" alt required/>
|
||||||
<div class="mb-4">
|
</label>
|
||||||
<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="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 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="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 ){
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ยืนยันรหัสผ่านใหม่</label>
|
<div class="bg-gray-50 px-6 py-4 sm:flex sm:flex-row-reverse sm:px-8 rounded-b-2xl">
|
||||||
<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="กรอกยืนยันรหัสผ่านใหม่">
|
<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()">
|
||||||
@if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
|
ยืนยันการเปลี่ยนแปลง
|
||||||
<span class="text-red-600 md">รหัสผ่านไม่ตรงกัน</span>
|
</button>
|
||||||
}
|
<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">
|
||||||
</div>
|
ยกเลิก
|
||||||
<!-- <hr class="w-full h-[] bg-gray-100 border-0 rounded-sm md:my-1 dark:bg-gray-700"> -->
|
</button>
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button class="bg-red-500 text-white px-4 py-2 rounded" (click)="isModalOpen = false">
|
|
||||||
ปิด
|
|
||||||
</button>
|
|
||||||
<button class="bg-green-500 text-white px-4 py-2 rounded shadow-[0_1px_18px_var(--color-green-300)] hover:-translate-y-1.5 hover:shadow-[0_6px_18px_var(--color-green-500)] transition-all duration-500 ease-in-out" (click)="onSetNewPassword()">
|
|
||||||
ยืนยัน
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
|
|
||||||
/* Page layout */
|
/* Page layout */
|
||||||
.login-widget {
|
.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;
|
min-height: 100vh;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -37,8 +35,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
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);
|
max-height: calc(100vh - 56px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@@ -46,7 +42,7 @@
|
|||||||
/* Modal/backdrop styles */
|
/* Modal/backdrop styles */
|
||||||
.login-backdrop{
|
.login-backdrop{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0; /* top:0; right:0; bottom:0; left:0; */
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.38);
|
background: rgba(0,0,0,0.38);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -64,33 +60,10 @@
|
|||||||
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
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 */
|
||||||
.brand{
|
.brand{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 18px; /* Use padding from modal-card .brand */
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid #eef2f5;
|
border-bottom: 1px solid #eef2f5;
|
||||||
}
|
}
|
||||||
@@ -116,12 +89,11 @@ button.primary{
|
|||||||
|
|
||||||
/* Form area */
|
/* Form area */
|
||||||
.form{
|
.form{
|
||||||
/* keep compact spacing inside the card */
|
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 0 2px;
|
padding: 6px 22px 2px 22px; /* Adjusted padding to match card padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Field label wrapper */
|
/* Field label wrapper */
|
||||||
@@ -185,8 +157,9 @@ input:focus{
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
button.primary{
|
button.primary{
|
||||||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
/* ⭐️ แก้ไขตรงนี้: เปลี่ยนสีข้อความเป็นสีดำตามคำขอ */
|
||||||
color: #000000;
|
color: #000000;
|
||||||
|
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -196,6 +169,7 @@ button.primary{
|
|||||||
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
||||||
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary:hover:not(:disabled){
|
button.primary:hover:not(:disabled){
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
||||||
@@ -206,6 +180,7 @@ button.primary:active{
|
|||||||
button.primary:disabled{
|
button.primary:disabled{
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
color: #000000; /* ข้อความ Disabled ก็ยังเป็นสีดำ */
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +205,7 @@ button.primary:disabled{
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.biometric svg{ display: block; opacity: .95; }
|
.biometric svg{ display: block; opacity: .95; }
|
||||||
.biometric:hover{
|
.biometric:hover{
|
||||||
background: rgba(0,120,212,0.04);
|
background: rgba(0,120,212,0.04);
|
||||||
@@ -276,14 +252,14 @@ button.primary:disabled{
|
|||||||
|
|
||||||
/* Small screens */
|
/* Small screens */
|
||||||
@media (max-width:420px){
|
@media (max-width:420px){
|
||||||
|
.login-backdrop{ padding: 12px; }
|
||||||
|
.login-modal{ max-width: 100%; }
|
||||||
|
.modal-card .brand{ padding: 12px; }
|
||||||
|
|
||||||
.login-widget .card{
|
.login-widget .card{
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.brand h1{ font-size: 18px; }
|
.brand h1{ font-size: 18px; }
|
||||||
.brand .subtitle{
|
|
||||||
font-family: "Kanit";
|
|
||||||
font-weight: 1000;
|
|
||||||
font-style: normal; }
|
|
||||||
.biometric span, .primary{ font-size: 13px; }
|
.biometric span, .primary{ font-size: 13px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
<!-- Modal-like backdrop that covers the viewport -->
|
<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-backdrop">
|
<div class="login-modal d-flex align-items-center justify-content-center bg-white rounded-2xl">
|
||||||
<div class="login-modal d-flex align-items-center justify-content-center">
|
|
||||||
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||||
<div class="brand">
|
|
||||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
|
||||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
|
||||||
<h1 id="signin-title" class='kanit-bold'>เข้าสู่ระบบ</h1>
|
|
||||||
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของคุณ</p>
|
|
||||||
</div>
|
|
||||||
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form px-3 pb-3">
|
|
||||||
<label class="field">
|
|
||||||
<span class="label-text">อีเมล์</span>
|
|
||||||
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<div class="brand">
|
||||||
<span class="label-text">รหัสผ่าน</span>
|
<img src="/logo.png" alt="Company logo" class="logo"/>
|
||||||
<input type="password" formControlName="password" autocomplete="current-password" required />
|
<h1 id="signin-title" class="kanit-bold">เข้าสู่ระบบ</h1>
|
||||||
</label>
|
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของท่าน</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form">
|
||||||
<label class="stay-signed">
|
|
||||||
<input type="checkbox" formControlName="remember" />
|
<label class="field">
|
||||||
<span>จดจำรหัสผ่าน</span>
|
<span class="label-text">อีเมล์</span>
|
||||||
|
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required class="input-field" />
|
||||||
</label>
|
</label>
|
||||||
<!-- <fa-icon [icon]="faCoffee" /> -->
|
|
||||||
<button type="submit" class="primary" [disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
|
|
||||||
เข้าสู่ระบบ
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
<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="alt-options">
|
<div class="actions d-flex justify-content-between align-items-center mt-4">
|
||||||
<button type="button" class="biometric" (click)="useBiometric()">
|
<label class="stay-signed">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
<input type="checkbox" formControlName="remember" />
|
||||||
<path d="M12 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" fill="currentColor" opacity=".9"/>
|
<span>จดจำรหัสผ่าน</span>
|
||||||
<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"/>
|
</label>
|
||||||
<path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
<button type="submit" class="primary login-button"
|
||||||
</svg>
|
[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>
|
<span>เข้าสู่ระบบด้วย Windows Hello / Touch ID</span>
|
||||||
</button>
|
</button>
|
||||||
|
<a class="help-link mt-2" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="help-link" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
|
<div class="footer mt-5 text-center">
|
||||||
</div>
|
<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 class="footer">
|
|
||||||
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
|
|
||||||
<span class="divider">•</span>
|
|
||||||
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export class LoginPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createAccount(): void {
|
createAccount(): void {
|
||||||
|
this.router.navigate(['/login/register']);
|
||||||
this.message = 'Create account flow not implemented.';
|
this.message = 'Create account flow not implemented.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -281,6 +281,23 @@
|
|||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
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 input:focus,
|
||||||
.quick-log__form textarea:focus {
|
.quick-log__form textarea:focus {
|
||||||
border-color: #0ea5e9;
|
border-color: #0ea5e9;
|
||||||
|
|||||||
@@ -13,7 +13,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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">
|
<article class="period-card" *ngFor="let summary of periodSummaries">
|
||||||
<header class="period-card__header">
|
<header class="period-card__header">
|
||||||
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
|
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
|
||||||
@@ -39,9 +68,9 @@
|
|||||||
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
|
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section> -->
|
||||||
|
|
||||||
<section class="dashboard__stats">
|
<!-- <section class="dashboard__stats">
|
||||||
<article class="stat-card" *ngFor="let card of kpiCards">
|
<article class="stat-card" *ngFor="let card of kpiCards">
|
||||||
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
|
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
|
||||||
<div class="stat-card__body">
|
<div class="stat-card__body">
|
||||||
@@ -50,65 +79,131 @@
|
|||||||
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
|
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section> -->
|
||||||
|
|
||||||
<section class="ledger-grid">
|
<div class="ledger-grid" [formGroup]="saveFrm">
|
||||||
<article class="panel quick-log">
|
<form class="panel quick-log" [formGroup]="saveFrm">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>บันทึกรายการแบบรวดเร็ว</h2>
|
<h2>บันทึกรายการแบบรวดเร็ว</h2>
|
||||||
<p>จดรายรับรายจ่ายภายในไม่กี่คลิก</p>
|
<p>จดรายรับรายจ่ายภายในไม่กี่คลิก</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- เปลี่ยน 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'; 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>
|
</div>
|
||||||
<form class="quick-log__form">
|
</label>
|
||||||
<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"/>
|
<!-- 2. ส่วนวันที่ -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label>
|
||||||
|
<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>
|
</label>
|
||||||
<div class="quick-log__grid">
|
|
||||||
<label>
|
<!-- Validate วันที่ -->
|
||||||
<span>หมวดหมู่</span>
|
@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'){
|
@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">
|
<select formControlName="actcat"
|
||||||
<option value="">ไม่เลือก</option>
|
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) {
|
@for (item of myDropAct.income; track item.dtlcod) {
|
||||||
<option [value]="item.dtlcod">
|
<option [value]="item.dtlcod">{{ item.dtlnam }}</option>
|
||||||
{{ item.dtlnam }}
|
|
||||||
</option>
|
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
}@else if(mode == 'e'){
|
}@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">
|
<select formControlName="actcat"
|
||||||
<option value="">ไม่เลือก</option>
|
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) {
|
@for (item of myDropAct.expense; track item.dtlcod) {
|
||||||
<option [value]="item.dtlcod">
|
<option [value]="item.dtlcod">{{ item.dtlnam }}</option>
|
||||||
{{ item.dtlnam }}
|
|
||||||
</option>
|
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
</label>
|
</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>
|
<label>
|
||||||
<span>ยอดเงิน (฿)</span>
|
<span>ยอดเงิน (฿) <span class="text-red-500">*</span></span>
|
||||||
<input type="number" placeholder="0.00" />
|
<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>
|
</label>
|
||||||
</div>
|
|
||||||
|
<!-- 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>
|
<label>
|
||||||
<span>บันทึกเพิ่มเติม</span>
|
<span>บันทึกเพิ่มเติม</span>
|
||||||
<textarea rows="3" placeholder="รายละเอียดการรับ/จ่าย"></textarea>
|
<textarea rows="3" formControlName="actcmt" placeholder="รายละเอียดการรับ/จ่าย"
|
||||||
|
[class.border-red-500]="saveFrm.get('actcmt')?.invalid"></textarea>
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="btn btn--primary">บันทึกรายการ</button>
|
|
||||||
</form>
|
<!-- Validate ความยาวตัวอักษร (ถ้ามี) -->
|
||||||
</article>
|
@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">
|
<article class="panel ledger-panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
@@ -164,7 +259,7 @@
|
|||||||
<span class="ledger-category">{{ idx.actcatnam }}</span>
|
<span class="ledger-category">{{ idx.actcatnam }}</span>
|
||||||
|
|
||||||
<span class="ledger-amount" [ngClass]="idx.acttyp === 'i' ? 'is-credit' : 'is-debit'">
|
<span class="ledger-amount" [ngClass]="idx.acttyp === 'i' ? 'is-credit' : 'is-debit'">
|
||||||
{{ idx.actqty }}
|
{{ idx.actqty | number:'1.0-2' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="ledger-note">{{ idx.actcmt }}</span>
|
<span class="ledger-note">{{ idx.actcmt }}</span>
|
||||||
@@ -172,7 +267,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<section class="dashboard__grid">
|
<section class="dashboard__grid">
|
||||||
<!-- <article class="panel panel--main">
|
<!-- <article class="panel panel--main">
|
||||||
@@ -212,14 +307,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="pie-legend__label">{{ idx.label }}</p>
|
<p class="pie-legend__label">{{ idx.label }}</p>
|
||||||
<p class="pie-legend__value">{{ idx.percent }}%</p>
|
<p class="pie-legend__value">{{ idx.percent }}%</p>
|
||||||
<p class="pie-legend__value">{{ idx.value }} บาท</p>
|
<p class="pie-legend__value">{{ idx.value | number:'1.0-2'}} บาท</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<!-- ตัวเลขซ้อนทับกัน -->
|
<!-- ตัวเลขซ้อนทับกัน -->
|
||||||
<article class="panel panel--side">
|
<!-- <article class="panel panel--side">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>สรุปสภาพคล่อง</h2>
|
<h2>สรุปสภาพคล่อง</h2>
|
||||||
@@ -236,9 +331,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>การแจ้งเตือนสำคัญ</h2>
|
<h2>การแจ้งเตือนสำคัญ</h2>
|
||||||
@@ -252,9 +377,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="alert__tag">{{ alert.tag }}</span>
|
<span class="alert__tag">{{ alert.tag }}</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article> -->
|
||||||
|
|
||||||
<article class="panel tasks-panel">
|
<!-- <article class="panel tasks-panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>รายการยอดค้างจ่าย</h2>
|
<h2>รายการยอดค้างจ่าย</h2>
|
||||||
@@ -271,7 +396,7 @@
|
|||||||
<span class="task__badge">{{ task.priority }}</span>
|
<span class="task__badge">{{ task.priority }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article> -->
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
import { GeneralService } from '../../services/generalservice';
|
import { GeneralService } from '../../services/generalservice';
|
||||||
import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData } from '../../interfaces/dashboard.interface'
|
import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData, QuickRatio} from '../../interfaces/dashboard.interface'
|
||||||
import { DashboardStateService } from '../../services/state/dashboard-state.service';
|
import { DashboardStateService } from '../../services/state/dashboard-state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,20 +11,21 @@ import { DashboardStateService } from '../../services/state/dashboard-state.serv
|
|||||||
styleUrl: './main-dashboard.component.css'
|
styleUrl: './main-dashboard.component.css'
|
||||||
})
|
})
|
||||||
export class MainDashboardComponent implements OnInit {
|
export class MainDashboardComponent implements OnInit {
|
||||||
|
@Output() saveEventSubmit = new EventEmitter<any>();
|
||||||
mode: string = 'i';
|
mode: string = 'i';
|
||||||
isModalOpen: boolean = false;
|
isModalOpen: boolean = false;
|
||||||
isSubmitting: boolean = false;
|
isSubmitting: boolean = false;
|
||||||
arrearsForm!: FormGroup;
|
arrearsForm!: FormGroup;
|
||||||
saveFrm!: FormGroup;
|
saveFrm!: FormGroup;
|
||||||
myActData: IActData[] = [];
|
myActData: IActData[] = [];
|
||||||
|
quickRatios: QuickRatio[] = [];
|
||||||
// myDropAct: IStateDrop[] = [];
|
// myDropAct: IStateDrop[] = [];
|
||||||
myDropAct: IStateDrop = { income: [], expense: [] };
|
myDropAct: IStateDrop = { income: [], expense: [] };
|
||||||
myActSumData: IActSumData = {
|
myActSumData: IActSumData = {
|
||||||
summary: {
|
summary: {
|
||||||
totalIncome: '',
|
totalIncome: '',
|
||||||
totalExpense: '',
|
totalExpense: '',
|
||||||
netProfit: '',
|
netProfit: 0,
|
||||||
profitRate: '',
|
profitRate: '',
|
||||||
adjustedProfitRate: '',
|
adjustedProfitRate: '',
|
||||||
period: ''
|
period: ''
|
||||||
@@ -34,46 +35,16 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
expense: []
|
expense: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ActSumDataGradient: any
|
ActSumDataGradient: any
|
||||||
|
|
||||||
|
|
||||||
readonly ownerName = 'Nuttakit';
|
ownerName = localStorage.getItem('username') || 'ชนกนันต์';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dashboardStateService: DashboardStateService
|
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 = [
|
// readonly revenueTrend = [
|
||||||
// { label: 'ม.ค.', value: 52 },
|
// { label: 'ม.ค.', value: 52 },
|
||||||
// { label: 'ก.พ.', value: 61 },
|
// { label: 'ก.พ.', value: 61 },
|
||||||
@@ -83,77 +54,21 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
// { label: 'มิ.ย.', value: 77 }
|
// { label: 'มิ.ย.', value: 77 }
|
||||||
// ];
|
// ];
|
||||||
|
|
||||||
readonly quickRatios = [
|
// readonly quickRatios = [
|
||||||
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
|
// { label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
|
||||||
{ label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
|
// { label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
|
||||||
{ label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
|
// { label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
|
||||||
];
|
// ];
|
||||||
|
// ฟังก์ชันนี้ควรเรียกหลังจากได้รับข้อมูล myActSumData แล้ว (เช่นใน subscribe หรือ ngOnChanges)
|
||||||
|
|
||||||
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 = [
|
// เพิ่มใน Class Component
|
||||||
{
|
isNumber(val: any): boolean {
|
||||||
title: 'กระทบยอดธนาคาร เดือน มิ.ย.',
|
return typeof val === 'number';
|
||||||
due: 'วันนี้ 16:00',
|
}
|
||||||
priority: 'สูง'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'เตรียมรายงาน VAT',
|
|
||||||
due: 'พรุ่งนี้ 10:30',
|
|
||||||
priority: 'กลาง'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ออกใบเสนอราคา โครงการใหม่',
|
|
||||||
due: 'ศุกร์ 14:00',
|
|
||||||
priority: 'ต่ำ'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// readonly ledgerEntries = [
|
// readonly ledgerEntries = [
|
||||||
// {
|
// {
|
||||||
@@ -190,14 +105,6 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
// }
|
// }
|
||||||
// ];
|
// ];
|
||||||
|
|
||||||
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 {
|
ngOnInit(): void {
|
||||||
this.setupFormControl();
|
this.setupFormControl();
|
||||||
@@ -217,8 +124,10 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
if (data) {
|
if (data) {
|
||||||
this.myActSumData = data;
|
this.myActSumData = data;
|
||||||
this.ActSumDataGradient = this.buildExpenseGradient()
|
this.ActSumDataGradient = this.buildExpenseGradient()
|
||||||
|
this.updateQuickRatios();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
setupFormControl(){
|
setupFormControl(){
|
||||||
this.arrearsForm = new FormGroup({
|
this.arrearsForm = new FormGroup({
|
||||||
@@ -230,15 +139,66 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.saveFrm = new FormGroup({
|
this.saveFrm = new FormGroup({
|
||||||
actacpdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
|
actacpdtm: new FormControl('',[Validators.required]),
|
||||||
actqty: new FormControl('',[Validators.required]),
|
actqty: new FormControl('',[Validators.required]),
|
||||||
actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]),
|
actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]),
|
||||||
actcmt: new FormControl('',[Validators.maxLength(200)])
|
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(){
|
onArrearsSubmit(){
|
||||||
|
|||||||
@@ -209,80 +209,127 @@
|
|||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- UPDATED PIE CHART SECTION (Donut Style) --- */
|
||||||
|
|
||||||
.pie-panel__content {
|
.pie-panel__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.5rem;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3rem; /* ระยะห่างระหว่างกราฟกับ Legend */
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
/* Flex เพื่อจัดรูตรงกลาง */
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
.pie-chart__center {
|
||||||
position: absolute;
|
width: 75%; /* ความหนาของขอบ */
|
||||||
top: 50%;
|
height: 75%;
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 110px;
|
|
||||||
height: 110px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
|
box-shadow: inset 0 2px 10px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pie-chart__center p {
|
.chart-shadow {
|
||||||
|
position: absolute;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0,0,0,0.03);
|
||||||
|
filter: blur(20px);
|
||||||
|
z-index: 1;
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-muted {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #94a3b8;
|
font-size: 0.8rem;
|
||||||
font-size: 0.85rem;
|
color: #9ca3af;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pie-chart__center strong {
|
.total-amount {
|
||||||
color: #0f172a;
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Legend Styles */
|
||||||
.pie-legend {
|
.pie-legend {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.8rem;
|
gap: 1.5rem; /* ระยะห่างแต่ละ item */
|
||||||
|
min-width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pie-legend li {
|
.pie-legend__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 0.6rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
width: 14px;
|
width: 12px;
|
||||||
height: 14px;
|
height: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
margin-top: 6px; /* ดันลงมาให้ตรงกับ Text บรรทัดแรก */
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-label {
|
.legend-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-percent {
|
||||||
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-value {
|
.item-value {
|
||||||
margin: 0;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- END UPDATED PIE CHART SECTION --- */
|
||||||
|
|
||||||
|
|
||||||
.preview-modal {
|
.preview-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -422,6 +469,7 @@
|
|||||||
|
|
||||||
.pie-panel__content {
|
.pie-panel__content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,59 @@
|
|||||||
<section class="report">
|
<div class="report">
|
||||||
<header class="report__header">
|
<div class="report__header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">สรุปรายงาน</p>
|
<p class="eyebrow">สรุปรายงาน</p>
|
||||||
<h1>รายงานรายรับรายจ่าย</h1>
|
<h1>รายงานรายรับรายจ่าย</h1>
|
||||||
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
|
<!-- <p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="report__actions">
|
<div class="report__actions">
|
||||||
<button class="btn btn--ghost">ส่งออกเป็น Excel</button>
|
<!-- <button class="btn btn--ghost">ส่งออกเป็น Excel</button> -->
|
||||||
<button class="btn btn--primary" (click)="openPreview()">ปริ้นรายงาน</button>
|
<button class="btn btn--primary" (click)="openPreview()">ปริ้นรายงาน</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<section class="summary-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
<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">
|
<div class="p-6 rounded-2xl bg-green-50 border border-green-100 shadow-sm hover:shadow-md transition duration-200">
|
||||||
<article class="panel">
|
<p class="text-sm font-medium text-green-800 opacity-70">รายรับรวม</p>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{ myActSumData.summary.totalIncome | number:'1.2-2' }}</h2>
|
||||||
|
<div class="mt-2 flex items-center text-sm font-medium text-green-600">
|
||||||
|
<!-- <span>+12.4% MoM</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 rounded-2xl bg-yellow-50 border border-yellow-100 shadow-sm hover:shadow-md transition duration-200">
|
||||||
|
<p class="text-sm font-medium text-yellow-800 opacity-70">รายจ่ายรวม</p>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{ myActSumData.summary.totalExpense | number:'1.2-2' }}</h2>
|
||||||
|
<div class="mt-2 flex items-center text-sm font-medium text-yellow-600">
|
||||||
|
<!-- <span>-4.1% MoM</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 rounded-2xl bg-purple-50 border border-purple-100 shadow-sm hover:shadow-md transition duration-200">
|
||||||
|
<p class="text-sm font-medium text-purple-800 opacity-70">กำไรสุทธิ</p>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{ myActSumData.summary.netProfit | number:'1.2-2' }}</h2>
|
||||||
|
<div class="mt-2 flex items-center text-sm font-medium text-purple-600">
|
||||||
|
<span>Margin {{ myActSumData.summary.profitRate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 rounded-2xl bg-gray-50 border border-gray-100 shadow-sm hover:shadow-md transition duration-200">
|
||||||
|
<p class="text-sm font-medium text-gray-600 opacity-70">บันทึกรายการ</p>
|
||||||
|
<h2 class="text-3xl font-bold text-gray-800 mt-2">{{myActData.length}} รายการ</h2>
|
||||||
|
<div class="mt-2 flex items-center text-xs font-medium text-gray-500">
|
||||||
|
<!-- <span>32 รายรับ · 54 รายจ่าย</span> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="report__content">
|
||||||
|
<div class="panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>สมุดรายวัน</h2>
|
<h2>สมุดรายวัน</h2>
|
||||||
<p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p>
|
<p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--compact btn--ghost">กรองข้อมูล</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table__head">
|
<div class="table__head">
|
||||||
@@ -37,106 +63,123 @@
|
|||||||
<span>หมวดหมู่</span>
|
<span>หมวดหมู่</span>
|
||||||
<span class="amount-col">ยอดเงิน</span>
|
<span class="amount-col">ยอดเงิน</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table__row" *ngFor="let record of formattedRecords">
|
@for (idx of myActData; track idx.actseq) {
|
||||||
<span>{{ record.date }}</span>
|
<div class="table__row">
|
||||||
<span class="mono">{{ record.doc }}</span>
|
<span>{{ idx.actacpdtm | date:'dd/MM/yyyy' }}</span>
|
||||||
|
<span class="mono">RCPT{{ idx.actseq }}</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ record.topic }}</strong>
|
<strong>{{ idx.actcatnam }}</strong>
|
||||||
<small class="muted">{{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }}</small>
|
<small class="muted">{{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}</small>
|
||||||
</span>
|
</span>
|
||||||
<span>{{ record.category }}</span>
|
<span>{{ idx.actcatnam }}</span>
|
||||||
<span class="amount-col" [ngClass]="record.tone">{{ record.displayAmount }}</span>
|
<span class="amount-col" [ngClass]="idx.acttyp === 'i' ? 'income' : 'expense' ">{{ idx.actqty | number:'1.2-2' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<div class="p-4 text-center text-gray-400">ไม่มีข้อมูลรายการ</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<article class="panel pie-panel">
|
<div class="panel pie-panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>สัดส่วนค่าใช้จ่าย</h2>
|
<h2>สัดส่วนค่าใช้จ่าย</h2>
|
||||||
<p>เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
<div class="pie-panel__content">
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div class="pie-chart" [style.background]="ActSumDataGradient">
|
||||||
|
<div class="pie-chart__center">
|
||||||
|
<p class="label-muted">รวมรายจ่าย</p>
|
||||||
|
<strong class="total-amount">{{ myActSumData.summary.totalExpense | number:'1.0-0' }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-shadow"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="pie-legend">
|
||||||
|
@for (part of myActSumData.pie.expense; track part.label) {
|
||||||
|
<li class="pie-legend__item">
|
||||||
|
<span class="swatch" [style.background]="part.color"></span>
|
||||||
|
<div class="legend-text">
|
||||||
|
<p class="item-label">{{ part.label }}</p>
|
||||||
|
<p class="item-percent">{{ part.percent }}%</p>
|
||||||
|
<p class="item-value">{{ part.value | number:'1.0-0' }} บาท</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PRINT PREVIEW MODAL -->
|
||||||
|
@if(printPreviewOpen){
|
||||||
|
<div class="preview-modal">
|
||||||
|
<div class="preview-modal__backdrop" (click)="closePreview()"></div>
|
||||||
|
<div class="preview-modal__content">
|
||||||
|
<div class="preview-modal__header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Print Preview</p>
|
||||||
|
<h2>รายงานรายรับรายจ่าย</h2>
|
||||||
|
</div>
|
||||||
|
<div class="preview-modal__actions">
|
||||||
|
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
|
||||||
|
<!-- ACTION ADDED HERE -->
|
||||||
|
<button class="btn btn--primary" (click)="printReport()">พิมพ์</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-sheet">
|
||||||
|
<div class="preview-sheet__header">
|
||||||
|
<div>
|
||||||
|
<h3>Accounting Summary</h3>
|
||||||
|
<p>Prepared on {{ reportRange.end }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-pie">
|
||||||
|
<div class="mini-pie" [style.background]="ActSumDataGradient"></div>
|
||||||
|
<ul>
|
||||||
|
@for (part of myActSumData.pie.expense; track part.label) {
|
||||||
|
<li>
|
||||||
|
<span class="swatch" [style.background]="part.color"></span>
|
||||||
|
<span>{{ part.label }} · {{ part.percent }}%</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="preview-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>วันที่</th>
|
||||||
|
<th>เลขที่</th>
|
||||||
|
<th>หัวข้อ</th>
|
||||||
|
<th>หมวดหมู่</th>
|
||||||
|
<th>ยอดเงิน</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (idx of myActData; track idx.actseq) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ idx.actacpdtm | date:'dd/MM/yyyy'}}</td>
|
||||||
|
<td>RCPT{{ idx.actseq }}</td>
|
||||||
|
<td>{{ idx.actcatnam }}</td>
|
||||||
|
<td>{{ idx.actcatnam }}</td>
|
||||||
|
<td [ngClass]="idx.acttyp === 'i' ? 'income' : 'expense' ">{{ idx.actqty | number:'1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- The rest of the dashboard HTML is excluded for brevity/focus on the core report logic -->
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Component } from '@angular/core';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { GeneralService } from './../../services/generalservice';
|
||||||
|
import { QuickRatio, IStateDrop, IActSumData } from './../../interfaces/dashboard.interface';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { IActData } from '../../interfaces/dashboard.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-report',
|
selector: 'app-main-report',
|
||||||
@@ -6,7 +10,36 @@ import { Component } from '@angular/core';
|
|||||||
standalone: false,
|
standalone: false,
|
||||||
styleUrls: ['./main-report.component.css']
|
styleUrls: ['./main-report.component.css']
|
||||||
})
|
})
|
||||||
export class MainReportComponent {
|
export class MainReportComponent implements OnInit{
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private generalService: GeneralService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
){}
|
||||||
readonly reportRange = {
|
readonly reportRange = {
|
||||||
start: '1 มิถุนายน 2567',
|
start: '1 มิถุนายน 2567',
|
||||||
end: '30 มิถุนายน 2567'
|
end: '30 มิถุนายน 2567'
|
||||||
@@ -102,6 +135,60 @@ export class MainReportComponent {
|
|||||||
|
|
||||||
printPreviewOpen = false;
|
printPreviewOpen = false;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.OnSearchSum({}, false);
|
||||||
|
this.OnSearchAct({});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OnSearchAct(value: any): void {
|
||||||
|
const uri = '/api/web/accountingSearch';
|
||||||
|
let request = {
|
||||||
|
token: value
|
||||||
|
}
|
||||||
|
this.generalService.postRequest(uri, request).subscribe({
|
||||||
|
next: (result: any) => {
|
||||||
|
if (result.code === '200') {
|
||||||
|
this.generalService.trowApi(result);
|
||||||
|
this.myActData = result.data;
|
||||||
|
}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.ActSumDataGradient = this.buildExpenseGradient();
|
||||||
|
}else{
|
||||||
|
this.generalService.trowApi(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
this.generalService.trowApi(error);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get expenseGradient(): string {
|
get expenseGradient(): string {
|
||||||
let current = 0;
|
let current = 0;
|
||||||
const segments = this.expenseBreakdown
|
const segments = this.expenseBreakdown
|
||||||
@@ -115,6 +202,24 @@ export class MainReportComponent {
|
|||||||
return `conic-gradient(${segments})`;
|
return `conic-gradient(${segments})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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})`;
|
||||||
|
}
|
||||||
|
|
||||||
get formattedRecords() {
|
get formattedRecords() {
|
||||||
return this.ledgerRecords.map(record => ({
|
return this.ledgerRecords.map(record => ({
|
||||||
...record,
|
...record,
|
||||||
@@ -123,6 +228,10 @@ export class MainReportComponent {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printReport(): void {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
openPreview(): void {
|
openPreview(): void {
|
||||||
this.printPreviewOpen = true;
|
this.printPreviewOpen = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
/* .sidebar {
|
.sidebar-container {
|
||||||
width: 220px;
|
width: 250px;
|
||||||
background: #222;
|
transition: width 0.3s ease-in-out;
|
||||||
color: white;
|
flex-shrink: 0; /* Prevent the sidebar from shrinking in the flex container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container.is-collapsed {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container.is-mobile {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 20px;
|
width: 250px; /* Width when shown on mobile */
|
||||||
} */
|
z-index: 40;
|
||||||
/* .sidebar ul {
|
transform: translateX(-100%);
|
||||||
list-style: none;
|
transition: transform 0.3s ease-in-out;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
.sidebar li {
|
|
||||||
margin: 10px 0;
|
.sidebar-container.is-mobile.is-open {
|
||||||
cursor: pointer;
|
transform: translateX(0);
|
||||||
transition: 0.2s;
|
|
||||||
}
|
}
|
||||||
.sidebar li:hover {
|
|
||||||
color: #00bcd4;
|
|
||||||
} */
|
|
||||||
|
|
||||||
@keyframes spin-slow {
|
@keyframes spin-slow {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
@@ -25,15 +30,3 @@
|
|||||||
.animate-spin-slow {
|
.animate-spin-slow {
|
||||||
animation: spin-slow 8s linear infinite;
|
animation: spin-slow 8s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.sidebar {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 50;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.expanded {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
<div
|
<div
|
||||||
class="h-screen bg-linear-to-b from-amber-950 to-amber-900 text-gray-100 shadow-2xl flex flex-col relative transition-all duration-300 ease-in-out"
|
[class.is-mobile]="isMobile"
|
||||||
[@sidebarState]="isOpen ? 'expanded' : 'collapsed'">
|
[class.is-open]="isOpen"
|
||||||
|
[class.is-collapsed]="!isOpen && !isMobile"
|
||||||
|
class="sidebar-container h-full bg-linear-to-b from-amber-950 to-amber-900 text-gray-100 shadow-2xl flex flex-col relative">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
(click)="toggleSidebar()"
|
(click)="toggleSidebar()"
|
||||||
|
[class.hidden]="isMobile"
|
||||||
class="absolute -right-3 top-6 bg-amber-700 hover:bg-amber-600 text-white rounded-full p-2 shadow-md transition-all duration-300">
|
class="absolute -right-3 top-6 bg-amber-700 hover:bg-amber-600 text-white rounded-full p-2 shadow-md transition-all duration-300">
|
||||||
<i class="fas" [ngClass]="isOpen ? 'fa-angle-left' : 'fa-angle-right'"></i>
|
<i class="fas" [ngClass]="isOpen ? 'fa-angle-left' : 'fa-angle-right'"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex items-center gap-3 p-5">
|
<div class="flex items-center gap-3 p-5">
|
||||||
<h3 *ngIf="isOpen" class="text-2xl font-bold transition-all duration-300">
|
<h3 *ngIf="isOpen" class="text-2xl font-bold transition-all duration-300">
|
||||||
Global Sidebar
|
<img src="logo.png" alt="">
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-t border-amber-700 mx-4 my-4 opacity-70">
|
<hr class="border-t border-amber-700 mx-4 my-4 opacity-70">
|
||||||
|
|
||||||
<ul class="flex flex-col gap-2 px-2 grow">
|
<ul class="flex flex-col gap-2 px-2 flex-1 overflow-y-auto">
|
||||||
|
|
||||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
<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"
|
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||||
@@ -24,13 +27,13 @@
|
|||||||
<i class="fas fa-tachometer-alt text-xl group-hover:scale-110 transition-transform"></i>
|
<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>
|
<span *ngIf="isOpen" class="text-lg font-medium">Dashboard</span>
|
||||||
</li>
|
</li>
|
||||||
|
<!--
|
||||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
<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"
|
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||||
(click)="navigate('/main/profile')">
|
(click)="navigate('/main/profile')">
|
||||||
<i class="fas fa-user-circle text-xl group-hover:scale-110 transition-transform"></i>
|
<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>
|
<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
|
<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"
|
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||||
@@ -38,28 +41,16 @@
|
|||||||
<i class="fas fa-chart-bar text-xl group-hover:scale-110 transition-transform"></i>
|
<i class="fas fa-chart-bar text-xl group-hover:scale-110 transition-transform"></i>
|
||||||
<span *ngIf="isOpen" class="text-lg font-medium">Report</span>
|
<span *ngIf="isOpen" class="text-lg font-medium">Report</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer mt-auto
|
|
||||||
hover:bg-red-700 hover:shadow-lg transition-all duration-300 ease-in-out"
|
|
||||||
(click)="logout()">
|
|
||||||
<i class="fas fa-sign-out-alt text-xl group-hover:scale-110 transition-transform"></i>
|
|
||||||
<span *ngIf="isOpen" class="text-lg font-medium text-red-200">Logout</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto p-2">
|
||||||
|
<ul class="flex flex-col">
|
||||||
|
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||||
|
hover:bg-red-700 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||||
|
(click)="logout()">
|
||||||
|
<i class="fas fa-sign-out-alt text-xl group-hover:scale-110 transition-transform"></i>
|
||||||
|
<span *ngIf="isOpen" class="text-lg font-medium text-red-200">Logout</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
*ngIf="isMobile && showOverlay"
|
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300"
|
|
||||||
(click)="toggleSidebar()">
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
|
|
||||||
<div class="flex-1 bg-gray-100 text-gray-900 overflow-y-auto">
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,15 @@
|
|||||||
import { Component, HostListener, OnInit } from '@angular/core';
|
import { Component, HostListener, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sidebar',
|
selector: 'app-sidebar',
|
||||||
standalone: false,
|
standalone: false,
|
||||||
templateUrl: './sidebar.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrls: ['./sidebar.component.css'],
|
styleUrls: ['./sidebar.component.css']
|
||||||
animations: [
|
|
||||||
trigger('sidebarState', [
|
|
||||||
state('expanded', style({
|
|
||||||
width: '220px',
|
|
||||||
opacity: 1
|
|
||||||
})),
|
|
||||||
state('collapsed', style({
|
|
||||||
width: '70px',
|
|
||||||
opacity: 0.95
|
|
||||||
})),
|
|
||||||
transition('expanded <=> collapsed', [
|
|
||||||
animate('300ms ease-in-out')
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class SidebarComponent implements OnInit {
|
export class SidebarComponent implements OnInit {
|
||||||
isOpen = true; // ขยายไหม
|
isOpen = true;
|
||||||
isMobile = false; // ตรวจอุปกรณ์
|
isMobile = false;
|
||||||
showOverlay = false; // สำหรับ mobile overlay
|
|
||||||
|
|
||||||
constructor(private router: Router) {}
|
constructor(private router: Router) {}
|
||||||
|
|
||||||
@@ -39,24 +22,21 @@ export class SidebarComponent implements OnInit {
|
|||||||
checkDevice() {
|
checkDevice() {
|
||||||
this.isMobile = window.innerWidth <= 768;
|
this.isMobile = window.innerWidth <= 768;
|
||||||
if (this.isMobile) {
|
if (this.isMobile) {
|
||||||
this.isOpen = false; // ซ่อน sidebar ตอนเข้า mobile
|
this.isOpen = false;
|
||||||
} else {
|
} else {
|
||||||
this.isOpen = true; // เปิดไว้ตลอดใน desktop
|
this.isOpen = true;
|
||||||
this.showOverlay = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
if (this.isMobile) {
|
this.isOpen = !this.isOpen;
|
||||||
this.showOverlay = !this.showOverlay;
|
|
||||||
this.isOpen = this.showOverlay;
|
|
||||||
} else {
|
|
||||||
this.isOpen = !this.isOpen;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(path: string) {
|
navigate(path: string) {
|
||||||
this.router.navigate([path]);
|
this.router.navigate([path]);
|
||||||
|
if (this.isMobile) {
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
} @else if(mode == "forgot-password"){
|
} @else if(mode == "forgot-password"){
|
||||||
<app-login-forgot (otpEventSubmit)="onOtpSendSubmit($event)" (otpVerifyEventSubmit)="onVerifySubmit($event)"></app-login-forgot>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { finalize } from 'rxjs/operators';
|
|||||||
export class LoginContentComponent implements OnInit {
|
export class LoginContentComponent implements OnInit {
|
||||||
@ViewChild(LoginForgotComponent) loginForgotComponent!: LoginForgotComponent;
|
@ViewChild(LoginForgotComponent) loginForgotComponent!: LoginForgotComponent;
|
||||||
@ViewChild(LoginPageComponent) loginPageComponent!: LoginPageComponent;
|
@ViewChild(LoginPageComponent) loginPageComponent!: LoginPageComponent;
|
||||||
mode: 'forgot-password' | 'default' = 'default';
|
mode: 'forgot-password' | 'register' | 'default' = 'default';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private generalService: GeneralService,
|
private generalService: GeneralService,
|
||||||
@@ -27,6 +27,8 @@ export class LoginContentComponent implements OnInit {
|
|||||||
|
|
||||||
if (param === 'forgot-password') {
|
if (param === 'forgot-password') {
|
||||||
this.mode = 'forgot-password';
|
this.mode = 'forgot-password';
|
||||||
|
}else if(param === 'register'){
|
||||||
|
this.mode = 'register';
|
||||||
} else {
|
} else {
|
||||||
// this.router.navigate(['/login']); // This can cause navigation loops
|
// this.router.navigate(['/login']); // This can cause navigation loops
|
||||||
this.mode = 'default';
|
this.mode = 'default';
|
||||||
@@ -57,6 +59,7 @@ export class LoginContentComponent implements OnInit {
|
|||||||
if (result.code === '200' && result.data?.token) {
|
if (result.code === '200' && result.data?.token) {
|
||||||
this.generalService.trowApi(result);
|
this.generalService.trowApi(result);
|
||||||
localStorage.setItem('access_token', result.data.token);
|
localStorage.setItem('access_token', result.data.token);
|
||||||
|
localStorage.setItem('username', result.data.usrthinam);
|
||||||
this.router.navigate(['main/dashboard']);
|
this.router.navigate(['main/dashboard']);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = result.message_th || result.message || 'Sign-in failed.';
|
const errorMessage = result.message_th || result.message || 'Sign-in failed.';
|
||||||
@@ -76,6 +79,33 @@ export class LoginContentComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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){
|
onOtpSendSubmit(value: any){
|
||||||
let uri = '/api/login/otp/send';
|
let uri = '/api/login/otp/send';
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<app-main-dashboard></app-main-dashboard>
|
<app-main-dashboard (saveEventSubmit)="OnSaveSubmit($event)"></app-main-dashboard>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class MainDashboardContentComponent implements OnInit {
|
|||||||
summary: {
|
summary: {
|
||||||
totalIncome: '',
|
totalIncome: '',
|
||||||
totalExpense: '',
|
totalExpense: '',
|
||||||
netProfit: '',
|
netProfit: 0,
|
||||||
profitRate: '',
|
profitRate: '',
|
||||||
adjustedProfitRate: '',
|
adjustedProfitRate: '',
|
||||||
period: ''
|
period: ''
|
||||||
@@ -67,7 +67,27 @@ export class MainDashboardContentComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
OnSetupDashboard(value: any, setupFirst: boolean): void {
|
OnSetupDashboard(value: any, setupFirst: boolean): void {
|
||||||
const uri = '/api/web/accountingSetup';
|
const uri = '/api/web/accountingSetup';
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
<div class="flex h-screen overflow-hidden">
|
<!-- Mobile Header -->
|
||||||
<!-- Sidebar (เฉพาะ main) -->
|
<div class="md:hidden flex items-center justify-between bg-white text-gray-800 p-4 shadow-md relative z-50">
|
||||||
<app-sidebar></app-sidebar>
|
<button (click)="sidebar.toggleSidebar()" class="p-2">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-4 6h10"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex h-screen bg-gray-50">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<app-sidebar #sidebar></app-sidebar>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 overflow-y-auto bg-gray-50 text-gray-900">
|
<div class="flex-1 overflow-y-auto overflow-x-hidden text-gray-900">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { LoginPageComponent } from '../../component/login-page/login-page.compon
|
|||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { LoginForgotComponent } from '../../component/login-forgot/login-forgot.component';
|
import { LoginForgotComponent } from '../../component/login-forgot/login-forgot.component';
|
||||||
|
import { LoginRegisterComponent } from '../../component/login-register/login-register.component';
|
||||||
// import { AppModule } from '../../app.module';
|
// import { AppModule } from '../../app.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
LoginContentComponent,
|
LoginContentComponent,
|
||||||
LoginPageComponent,
|
LoginPageComponent,
|
||||||
LoginForgotComponent
|
LoginForgotComponent,
|
||||||
|
LoginRegisterComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface IActSumData {
|
|||||||
export interface IActSummary {
|
export interface IActSummary {
|
||||||
totalIncome: string;
|
totalIncome: string;
|
||||||
totalExpense: string;
|
totalExpense: string;
|
||||||
netProfit: string;
|
netProfit: number;
|
||||||
profitRate: string;
|
profitRate: string;
|
||||||
adjustedProfitRate: string;
|
adjustedProfitRate: string;
|
||||||
period: string;
|
period: string;
|
||||||
@@ -53,6 +53,14 @@ export interface IActCategory {
|
|||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface QuickRatio {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
colorClass: string; // ตัวเก็บชื่อ class สี
|
||||||
|
}
|
||||||
|
|
||||||
|
// export
|
||||||
// ข้อมูลสินค้าหลัก
|
// ข้อมูลสินค้าหลัก
|
||||||
// export interface IProduct {
|
// export interface IProduct {
|
||||||
// id: string;
|
// id: string;
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export class AccDateFormatPipe implements PipeTransform {
|
|||||||
const str = value.toString();
|
const str = value.toString();
|
||||||
if (str.length !== 12) return str;
|
if (str.length !== 12) return str;
|
||||||
|
|
||||||
const dd = str.slice(0, 2);
|
const yyyy = str.slice(0, 4);
|
||||||
const mm = str.slice(2, 4);
|
const mm = str.slice(4, 6);
|
||||||
const yyyy = str.slice(4, 8);
|
const dd = str.slice(6, 8);
|
||||||
const hh = str.slice(8, 10);
|
const hh = str.slice(8, 10);
|
||||||
const min = str.slice(10, 12);
|
const min = str.slice(10, 12);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
|
// apiBaseUrl: 'https://bread-leader-move-created.trycloudflare.com'
|
||||||
|
|
||||||
|
|
||||||
apiBaseUrl: 'http://localhost:8000'
|
apiBaseUrl: 'http://localhost:8000'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>AccountingNgNuttakit</title>
|
<title>SmartAccount</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
|||||||
@@ -12,10 +12,15 @@
|
|||||||
z-index: 999999 !important;
|
z-index: 999999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input::placeholder{
|
||||||
|
color: #9aa3ad;
|
||||||
|
}
|
||||||
|
|
||||||
/* Make sure the page and app root occupy full height so 100vh aligns */
|
/* Make sure the page and app root occupy full height so 100vh aligns */
|
||||||
html, body, app-root {
|
html, body, app-root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
/* เริ่มต้น: สำหรับ Desktop */
|
/* เริ่มต้น: สำหรับ Desktop */
|
||||||
.login-mobile {
|
.login-mobile {
|
||||||
|
|||||||
553
comunicate-demo/index.html
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TTC Communication Client</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- Socket.io Client -->
|
||||||
|
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#4F46E5', // Indigo 600
|
||||||
|
secondary: '#10B981', // Emerald 500
|
||||||
|
danger: '#EF4444', // Red 500
|
||||||
|
dark: '#1F2937' // Gray 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 8px; }
|
||||||
|
::-webkit-scrollbar-track { background: #f1f1f1; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
transform: scaleX(-1); /* Mirror effect for self view */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 min-h-screen text-gray-800 font-sans">
|
||||||
|
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="bg-white shadow-sm border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-primary text-white p-2 rounded-lg">
|
||||||
|
<i class="fa-solid fa-network-wired"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="font-bold text-xl text-gray-700">TTC Microservice <span class="text-xs font-normal bg-gray-200 px-2 py-0.5 rounded text-gray-500">Demo Client</span></h1>
|
||||||
|
</div>
|
||||||
|
<div id="connectionStatus" class="flex items-center gap-2 text-sm text-red-500 font-medium">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
|
||||||
|
Disconnected
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-6 max-w-6xl grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
|
||||||
|
<!-- LEFT PANEL: Config & Notify -->
|
||||||
|
<div class="lg:col-span-4 space-y-6">
|
||||||
|
|
||||||
|
<!-- 1. Authentication -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
||||||
|
<h2 class="text-lg font-bold mb-4 flex items-center gap-2 text-gray-700">
|
||||||
|
<i class="fa-solid fa-key text-primary"></i> Authentication
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold text-gray-500 uppercase">Server URL</label>
|
||||||
|
<!-- [FIX] เปลี่ยนค่าเริ่มต้นให้ว่างไว้ เดี๋ยว Script จะเติมให้เองตาม Context -->
|
||||||
|
<input type="text" id="serverUrl"
|
||||||
|
class="w-full mt-1 p-2 bg-gray-50 border border-gray-300 rounded focus:ring-2 focus:ring-primary focus:outline-none transition-all"
|
||||||
|
placeholder="Auto-detected..." value=" https://entrepreneur-faced-browsing-gateway.trycloudflare.com">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold text-gray-500 uppercase">JWT Token</label>
|
||||||
|
<textarea id="jwtToken" rows="2" class="w-full mt-1 p-2 bg-gray-50 border border-gray-300 rounded focus:ring-2 focus:ring-primary focus:outline-none text-xs font-mono" placeholder="Paste your Bearer token here..."></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="btnConnect" class="w-full bg-primary hover:bg-indigo-700 text-white font-medium py-2 rounded transition-colors shadow-sm flex justify-center items-center gap-2">
|
||||||
|
<i class="fa-solid fa-link"></i> Connect Socket
|
||||||
|
</button>
|
||||||
|
<div id="userInfoDisplay" class="hidden mt-2 p-2 bg-indigo-50 text-indigo-700 text-xs rounded border border-indigo-100">
|
||||||
|
<!-- User info will show here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Notification System -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 flex flex-col h-[500px]">
|
||||||
|
<h2 class="text-lg font-bold mb-4 flex items-center gap-2 text-gray-700">
|
||||||
|
<i class="fa-solid fa-bell text-secondary"></i> Notification
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Notify Form -->
|
||||||
|
<div class="space-y-3 mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<input type="text" id="notifyTargetId" class="w-full p-2 text-sm border border-gray-300 rounded" placeholder="Target User ID (seq)">
|
||||||
|
<input type="text" id="notifyTitle" class="w-full p-2 text-sm border border-gray-300 rounded" placeholder="Topic / Title">
|
||||||
|
<input type="text" id="notifyMessage" class="w-full p-2 text-sm border border-gray-300 rounded" placeholder="Message body...">
|
||||||
|
<button id="btnSendNotify" class="w-full bg-secondary hover:bg-emerald-600 text-white text-sm font-medium py-2 rounded transition-colors">
|
||||||
|
Send Realtime Notify
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notify Logs -->
|
||||||
|
<div class="flex-1 overflow-y-auto space-y-2 pr-1" id="notifyLogs">
|
||||||
|
<div class="text-center text-gray-400 text-xs mt-10">No notifications yet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT PANEL: VoIP -->
|
||||||
|
<div class="lg:col-span-8">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 h-full flex flex-col">
|
||||||
|
<h2 class="text-lg font-bold mb-4 flex items-center gap-2 text-gray-700 border-b pb-3">
|
||||||
|
<i class="fa-solid fa-video text-pink-500"></i> Video Conference / VoIP
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Control Bar -->
|
||||||
|
<div class="flex flex-wrap gap-3 mb-4 items-end bg-gray-50 p-3 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="text-xs font-semibold text-gray-500 uppercase">Call To (User ID)</label>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input type="text" id="callTargetId" class="flex-1 p-2 border border-gray-300 rounded focus:ring-2 focus:ring-pink-500 focus:outline-none" placeholder="User ID...">
|
||||||
|
<button id="btnCall" class="bg-pink-500 hover:bg-pink-600 text-white px-4 py-2 rounded shadow-sm flex items-center gap-2 transition-colors">
|
||||||
|
<i class="fa-solid fa-phone"></i> Call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="btnHangup" class="hidden bg-red-500 hover:bg-red-600 text-white px-6 py-2 rounded shadow-sm flex items-center gap-2 transition-colors">
|
||||||
|
<i class="fa-solid fa-phone-slash"></i> Hangup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incoming Call Alert (Modal-ish) -->
|
||||||
|
<div id="incomingCallAlert" class="hidden mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex justify-between items-center shadow-sm animate-bounce">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-yellow-100 p-2 rounded-full text-yellow-600">
|
||||||
|
<i class="fa-solid fa-phone-volume fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-gray-800">Incoming Call...</h3>
|
||||||
|
<p class="text-sm text-gray-500">From User: <span id="callerNameDisplay" class="font-mono text-black">Unknown</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="btnAccept" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded shadow text-sm font-bold">
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
<button id="btnReject" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded shadow text-sm font-bold">
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Area -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 flex-1 min-h-[400px]">
|
||||||
|
<!-- Local -->
|
||||||
|
<div class="relative bg-gray-900 rounded-xl overflow-hidden shadow-inner group">
|
||||||
|
<video id="localVideo" autoplay playsinline muted class="w-full h-full object-cover video-container opacity-50"></video>
|
||||||
|
<div class="absolute bottom-4 left-4 text-white text-xs bg-black/50 px-2 py-1 rounded backdrop-blur-sm">
|
||||||
|
You (Local)
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center text-gray-500 group-hover:hidden" id="localPlaceholder">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fa-solid fa-camera-slash fa-2x mb-2"></i>
|
||||||
|
<p class="text-xs">Camera Off</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote -->
|
||||||
|
<div class="relative bg-gray-900 rounded-xl overflow-hidden shadow-inner">
|
||||||
|
<video id="remoteVideo" autoplay playsinline class="w-full h-full object-cover"></video>
|
||||||
|
<div class="absolute bottom-4 left-4 text-white text-xs bg-black/50 px-2 py-1 rounded backdrop-blur-sm">
|
||||||
|
Remote User
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center text-gray-600" id="remotePlaceholder">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fa-solid fa-user-slash fa-2x mb-2"></i>
|
||||||
|
<p class="text-xs">Waiting for connection...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN SCRIPT -->
|
||||||
|
<script type="module">
|
||||||
|
/** * TTC Client Logic
|
||||||
|
* Implements Socket.io & WebRTC Standard
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
let socket = null;
|
||||||
|
let myStream = null;
|
||||||
|
let peerConnection = null;
|
||||||
|
let incomingSignal = null; // Store offer temporarily
|
||||||
|
let activeCallUser = null;
|
||||||
|
let myUserId = null;
|
||||||
|
|
||||||
|
// --- Config ---
|
||||||
|
const rtcConfig = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
// { urls: 'stun:global.stun.twilio.com:3478' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const els = {
|
||||||
|
serverUrl: document.getElementById('serverUrl'),
|
||||||
|
jwtToken: document.getElementById('jwtToken'),
|
||||||
|
btnConnect: document.getElementById('btnConnect'),
|
||||||
|
status: document.getElementById('connectionStatus'),
|
||||||
|
userInfo: document.getElementById('userInfoDisplay'),
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
notifyTarget: document.getElementById('notifyTargetId'),
|
||||||
|
notifyTitle: document.getElementById('notifyTitle'),
|
||||||
|
notifyMsg: document.getElementById('notifyMessage'),
|
||||||
|
btnSendNotify: document.getElementById('btnSendNotify'),
|
||||||
|
notifyLogs: document.getElementById('notifyLogs'),
|
||||||
|
|
||||||
|
// VoIP
|
||||||
|
callTarget: document.getElementById('callTargetId'),
|
||||||
|
btnCall: document.getElementById('btnCall'),
|
||||||
|
btnHangup: document.getElementById('btnHangup'),
|
||||||
|
incomingAlert: document.getElementById('incomingCallAlert'),
|
||||||
|
callerName: document.getElementById('callerNameDisplay'),
|
||||||
|
btnAccept: document.getElementById('btnAccept'),
|
||||||
|
btnReject: document.getElementById('btnReject'),
|
||||||
|
|
||||||
|
localVideo: document.getElementById('localVideo'),
|
||||||
|
remoteVideo: document.getElementById('remoteVideo'),
|
||||||
|
localPlace: document.getElementById('localPlaceholder'),
|
||||||
|
remotePlace: document.getElementById('remotePlaceholder'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// [FIX] Auto-populate Server URL to avoid Mixed Content Error
|
||||||
|
// ถ้าเข้าผ่าน https (ngrok) มันจะใช้ https ตามอัตโนมัติ
|
||||||
|
// window.onload = () => {
|
||||||
|
// els.serverUrl.value = window.location.origin;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// --- 1. Connection Logic ---
|
||||||
|
els.btnConnect.addEventListener('click', () => {
|
||||||
|
const url = els.serverUrl.value;
|
||||||
|
const token = els.jwtToken.value.trim();
|
||||||
|
|
||||||
|
//if (!token) return alert('Please enter a JWT Token');
|
||||||
|
|
||||||
|
// [FIX] Initialize Socket with Secure Options
|
||||||
|
// window.location.origin จะแก้ปัญหา Mixed Content เพราะมันจะใช้ protocol เดียวกับหน้าเว็บ
|
||||||
|
socket = io(url, {
|
||||||
|
auth: { token: token },
|
||||||
|
transports: ['websocket', 'polling'], // เพิ่ม polling เพื่อความชัวร์ในบาง network
|
||||||
|
secure: true, // Force secure connection
|
||||||
|
rejectUnauthorized: false // ยอมรับ Self-signed cert กรณีเทส local
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Connection Events
|
||||||
|
socket.on('connect', () => {
|
||||||
|
updateStatus(true);
|
||||||
|
console.log('✅ Connected:', socket.id);
|
||||||
|
// Decode token roughly to show ID (Optional)
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
|
myUserId = payload.id;
|
||||||
|
els.userInfo.innerHTML = `Signed in as: <b>${payload.usrnam}</b> (ID: ${payload.id})`;
|
||||||
|
els.userInfo.classList.remove('hidden');
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
updateStatus(false);
|
||||||
|
console.log('❌ Disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (err) => {
|
||||||
|
updateStatus(false);
|
||||||
|
console.error('Socket Error:', err);
|
||||||
|
alert('Connection Failed: ' + err.message + '\n(Check Mixed Content/CORS in Console)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Register Listeners ---
|
||||||
|
setupNotificationListeners();
|
||||||
|
setupVoIPListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatus(isConnected) {
|
||||||
|
if(isConnected) {
|
||||||
|
els.status.innerHTML = `<div class="w-2 h-2 rounded-full bg-green-500"></div> Connected`;
|
||||||
|
els.status.className = "flex items-center gap-2 text-sm text-green-600 font-bold";
|
||||||
|
els.btnConnect.disabled = true;
|
||||||
|
els.btnConnect.innerText = "Connected";
|
||||||
|
els.btnConnect.classList.add('bg-gray-400', 'cursor-not-allowed');
|
||||||
|
} else {
|
||||||
|
els.status.innerHTML = `<div class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div> Disconnected`;
|
||||||
|
els.status.className = "flex items-center gap-2 text-sm text-red-500 font-medium";
|
||||||
|
els.btnConnect.disabled = false;
|
||||||
|
els.btnConnect.innerText = "Connect Socket";
|
||||||
|
els.btnConnect.classList.remove('bg-gray-400', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Notification Logic ---
|
||||||
|
function setupNotificationListeners() {
|
||||||
|
// Sending
|
||||||
|
els.btnSendNotify.addEventListener('click', () => {
|
||||||
|
if(!socket) return alert("Not connected");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
targetUserId: els.notifyTarget.value,
|
||||||
|
title: els.notifyTitle.value,
|
||||||
|
message: els.notifyMsg.value,
|
||||||
|
type: 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.emit('send_notification', data);
|
||||||
|
addNotifyLog('outgoing', `To ${data.targetUserId}: ${data.title}`);
|
||||||
|
els.notifyMsg.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receiving
|
||||||
|
socket.on('receive_notification', (data) => {
|
||||||
|
addNotifyLog('incoming', `From ${data.from}: ${data.title} - ${data.message}`);
|
||||||
|
// Play sound if needed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNotifyLog(type, text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const time = new Date().toLocaleTimeString('th-TH', { hour: '2-digit', minute:'2-digit'});
|
||||||
|
|
||||||
|
if(type === 'incoming') {
|
||||||
|
div.className = "bg-white border-l-4 border-secondary p-3 rounded shadow-sm text-sm";
|
||||||
|
div.innerHTML = `<div class="flex justify-between text-xs text-gray-400 mb-1"><span>Incoming</span> <span>${time}</span></div>
|
||||||
|
<div class="text-gray-700">${text}</div>`;
|
||||||
|
} else {
|
||||||
|
div.className = "bg-gray-50 border-l-4 border-gray-300 p-3 rounded text-sm";
|
||||||
|
div.innerHTML = `<div class="flex justify-between text-xs text-gray-400 mb-1"><span>Sent</span> <span>${time}</span></div>
|
||||||
|
<div class="text-gray-600">${text}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
els.notifyLogs.prepend(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. VoIP / WebRTC Logic ---
|
||||||
|
async function getLocalStream() {
|
||||||
|
try {
|
||||||
|
// [FIX] ใช้ constraints แบบง่าย เพื่อลดโอกาสเกิด OverconstrainedError
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
audio: { echoCancellation: true, noiseSuppression: true }
|
||||||
|
});
|
||||||
|
myStream = stream;
|
||||||
|
els.localVideo.srcObject = stream;
|
||||||
|
els.localVideo.classList.remove('opacity-50');
|
||||||
|
els.localPlace.classList.add('hidden');
|
||||||
|
return stream;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Media Error:", err);
|
||||||
|
|
||||||
|
let msg = "Cannot access Camera/Microphone.";
|
||||||
|
if(location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
|
msg += " (Browser requires HTTPS or Localhost)";
|
||||||
|
} else {
|
||||||
|
msg += " " + err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(msg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPeerConnection(targetId) {
|
||||||
|
const pc = new RTCPeerConnection(rtcConfig);
|
||||||
|
|
||||||
|
// Add local tracks
|
||||||
|
if(myStream) {
|
||||||
|
myStream.getTracks().forEach(track => pc.addTrack(track, myStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ICE Candidates
|
||||||
|
pc.onicecandidate = (event) => {
|
||||||
|
if (event.candidate) {
|
||||||
|
socket.emit('send_ice_candidate', {
|
||||||
|
targetUserId: targetId,
|
||||||
|
candidate: event.candidate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Remote Stream
|
||||||
|
pc.ontrack = (event) => {
|
||||||
|
const [remoteStream] = event.streams;
|
||||||
|
els.remoteVideo.srcObject = remoteStream;
|
||||||
|
els.remotePlace.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connection State Changes
|
||||||
|
pc.onconnectionstatechange = () => {
|
||||||
|
console.log("Peer State:", pc.connectionState);
|
||||||
|
if(pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
|
||||||
|
hangupUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVoIPListeners() {
|
||||||
|
|
||||||
|
// --- A. Caller Logic ---
|
||||||
|
els.btnCall.addEventListener('click', async () => {
|
||||||
|
const target = els.callTarget.value;
|
||||||
|
if(!target || !socket) return;
|
||||||
|
|
||||||
|
activeCallUser = target;
|
||||||
|
|
||||||
|
// 1. Get Media
|
||||||
|
const stream = await getLocalStream();
|
||||||
|
if (!stream) return; // Exit if media failed
|
||||||
|
|
||||||
|
// 2. Create PC
|
||||||
|
peerConnection = createPeerConnection(target);
|
||||||
|
|
||||||
|
// 3. Create Offer
|
||||||
|
const offer = await peerConnection.createOffer();
|
||||||
|
await peerConnection.setLocalDescription(offer);
|
||||||
|
|
||||||
|
// 4. Send Signal
|
||||||
|
socket.emit('call_user', {
|
||||||
|
userToCall: target,
|
||||||
|
signalData: offer
|
||||||
|
});
|
||||||
|
|
||||||
|
uiCallingState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- B. Callee Logic (Incoming) ---
|
||||||
|
socket.on('call_incoming', (data) => {
|
||||||
|
incomingSignal = data.signal;
|
||||||
|
activeCallUser = data.from; // Store who is calling
|
||||||
|
els.callerName.innerText = `ID: ${data.from}`;
|
||||||
|
|
||||||
|
els.incomingAlert.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
els.btnAccept.addEventListener('click', async () => {
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
|
||||||
|
// 1. Get Media
|
||||||
|
const stream = await getLocalStream();
|
||||||
|
if (!stream) return;
|
||||||
|
|
||||||
|
// 2. Create PC
|
||||||
|
peerConnection = createPeerConnection(activeCallUser);
|
||||||
|
|
||||||
|
// 3. Set Remote Desc (Offer)
|
||||||
|
await peerConnection.setRemoteDescription(incomingSignal);
|
||||||
|
|
||||||
|
// 4. Create Answer
|
||||||
|
const answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
|
||||||
|
// 5. Send Answer
|
||||||
|
socket.emit('answer_call', {
|
||||||
|
to: activeCallUser,
|
||||||
|
signal: answer
|
||||||
|
});
|
||||||
|
|
||||||
|
uiConnectedState();
|
||||||
|
});
|
||||||
|
|
||||||
|
els.btnReject.addEventListener('click', () => {
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
// Optional: Send reject signal
|
||||||
|
hangupUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- C. Connection Establishment ---
|
||||||
|
socket.on('call_accepted', async (data) => {
|
||||||
|
// Caller receives Answer
|
||||||
|
await peerConnection.setRemoteDescription(data.signal);
|
||||||
|
uiConnectedState();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('receive_ice_candidate', async (data) => {
|
||||||
|
if(peerConnection) {
|
||||||
|
try {
|
||||||
|
await peerConnection.addIceCandidate(data.candidate);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("ICE Error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- D. Hangup ---
|
||||||
|
els.btnHangup.addEventListener('click', () => {
|
||||||
|
socket.emit('end_call', { targetUserId: activeCallUser });
|
||||||
|
hangupUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('call_ended', () => {
|
||||||
|
alert("Call Ended by remote user");
|
||||||
|
hangupUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI Helpers ---
|
||||||
|
function uiCallingState() {
|
||||||
|
els.btnCall.disabled = true;
|
||||||
|
els.btnCall.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Calling...`;
|
||||||
|
els.btnHangup.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiConnectedState() {
|
||||||
|
els.btnCall.classList.add('hidden');
|
||||||
|
els.btnHangup.classList.remove('hidden');
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hangupUI() {
|
||||||
|
if(peerConnection) peerConnection.close();
|
||||||
|
if(myStream) myStream.getTracks().forEach(t => t.stop());
|
||||||
|
|
||||||
|
peerConnection = null;
|
||||||
|
myStream = null;
|
||||||
|
activeCallUser = null;
|
||||||
|
incomingSignal = null;
|
||||||
|
|
||||||
|
els.localVideo.srcObject = null;
|
||||||
|
els.remoteVideo.srcObject = null;
|
||||||
|
els.localPlace.classList.remove('hidden');
|
||||||
|
els.remotePlace.classList.remove('hidden');
|
||||||
|
|
||||||
|
els.btnCall.disabled = false;
|
||||||
|
els.btnCall.classList.remove('hidden');
|
||||||
|
els.btnCall.innerHTML = `<i class="fa-solid fa-phone"></i> Call`;
|
||||||
|
els.btnHangup.classList.add('hidden');
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
comunicate-demo/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 1. สร้างไฟล์นี้ในโฟลเดอร์เดียวกับ index.html
|
||||||
|
// 2. รันคำสั่ง: npm install express (ถ้ายังไม่มี)
|
||||||
|
// 3. สตาร์ทเซิร์ฟเวอร์: node server_frontend.js
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const app = express();
|
||||||
|
const PORT = 80; // พอร์ตสำหรับหน้าเว็บ (แยกกับ API 1011)
|
||||||
|
|
||||||
|
// ให้บริการไฟล์ Static ในโฟลเดอร์ปัจจุบัน
|
||||||
|
app.use(express.static(path.join(__dirname)));
|
||||||
|
|
||||||
|
// Route หลักส่ง index.html
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// สั่งให้ Listen ทุก IP ในเครื่อง (0.0.0.0)
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log('---------------------------------------------------');
|
||||||
|
console.log(`🚀 Frontend Server running!`);
|
||||||
|
console.log(`🏠 Local: http://localhost:${PORT}`);
|
||||||
|
console.log(`📡 Network: http://<YOUR_IP_ADDRESS>:${PORT}`);
|
||||||
|
console.log('---------------------------------------------------');
|
||||||
|
console.log('To find your IP: Run "ipconfig" (Windows) or "ifconfig" (Mac/Linux)');
|
||||||
|
});
|
||||||
17
comunicate-demo/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "comunicate-demo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"path": "^0.12.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
listen-pipe.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
trap "rm -f $HOME/execpipe" EXIT
|
||||||
|
trap "rm -f $HOME/execpipe" ERR
|
||||||
|
|
||||||
|
mkfifo $HOME/execpipe
|
||||||
|
while true; do eval "$(cat $HOME/execpipe)"; done
|
||||||
35
ng-ttc-frontend/.vscode/launch.json
vendored
@@ -1,20 +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"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
{
|
{
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "ng serve",
|
"name": "Launch Chrome against localhost",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "npm: start",
|
"url": "http://localhost:4200",
|
||||||
"url": "http://localhost:4200/"
|
"webRoot": "${workspaceFolder}"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ng test",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "npm: test",
|
|
||||||
"url": "http://localhost:9876/debug.html"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
|
||||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@@ -116,12 +114,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/bootstrap/dist/css/bootstrap.min.css",
|
|
||||||
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
314
ng-ttc-frontend/package-lock.json
generated
@@ -25,15 +25,14 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"bootstrap": "^5.3.8",
|
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"jose": "^6.1.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"ng2-charts": "^6.0.1",
|
"ng2-charts": "^6.0.1",
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.17",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"ngx-toastr": "^19.1.0",
|
"ngx-toastr": "^19.1.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -626,8 +625,8 @@
|
|||||||
"version": "20.3.10",
|
"version": "20.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.10.tgz",
|
||||||
"integrity": "sha512-COOT2eVebDwHhwENk12VR6m0wjL8D7p0dncEHF15zaBt1IXEnVhGESjSrs5klnPnt5T55qCBKyCTaeK7i/cS8Q==",
|
"integrity": "sha512-COOT2eVebDwHhwENk12VR6m0wjL8D7p0dncEHF15zaBt1IXEnVhGESjSrs5klnPnt5T55qCBKyCTaeK7i/cS8Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
"ajv-formats": "3.0.1",
|
"ajv-formats": "3.0.1",
|
||||||
@@ -654,8 +653,8 @@
|
|||||||
"version": "20.3.10",
|
"version": "20.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.10.tgz",
|
||||||
"integrity": "sha512-2N2WF9lj+kr3uCG4+vFadYCL5hAT4dxMgzwScSdOqSd0O+GZD0CzKbDzlfvWIWC/ZealC5Sh4dFEQaRfmy72xA==",
|
"integrity": "sha512-2N2WF9lj+kr3uCG4+vFadYCL5hAT4dxMgzwScSdOqSd0O+GZD0CzKbDzlfvWIWC/ZealC5Sh4dFEQaRfmy72xA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": "20.3.10",
|
"@angular-devkit/core": "20.3.10",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
@@ -673,6 +672,7 @@
|
|||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -685,6 +685,7 @@
|
|||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
@@ -697,12 +698,14 @@
|
|||||||
"version": "10.6.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@angular-devkit/schematics/node_modules/is-interactive": {
|
"node_modules/@angular-devkit/schematics/node_modules/is-interactive": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -715,6 +718,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -727,6 +731,7 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
@@ -743,6 +748,7 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -755,6 +761,7 @@
|
|||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
||||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
@@ -778,6 +785,7 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^10.3.0",
|
"emoji-regex": "^10.3.0",
|
||||||
@@ -795,6 +803,7 @@
|
|||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^6.0.1"
|
"ansi-regex": "^6.0.1"
|
||||||
@@ -811,7 +820,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.12.tgz",
|
||||||
"integrity": "sha512-tkzruF0pbcOrC2lwsPKjkp5btazs6vcX4At7kyVFjjuPbgI6RNG+MoFXHpN9ypenscYtTAhDcPSmjBnzoDaXhQ==",
|
"integrity": "sha512-tkzruF0pbcOrC2lwsPKjkp5btazs6vcX4At7kyVFjjuPbgI6RNG+MoFXHpN9ypenscYtTAhDcPSmjBnzoDaXhQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -970,7 +978,6 @@
|
|||||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -1057,22 +1064,6 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/cdk": {
|
|
||||||
"version": "20.2.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.13.tgz",
|
|
||||||
"integrity": "sha512-h1jTkCmJ/rEQQMkxgKFMCBOrMfjZEnppgdekNmSTerwdVp4vdosTDTzFH/kwiOGFeRClffmvqQ2XLG8mQOKOtA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"parse5": "^8.0.0",
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@angular/common": "^20.0.0 || ^21.0.0",
|
|
||||||
"@angular/core": "^20.0.0 || ^21.0.0",
|
|
||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@angular/cli": {
|
"node_modules/@angular/cli": {
|
||||||
"version": "20.3.10",
|
"version": "20.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.10.tgz",
|
||||||
@@ -1126,7 +1117,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.12.tgz",
|
||||||
"integrity": "sha512-rFcDfe67ffrb435C6t2lc27WGbizeOcgce30tUhH0iezwEvU+kHHWezXXX6Ylx3TFgqGkhcxL0fliuFYrpM1Vw==",
|
"integrity": "sha512-rFcDfe67ffrb435C6t2lc27WGbizeOcgce30tUhH0iezwEvU+kHHWezXXX6Ylx3TFgqGkhcxL0fliuFYrpM1Vw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -1143,7 +1133,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.12.tgz",
|
||||||
"integrity": "sha512-bGESKz97nWiEQ/sydTq/Lzv3zlLvDb8t0msLG5Xti7Ch1EdLddXS8d2D/zFsjiGbAUKVsT6RgPCLHYoi4ocbhA==",
|
"integrity": "sha512-bGESKz97nWiEQ/sydTq/Lzv3zlLvDb8t0msLG5Xti7Ch1EdLddXS8d2D/zFsjiGbAUKVsT6RgPCLHYoi4ocbhA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -1157,7 +1146,6 @@
|
|||||||
"integrity": "sha512-3SJkexqsydYjIs0iLiJr5AdwkvumpzvjJM6s76iaxXHkRll5k/vM0wqkXLlSIwieBrecO9D4J73lDLWDevXl5A==",
|
"integrity": "sha512-3SJkexqsydYjIs0iLiJr5AdwkvumpzvjJM6s76iaxXHkRll5k/vM0wqkXLlSIwieBrecO9D4J73lDLWDevXl5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.28.3",
|
"@babel/core": "7.28.3",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||||
@@ -1190,7 +1178,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.12.tgz",
|
||||||
"integrity": "sha512-K7vibMr55a7+EsuDhkg4Pk+ELuMm12olllwqL/CiQUcHXZ9Zgc4KYGTUuxWB69qJCG90gdSZS7tm5Dx0wDcyjg==",
|
"integrity": "sha512-K7vibMr55a7+EsuDhkg4Pk+ELuMm12olllwqL/CiQUcHXZ9Zgc4KYGTUuxWB69qJCG90gdSZS7tm5Dx0wDcyjg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -1234,7 +1221,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.12.tgz",
|
||||||
"integrity": "sha512-14KQsXZyaQhbRwFz1W58CtbXQc9L+mfuHBgwQjQo99422Yk0ye5WVMb6DHH7dH671qFVqL0XL7zdOPBebaAnJQ==",
|
"integrity": "sha512-14KQsXZyaQhbRwFz1W58CtbXQc9L+mfuHBgwQjQo99422Yk0ye5WVMb6DHH7dH671qFVqL0XL7zdOPBebaAnJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -1319,7 +1305,6 @@
|
|||||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -3166,7 +3151,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.4.tgz",
|
||||||
"integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==",
|
"integrity": "sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -3205,7 +3189,6 @@
|
|||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -3769,68 +3752,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/windows-sign": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"cross-dirname": "^0.1.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"fs-extra": "^11.1.1",
|
|
||||||
"minimist": "^1.2.8",
|
|
||||||
"postject": "^1.0.0-alpha.6"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"electron-windows-sign": "bin/electron-windows-sign.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
|
|
||||||
"version": "11.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
|
|
||||||
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"jsonfile": "^6.0.1",
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/windows-sign/node_modules/jsonfile": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/windows-sign/node_modules/universalify": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@epic-web/invariant": {
|
"node_modules/@epic-web/invariant": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
@@ -4602,7 +4523,6 @@
|
|||||||
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
|
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/checkbox": "^4.2.1",
|
"@inquirer/checkbox": "^4.2.1",
|
||||||
"@inquirer/confirm": "^5.1.14",
|
"@inquirer/confirm": "^5.1.14",
|
||||||
@@ -7031,17 +6951,6 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@popperjs/core": {
|
|
||||||
"version": "2.11.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
|
||||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/popperjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.52.3",
|
"version": "4.52.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
|
||||||
@@ -8275,7 +8184,6 @@
|
|||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -8658,7 +8566,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -8749,8 +8656,8 @@
|
|||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -8766,6 +8673,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||||
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.0.0"
|
"ajv": "^8.0.0"
|
||||||
@@ -9434,25 +9342,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap": {
|
|
||||||
"version": "5.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
|
||||||
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/twbs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/bootstrap"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@popperjs/core": "^2.11.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bplist-parser": {
|
"node_modules/bplist-parser": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
|
||||||
@@ -9510,7 +9399,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -9888,7 +9776,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -9900,7 +9787,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"readdirp": "^4.0.1"
|
||||||
@@ -9969,6 +9856,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"restore-cursor": "^5.0.0"
|
"restore-cursor": "^5.0.0"
|
||||||
@@ -9984,6 +9872,7 @@
|
|||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -10608,14 +10497,6 @@
|
|||||||
"buffer": "^5.1.0"
|
"buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cross-dirname": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/cross-env": {
|
"node_modules/cross-env": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||||
@@ -10983,7 +10864,6 @@
|
|||||||
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
|
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "26.0.12",
|
"app-builder-lib": "26.0.12",
|
||||||
"builder-util": "26.0.11",
|
"builder-util": "26.0.11",
|
||||||
@@ -11316,19 +11196,6 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-builder-squirrel-windows": {
|
|
||||||
"version": "26.0.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz",
|
|
||||||
"integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"app-builder-lib": "26.0.12",
|
|
||||||
"builder-util": "26.0.11",
|
|
||||||
"electron-winstaller": "5.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-builder/node_modules/cliui": {
|
"node_modules/electron-builder/node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -11501,42 +11368,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/electron-winstaller": {
|
|
||||||
"version": "5.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
|
||||||
"integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@electron/asar": "^3.2.1",
|
|
||||||
"debug": "^4.1.1",
|
|
||||||
"fs-extra": "^7.0.1",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"temp": "^0.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@electron/windows-sign": "^1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-winstaller/node_modules/fs-extra": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"graceful-fs": "^4.1.2",
|
|
||||||
"jsonfile": "^4.0.0",
|
|
||||||
"universalify": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6 <7 || >=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron/node_modules/@types/node": {
|
"node_modules/electron/node_modules/@types/node": {
|
||||||
"version": "22.19.1",
|
"version": "22.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||||
@@ -12051,7 +11882,6 @@
|
|||||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -12218,6 +12048,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
@@ -12261,6 +12092,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -12626,6 +12458,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -13776,8 +13609,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz",
|
||||||
"integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==",
|
"integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/jest-worker": {
|
"node_modules/jest-worker": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
@@ -13816,11 +13648,19 @@
|
|||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -13875,6 +13715,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-stringify-safe": {
|
"node_modules/json-stringify-safe": {
|
||||||
@@ -13939,7 +13780,6 @@
|
|||||||
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
|
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@colors/colors": "1.5.0",
|
"@colors/colors": "1.5.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
@@ -14260,7 +14100,6 @@
|
|||||||
"integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==",
|
"integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"copy-anything": "^2.0.1",
|
"copy-anything": "^2.0.1",
|
||||||
"parse-node-version": "^1.0.1",
|
"parse-node-version": "^1.0.1",
|
||||||
@@ -14640,7 +14479,6 @@
|
|||||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cli-truncate": "^4.0.0",
|
"cli-truncate": "^4.0.0",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
@@ -15084,6 +14922,7 @@
|
|||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
@@ -15352,6 +15191,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -16903,6 +16743,7 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||||
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-function": "^5.0.0"
|
"mimic-function": "^5.0.0"
|
||||||
@@ -17494,6 +17335,7 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "^6.0.0"
|
"entities": "^6.0.0"
|
||||||
@@ -17547,6 +17389,7 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
@@ -17678,6 +17521,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -17754,7 +17598,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -17887,34 +17730,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/postject": {
|
|
||||||
"version": "1.0.0-alpha.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
|
||||||
"integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^9.4.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"postject": "dist/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postject/node_modules/commander": {
|
|
||||||
"version": "9.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
|
||||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || >=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proc-log": {
|
"node_modules/proc-log": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz",
|
||||||
@@ -18178,7 +17993,7 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 14.18.0"
|
||||||
@@ -18274,6 +18089,7 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -18401,6 +18217,7 @@
|
|||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||||
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"onetime": "^7.0.0",
|
"onetime": "^7.0.0",
|
||||||
@@ -18659,7 +18476,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -18726,7 +18542,6 @@
|
|||||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"immutable": "^5.0.2",
|
"immutable": "^5.0.2",
|
||||||
@@ -19200,6 +19015,7 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -19432,6 +19248,7 @@
|
|||||||
"version": "0.7.6",
|
"version": "0.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||||
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
|
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
@@ -19624,6 +19441,7 @@
|
|||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||||
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -19821,20 +19639,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/temp": {
|
|
||||||
"version": "0.9.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
|
||||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"rimraf": "~2.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/temp-file": {
|
"node_modules/temp-file": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
|
||||||
@@ -19884,27 +19688,12 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/temp/node_modules/rimraf": {
|
|
||||||
"version": "2.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
|
||||||
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
|
||||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rimraf": "bin.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.43.1",
|
"version": "5.43.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.14.0",
|
||||||
@@ -20108,8 +19897,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD",
|
"license": "0BSD"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tuf-js": {
|
"node_modules/tuf-js": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -20490,7 +20278,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -20831,7 +20618,6 @@
|
|||||||
"integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==",
|
"integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
@@ -21428,7 +21214,6 @@
|
|||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -21447,8 +21232,7 @@
|
|||||||
"version": "0.15.1",
|
"version": "0.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||||
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,15 +57,14 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"bootstrap": "^5.3.8",
|
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"jose": "^6.1.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"ng2-charts": "^6.0.1",
|
"ng2-charts": "^6.0.1",
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.17",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
@@ -86,7 +85,7 @@
|
|||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"ngx-toastr": "^19.1.0",
|
"ngx-toastr": "^19.1.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
ng-ttc-frontend/public/ballot_0.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
ng-ttc-frontend/public/bell_0.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
ng-ttc-frontend/public/calendar_0.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
ng-ttc-frontend/public/chart-histogram_0.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
ng-ttc-frontend/public/chart-simple.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
ng-ttc-frontend/public/check_0.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
ng-ttc-frontend/public/coins_0.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
ng-ttc-frontend/public/exclamation_0.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ng-ttc-frontend/public/exit_0.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
ng-ttc-frontend/public/form_0.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
ng-ttc-frontend/public/home (1)_0.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
ng-ttc-frontend/public/home_0.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
ng-ttc-frontend/public/icons8-home-384.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
ng-ttc-frontend/public/interrogation.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
ng-ttc-frontend/public/interrogation_0.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
ng-ttc-frontend/public/locked-computer_0.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
ng-ttc-frontend/public/menu-burger_0.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
ng-ttc-frontend/public/pencil_0.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
ng-ttc-frontend/public/plus-small_0.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
ng-ttc-frontend/public/search_0.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
ng-ttc-frontend/public/settings (1)_0.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
ng-ttc-frontend/public/stamp_0.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
ng-ttc-frontend/public/user_0.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@@ -2,11 +2,12 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { SidebarContentComponent } from './content/sidebar-content/sidebar-content.component';
|
import { SidebarContentComponent } from './content/sidebar-content/sidebar-content.component';
|
||||||
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
|
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
|
||||||
// import { authGuard } from './services/auth.guard';
|
import { authGuard } from './services/auth.guard';
|
||||||
|
import { loginGuard } from './services/login.guard';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
|
||||||
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) },
|
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule), canActivate: [loginGuard] },
|
||||||
|
|
||||||
{ path: 'license', component: LicensePrivacyTermsComponent},
|
{ path: 'license', component: LicensePrivacyTermsComponent},
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ const routes: Routes = [
|
|||||||
path: 'main',
|
path: 'main',
|
||||||
component: SidebarContentComponent,
|
component: SidebarContentComponent,
|
||||||
canActivate: [
|
canActivate: [
|
||||||
// authGuard
|
authGuard
|
||||||
],
|
],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ThemeService } from './services/theme.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -6,6 +7,13 @@ import { Component } from '@angular/core';
|
|||||||
standalone: false,
|
standalone: false,
|
||||||
styleUrl: './app.component.css'
|
styleUrl: './app.component.css'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
|
constructor(private themeService: ThemeService) {}
|
||||||
title = 'ng-ttc-frontend';
|
title = 'ng-ttc-frontend';
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// โหลดธีมเมื่อคอมโพเนนต์เริ่มต้นทำงาน
|
||||||
|
this.themeService.getCurrentTheme();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import { SidebarContentComponent } from './content/sidebar-content/sidebar-conte
|
|||||||
import { SidebarComponent } from './component/sidebar/sidebar.component';
|
import { SidebarComponent } from './component/sidebar/sidebar.component';
|
||||||
// import { ReactiveFormsModule } from '@angular/forms';
|
// import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
|
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
|
||||||
|
import { TokenTimerComponent } from './component/token-timer/token-timer.component';
|
||||||
// import { MainDashboardContentComponent } from './content/main-dashboard-content/main-dashboard-content.component';
|
// import { MainDashboardContentComponent } from './content/main-dashboard-content/main-dashboard-content.component';
|
||||||
// import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component';
|
// import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component';
|
||||||
// import { LoginForgotComponent } from './component/login-forgot/login-forgot.component';
|
// import { LoginForgotComponent } from './component/login-forgot/login-forgot.component';
|
||||||
@@ -23,6 +24,14 @@ import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/
|
|||||||
|
|
||||||
|
|
||||||
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
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 { BudgetAprovalContent } from './content/budget-aproval-content/budget-aproval-content';
|
||||||
|
import { ThemeSwitcherComponent } from './component/theme-switcher/theme-switcher';
|
||||||
|
import { ChatWidgetComponent } from './component/chat-widget-component/chat-widget-component';
|
||||||
|
// import { BudgetAproval } from './component/budget-aproval/budget-aproval';
|
||||||
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
|
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
|
||||||
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
|
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
|
||||||
|
|
||||||
@@ -33,6 +42,14 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
|||||||
SidebarContentComponent,
|
SidebarContentComponent,
|
||||||
SidebarComponent,
|
SidebarComponent,
|
||||||
LicensePrivacyTermsComponent,
|
LicensePrivacyTermsComponent,
|
||||||
|
TokenTimerComponent,
|
||||||
|
// ChatWidgetComponent,
|
||||||
|
// ThemeSwitcherComponent,
|
||||||
|
// BudgetAprovalContent
|
||||||
|
// MainProjectAdd,
|
||||||
|
// MainProject,
|
||||||
|
// MainProjectContent,
|
||||||
|
// BudgetAproval,
|
||||||
// AccDateFormatPipe
|
// AccDateFormatPipe
|
||||||
// DtmtodatetimePipe,
|
// DtmtodatetimePipe,
|
||||||
// MainDashboardContentComponent,
|
// MainDashboardContentComponent,
|
||||||
@@ -59,7 +76,10 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
|||||||
exports: [
|
exports: [
|
||||||
// AccDateFormatPipe
|
// AccDateFormatPipe
|
||||||
],
|
],
|
||||||
providers: [provideCharts(withDefaultRegisterables())],
|
providers: [
|
||||||
|
provideCharts(withDefaultRegisterables()),
|
||||||
|
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
|
||||||
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.animate-fade-in-down { animation: fadeInDown 0.3s ease-out; }
|
||||||
|
.animate-fade-in { animation: fadeIn 0.2s ease-out; }
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
input[type=number]::-webkit-inner-spin-button,
|
||||||
|
input[type=number]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
|
||||||
|
<div class="w-full p-6 space-y-8 bg-gray-50 min-h-screen font-sans text-gray-800">
|
||||||
|
|
||||||
|
<!-- 1. Header Section -->
|
||||||
|
<div class="flex flex-col md:flex-row md:justify-between md:items-end gap-4 border-b border-gray-200 pb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800 flex items-center gap-3">
|
||||||
|
<!-- <span class="bg-red-100 text-red-800 p-2 rounded-lg text-xl">💸</span> -->
|
||||||
|
<span>จัดสรรงบประมาณ</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-500 mt-1 ml-1 text-sm">โครงการ: <span class="font-medium text-red-900">{{projectTitle || prjnam}}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500 bg-white px-4 py-2 rounded-full shadow-sm border border-gray-100">
|
||||||
|
รายการทั้งหมด: <span class="font-bold text-red-800">{{ myTrnMst.length }}</span> รายการ
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Add/Edit Budget Form (Collapsible) -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden transition-all duration-300">
|
||||||
|
<!-- Toggle Header -->
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center p-4 bg-gray-50 cursor-pointer hover:bg-gray-100 transition select-none border-b border-gray-100"
|
||||||
|
(click)="toggleFormCollapse()"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 font-bold text-gray-700">
|
||||||
|
<!-- <span class="bg-red-600 text-white w-6 h-6 flex items-center justify-center rounded-full text-xs shadow-sm">
|
||||||
|
{{ isEditMode ? '✎' : '✚' }}
|
||||||
|
</span> -->
|
||||||
|
<span class="w-1 h-6 bg-red-900 rounded-full mr-1"></span>{{ isEditMode ? 'แก้ไขรายการ' : 'เพิ่มรายการงบประมาณ' }}
|
||||||
|
</div>
|
||||||
|
<button class="text-gray-400 transition-transform duration-300" [ngClass]="{'rotate-180': isFormExpanded}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Content -->
|
||||||
|
@if(isFormExpanded){
|
||||||
|
<div class="p-6 bg-white animate-fade-in-down">
|
||||||
|
<form [formGroup]="budgetForm">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 items-start">
|
||||||
|
|
||||||
|
<!-- Dropdown หมวดงบ -->
|
||||||
|
<div class="md:col-span-5 space-y-1">
|
||||||
|
<label class="block text-xs font-bold text-gray-500 uppercase">
|
||||||
|
หมวดงบประมาณ <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select formControlName="bdgcod"
|
||||||
|
class="w-full p-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent bg-gray-50 transition cursor-pointer"
|
||||||
|
[class.border-red-500]="budgetForm.get('bdgcod')?.invalid && budgetForm.get('bdgcod')?.touched">
|
||||||
|
<option value="">-- กรุณาเลือก --</option>
|
||||||
|
@for (item of myDropBdg; track item.bdgcod) {
|
||||||
|
<option [value]="item.bdgcod">{{ item.bdgnam }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<!-- Error Message -->
|
||||||
|
@if (budgetForm.get('bdgcod')?.invalid && budgetForm.get('bdgcod')?.touched) {
|
||||||
|
<div class="text-red-500 text-xs flex items-center gap-1">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
กรุณาเลือกหมวดงบ
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input จำนวนเงิน -->
|
||||||
|
<div class="md:col-span-5 space-y-1">
|
||||||
|
<label class="block text-xs font-bold text-gray-500 uppercase">
|
||||||
|
จำนวนเงิน <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="number" formControlName="amount" placeholder="0.00"
|
||||||
|
class="w-full p-3 pr-12 border border-gray-200 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent bg-gray-50 text-right font-mono text-gray-800 transition"
|
||||||
|
[class.border-red-500]="budgetForm.get('amount')?.invalid && budgetForm.get('amount')?.touched">
|
||||||
|
<span class="absolute right-4 top-3 text-gray-400 text-sm font-medium pointer-events-none">THB</span>
|
||||||
|
</div>
|
||||||
|
<!-- Error Message -->
|
||||||
|
@if (budgetForm.get('amount')?.invalid && budgetForm.get('amount')?.touched) {
|
||||||
|
<div class="text-red-500 text-xs flex items-center gap-1">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
|
ระบุจำนวนเงินที่ถูกต้อง
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="md:col-span-2 flex gap-2 pt-6">
|
||||||
|
<button type="button" (click)="onSaveSubmit()"
|
||||||
|
class="flex-1 bg-red-800 hover:bg-red-900 text-white font-bold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition active:scale-95 flex justify-center items-center gap-2">
|
||||||
|
<span>{{ isEditMode ? 'บันทึกแก้ไข' : 'เพิ่ม' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if(isEditMode) {
|
||||||
|
<button type="button" (click)="cancelEdit()"
|
||||||
|
class="bg-gray-200 hover:bg-gray-300 text-gray-600 font-bold py-3 px-4 rounded-lg transition active:scale-95">
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Budget Table -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-red-900 text-white text-sm uppercase tracking-wider">
|
||||||
|
<th class="py-4 px-6 font-semibold w-16 text-center">#</th>
|
||||||
|
<th class="py-4 px-6 font-semibold">รายการ / หมวดงบ</th>
|
||||||
|
<th class="py-4 px-6 font-semibold text-right w-48">จำนวนเงิน (บาท)</th>
|
||||||
|
<th class="py-4 px-6 font-semibold text-center w-32">จัดการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100 text-sm">
|
||||||
|
@for (idx of myTrnMst; track idx.trnseq; let i = $index) {
|
||||||
|
<tr class="hover:bg-red-50/40 transition group">
|
||||||
|
<td class="py-4 px-6 text-center text-gray-500 font-mono">{{ i + 1 }}</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="font-bold text-gray-800">{{ idx.trnbdgnam }}</div>
|
||||||
|
<div class="text-xs text-gray-400 font-mono mt-0.5">{{ idx.trnbdgcod }}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-right font-mono font-medium text-gray-700">
|
||||||
|
{{ idx.trnexpbdg | number:'1.2-2' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<div class="flex justify-center items-center gap-2">
|
||||||
|
<button (click)="editItem(i)" class="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition" title="แก้ไข">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button (click)="removeItem(i)" class="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition" title="ลบ">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="py-12 text-center text-gray-400 bg-gray-50/30">
|
||||||
|
<div class="text-4xl mb-2 opacity-50">📭</div>
|
||||||
|
<p>ยังไม่มีรายการงบประมาณ</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<!-- Footer Total & Action Buttons -->
|
||||||
|
<tfoot class="bg-gray-50 border-t-2 border-red-200">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="py-4 px-6 text-right font-bold text-gray-600 uppercase text-xs tracking-wider">รวมยอดสุทธิ</td>
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<span class="text-xl font-extrabold text-red-900">{{ getTotalAmount() | number:'1.2-2' }}</span>
|
||||||
|
<span class="text-xs text-gray-500 ml-1 font-medium">THB</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-3">
|
||||||
|
<!-- 🟢 ปุ่มย้อนกลับ (Icon Arrow) -->
|
||||||
|
<button (click)="goBack()" class="p-2.5 bg-white border border-gray-200 rounded-lg text-gray-500 hover:text-red-700 hover:border-red-200 hover:bg-red-50 hover:shadow-sm transition active:scale-95 group" title="ย้อนกลับ">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 group-hover:-translate-x-1 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 🟢 ปุ่มยืนยันบันทึกทั้งหมด -->
|
||||||
|
<button (click)="openConfirmModal()"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white text-xs font-bold py-2.5 px-5 rounded-lg shadow transition active:scale-95 flex items-center gap-2">
|
||||||
|
<span>✓</span> บันทึก
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(showConfirmModal) {
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl w-96 overflow-hidden transform transition-all scale-100">
|
||||||
|
|
||||||
|
<!-- 🟢 Dynamic Header Color -->
|
||||||
|
<div class="p-4 text-white flex justify-between items-center"
|
||||||
|
[ngClass]="myTrnMst.length === 0 ? 'bg-red-600' : 'bg-red-900'">
|
||||||
|
<h3 class="font-bold text-lg flex items-center gap-2">
|
||||||
|
<span>{{ myTrnMst.length === 0 ? '⚠️' : '💾' }}</span>
|
||||||
|
{{ myTrnMst.length === 0 ? 'ยืนยันการยกเลิก' : 'ยืนยันการบันทึก' }}
|
||||||
|
</h3>
|
||||||
|
<button (click)="closeConfirmModal()" class="text-white/70 hover:text-white">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 text-center space-y-4">
|
||||||
|
<!-- 🟢 Dynamic Icon -->
|
||||||
|
<div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-2"
|
||||||
|
[ngClass]="myTrnMst.length === 0 ? 'bg-red-50 text-red-600' : 'bg-red-50 text-red-600'">
|
||||||
|
<span class="text-3xl">{{ myTrnMst.length === 0 ? '🗑️' : '📝' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🟢 Dynamic Message -->
|
||||||
|
<p class="text-gray-600">
|
||||||
|
@if(myTrnMst.length === 0) {
|
||||||
|
คุณต้องการ <b>ยกเลิกการจัดสรรงบประมาณ</b> <br>ของโครงการนี้ใช่หรือไม่?
|
||||||
|
} @else {
|
||||||
|
คุณต้องการ <b>บันทึกข้อมูลการจัดสรรงบประมาณ</b> <br>ของโครงการนี้ใช่หรือไม่?
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Summary Info -->
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg border border-gray-100 text-sm">
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-gray-500">จำนวนรายการ:</span>
|
||||||
|
<span class="font-bold text-gray-800">{{ myTrnMst.length }} รายการ</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-500">ยอดรวม:</span>
|
||||||
|
<span class="font-bold text-red-800">{{ getTotalAmount() | number:'1.2-2' }} บาท</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🟢 List of Items (บอกรายการ) -->
|
||||||
|
<div class="text-xs text-gray-500 mt-2 text-left max-h-32 overflow-y-auto border-t border-gray-200 pt-2">
|
||||||
|
<div class="font-semibold mb-1">รายการ:</div>
|
||||||
|
@if(myTrnMst.length > 0) {
|
||||||
|
<ul class="list-disc pl-4 space-y-1">
|
||||||
|
@for(item of myTrnMst; track $index) {
|
||||||
|
<li>{{ item.trnbdgnam }} ({{ item.trnexpbdg | number }})</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
} @else {
|
||||||
|
<p class="text-red-500 italic">- ไม่พบรายการคงเหลือ (ลบทั้งหมด) -</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="p-4 bg-gray-50 flex gap-3 border-t border-gray-100">
|
||||||
|
<button (click)="closeConfirmModal()" class="flex-1 py-2.5 px-4 bg-white border border-gray-300 text-gray-700 font-bold rounded-xl hover:bg-gray-50 transition">
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
<!-- 🟢 Dynamic Button Color -->
|
||||||
|
<button (click)="confirmSave()"
|
||||||
|
class="flex-1 py-2.5 px-4 text-white font-bold rounded-xl shadow-md hover:shadow-lg transition"
|
||||||
|
[ngClass]="myTrnMst.length === 0 ? 'bg-red-600 hover:bg-red-700' : 'bg-red-800 hover:bg-red-900'">
|
||||||
|
{{ myTrnMst.length === 0 ? 'ยืนยันการยกเลิก' : 'ยืนยัน' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- 🟢 Modal Confirmation
|
||||||
|
@if(showConfirmModal) {
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl w-96 overflow-hidden transform transition-all scale-100">
|
||||||
|
|
||||||
|
Modal Header
|
||||||
|
<div class="bg-red-900 p-4 text-white flex justify-between items-center">
|
||||||
|
<h3 class="font-bold text-lg flex items-center gap-2">
|
||||||
|
<span>💾</span> ยืนยันการบันทึก
|
||||||
|
</h3>
|
||||||
|
<button (click)="closeConfirmModal()" class="text-white/70 hover:text-white">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Modal Body
|
||||||
|
<div class="p-6 text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span class="text-3xl">📝</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600">คุณต้องการบันทึกข้อมูลการจัดสรรงบประมาณใช่หรือไม่?</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg border border-gray-100 text-sm">
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="text-gray-500">จำนวนรายการ:</span>
|
||||||
|
<span class="font-bold text-gray-800">{{ myTrnMst.length }} รายการ</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-500">ยอดรวม:</span>
|
||||||
|
<span class="font-bold text-red-800">{{ getTotalAmount() | number:'1.2-2' }} บาท</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Modal Footer
|
||||||
|
<div class="p-4 bg-gray-50 flex gap-3 border-t border-gray-100">
|
||||||
|
<button (click)="closeConfirmModal()" class="flex-1 py-2.5 px-4 bg-white border border-gray-300 text-gray-700 font-bold rounded-xl hover:bg-gray-50 transition">
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
<button (click)="confirmSave()" class="flex-1 py-2.5 px-4 bg-red-800 text-white font-bold rounded-xl hover:bg-red-900 shadow-md hover:shadow-lg transition">
|
||||||
|
ยืนยัน
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 🔴 Modal Confirmation (Delete) -->
|
||||||
|
@if(showDeleteModal) {
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl w-96 overflow-hidden transform transition-all scale-100">
|
||||||
|
|
||||||
|
<div class="bg-red-600 p-4 text-white flex justify-between items-center">
|
||||||
|
<h3 class="font-bold text-lg flex items-center gap-2">
|
||||||
|
<span>🗑️</span> ยืนยันการลบ
|
||||||
|
</h3>
|
||||||
|
<button (click)="closeDeleteModal()" class="text-white/70 hover:text-white">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span class="text-3xl">⚠️</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600">คุณต้องการลบรายการนี้ใช่หรือไม่?</p>
|
||||||
|
<p class="text-sm text-gray-500">การกระทำนี้ไม่สามารถย้อนกลับได้ (ในหน้าจอนี้)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 bg-gray-50 flex gap-3 border-t border-gray-100">
|
||||||
|
<button (click)="closeDeleteModal()" class="flex-1 py-2.5 px-4 bg-white border border-gray-300 text-gray-700 font-bold rounded-xl hover:bg-gray-50 transition">
|
||||||
|
ยกเลิก
|
||||||
|
</button>
|
||||||
|
<button (click)="confirmDelete()" class="flex-1 py-2.5 px-4 bg-red-600 text-white font-bold rounded-xl hover:bg-red-700 shadow-md hover:shadow-lg transition">
|
||||||
|
ลบรายการ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import { TransactionStateService } from './../../services/state/transaction-state.service';
|
||||||
|
import { ITrnmst, IDropBdg } from './../../interfaces/main.interface';
|
||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
|
import { GeneralService } from '../../services/generalservice';
|
||||||
|
import { DashboardStateService } from '../../services/state/dashboard-state.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-budget-aproval',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './budget-aproval.html',
|
||||||
|
styleUrl: './budget-aproval.css',
|
||||||
|
})
|
||||||
|
export class BudgetAproval implements OnInit {
|
||||||
|
@Output() ExpenseEventSubmit = new EventEmitter<any>();
|
||||||
|
@Input() prjnam!: string;
|
||||||
|
@Input() prjseq!: string;
|
||||||
|
|
||||||
|
budgetForm!: FormGroup;
|
||||||
|
isFormExpanded = true;
|
||||||
|
isEditMode = false;
|
||||||
|
editingIndex: number = -1;
|
||||||
|
showConfirmModal = false;
|
||||||
|
myTrnMst: ITrnmst[]=[];
|
||||||
|
myDropBdg: IDropBdg[]=[];
|
||||||
|
param:string = '';
|
||||||
|
// State สำหรับการลบ
|
||||||
|
showDeleteModal = false;
|
||||||
|
deleteIndex: number = -1;
|
||||||
|
|
||||||
|
// budgetCategoriesDrop = [
|
||||||
|
// { dtlcod: 'EXP01', dtlnam: 'ค่าวัสดุอุปกรณ์ (EXP01)' },
|
||||||
|
// { dtlcod: 'EXP02', dtlnam: 'ค่าจ้างเหมาแรงงาน (EXP02)' },
|
||||||
|
// { dtlcod: 'EXP03', dtlnam: 'ค่าครุภัณฑ์ (EXP03)' }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// budgetItems: IBudgetItem[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private location: Location,
|
||||||
|
private toastr: ToastrService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private transactionStateService: TransactionStateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.param = this.route.snapshot.paramMap.get('seq') ?? '';
|
||||||
|
this.setupFormControl();
|
||||||
|
this.transactionStateService.getTransactionState().subscribe(data => {
|
||||||
|
this.myTrnMst = data || []
|
||||||
|
});
|
||||||
|
this.transactionStateService.getBudgetDrop().subscribe(data => {
|
||||||
|
this.myDropBdg = data ?? []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFormControl() {
|
||||||
|
this.budgetForm = new FormGroup({
|
||||||
|
bdgcod: new FormControl('', [Validators.required]),
|
||||||
|
amount: new FormControl('', [Validators.required, Validators.min(1)])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFormCollapse() {
|
||||||
|
this.isFormExpanded = !this.isFormExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// onSaveSubmit() {
|
||||||
|
// if (this.budgetForm.invalid) {
|
||||||
|
// this.budgetForm.markAllAsTouched();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const formValue = this.budgetForm.value;
|
||||||
|
// const selectedCode = formValue.bdgcod;
|
||||||
|
|
||||||
|
// // Validation: เช็คว่าหมวดงบซ้ำหรือไม่
|
||||||
|
// const duplicateIndex = this.myTrnMst.findIndex(idx => idx.trnbdgcod === selectedCode);
|
||||||
|
|
||||||
|
// if (duplicateIndex !== -1) {
|
||||||
|
// // กรณีเพิ่มใหม่ (เจอซ้ำ = ห้าม) หรือ กรณีแก้ไข (เจอซ้ำกับแถวอื่นที่ไม่ใช่ตัวเอง = ห้าม)
|
||||||
|
// if (!this.isEditMode || (this.isEditMode && duplicateIndex !== this.editingIndex)) {
|
||||||
|
// this.toastr.error(`หมวดงบประมาณนี้ มีอยู่ในตารางแล้ว`, 'ไม่สามารถเพิ่มซ้ำได้', {
|
||||||
|
// positionClass: 'toast-top-right',
|
||||||
|
// timeOut: 3500,
|
||||||
|
// progressBar: true,
|
||||||
|
// progressAnimation: 'decreasing',
|
||||||
|
// toastClass:
|
||||||
|
// 'ngx-toastr error-toast bg-white bg-opacity-90 text-red-700 shadow-lg'
|
||||||
|
// })
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const category = this.myDropBdg.find(c => c.bdgcod === selectedCode);
|
||||||
|
|
||||||
|
// const newItem: ITrnmst = {
|
||||||
|
// trnbdgcod: formValue.bdgcod,
|
||||||
|
// trnbdgnam: category ? category.bdgcod : formValue.bdgcod,
|
||||||
|
// trnexpbdg: Number(formValue.amount)
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (this.isEditMode && this.editingIndex > -1) {
|
||||||
|
// this.myTrnMst[this.editingIndex] = newItem;
|
||||||
|
// this.cancelEdit();
|
||||||
|
// } else {
|
||||||
|
// this.myTrnMst.push(newItem);
|
||||||
|
// this.budgetForm.reset();
|
||||||
|
// this.budgetForm.get('bdgcod')?.setValue('');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
onSaveSubmit() {
|
||||||
|
if (this.budgetForm.invalid) {
|
||||||
|
this.budgetForm.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formValue = this.budgetForm.value;
|
||||||
|
const selectedCode = formValue.bdgcod;
|
||||||
|
|
||||||
|
// 1. Validation: เช็คว่าหมวดงบซ้ำหรือไม่
|
||||||
|
const duplicateIndex = this.myTrnMst.findIndex(idx => idx.trnbdgcod === selectedCode);
|
||||||
|
|
||||||
|
if (duplicateIndex !== -1) {
|
||||||
|
// ถ้าไม่ใช่โหมดแก้ไข หรือ แก้ไขแต่ไปซ้ำกับคนอื่น
|
||||||
|
if (!this.isEditMode || (this.isEditMode && duplicateIndex !== this.editingIndex)) {
|
||||||
|
this.toastr.error(`หมวดงบประมาณนี้ (${selectedCode}) มีอยู่ในตารางแล้ว`, 'ไม่สามารถเพิ่มซ้ำได้', {
|
||||||
|
timeOut: 3500,
|
||||||
|
progressBar: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 2. ค้นหาชื่อหมวดงบ (Name) จาก Dropdown โดยใช้ Code ที่เลือก
|
||||||
|
const category = this.myDropBdg.find(c => c.bdgcod === selectedCode);
|
||||||
|
|
||||||
|
// ✅ 3. สร้าง Object ใหม่ โดยใส่ชื่อ (trnbdgnam) ที่ค้นหาได้ลงไป
|
||||||
|
const newItem: ITrnmst = {
|
||||||
|
trnbdgcod: selectedCode,
|
||||||
|
// ถ้าเจอชื่อให้ใส่ชื่อ ถ้าไม่เจอให้ใส่รหัสแทน (กันเหนียว)
|
||||||
|
trnbdgnam: category ? category.bdgnam : selectedCode,
|
||||||
|
trnexpbdg: Number(formValue.amount),
|
||||||
|
trnprjnam: this.projectTitle // คงชื่อโครงการเดิมไว้
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isEditMode && this.editingIndex > -1) {
|
||||||
|
// กรณีแก้ไข: Merge ข้อมูลใหม่ทับของเดิม (เพื่อรักษา field อื่นๆ เช่น trnseq ไว้ถ้ามี)
|
||||||
|
this.myTrnMst[this.editingIndex] = {
|
||||||
|
...this.myTrnMst[this.editingIndex],
|
||||||
|
...newItem
|
||||||
|
};
|
||||||
|
this.cancelEdit();
|
||||||
|
} else {
|
||||||
|
// กรณีเพิ่มใหม่
|
||||||
|
this.myTrnMst.push(newItem);
|
||||||
|
this.budgetForm.reset();
|
||||||
|
this.budgetForm.get('bdgcod')?.setValue('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editItem(index: number) {
|
||||||
|
const item = this.myTrnMst[index];
|
||||||
|
this.isEditMode = true;
|
||||||
|
this.editingIndex = index;
|
||||||
|
this.isFormExpanded = true;
|
||||||
|
this.budgetForm.patchValue({
|
||||||
|
bdgcod: item.trnbdgcod,
|
||||||
|
amount: item.trnexpbdg
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelEdit() {
|
||||||
|
this.isEditMode = false;
|
||||||
|
this.editingIndex = -1;
|
||||||
|
this.budgetForm.reset();
|
||||||
|
this.budgetForm.get('bdgcod')?.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(index: number) {
|
||||||
|
// // if(confirm('ต้องการลบรายการนี้หรือไม่?')) {
|
||||||
|
// this.myTrnMst.splice(index, 1);
|
||||||
|
// if (this.isEditMode && this.editingIndex === index) {
|
||||||
|
// this.cancelEdit();
|
||||||
|
// }
|
||||||
|
// // }
|
||||||
|
|
||||||
|
this.deleteIndex = index;
|
||||||
|
this.showDeleteModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDelete() {
|
||||||
|
if (this.deleteIndex > -1) {
|
||||||
|
this.myTrnMst.splice(this.deleteIndex, 1);
|
||||||
|
|
||||||
|
// ถ้าลบตัวที่กำลังแก้อยู่ ให้เคลียร์ฟอร์ม
|
||||||
|
if (this.isEditMode && this.editingIndex === this.deleteIndex) {
|
||||||
|
this.cancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastr.success('ลบรายการเรียบร้อย', 'สำเร็จ');
|
||||||
|
}
|
||||||
|
this.closeDeleteModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDeleteModal() {
|
||||||
|
this.showDeleteModal = false;
|
||||||
|
this.deleteIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotalAmount() {
|
||||||
|
return this.myTrnMst.reduce((sum, item) => sum + Number(item.trnexpbdg || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
openConfirmModal() {
|
||||||
|
this.showConfirmModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeConfirmModal() {
|
||||||
|
this.showConfirmModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmSave() {
|
||||||
|
const expenseList = this.myTrnMst.map(item => ({
|
||||||
|
bdgcod: item.trnbdgcod,
|
||||||
|
amount: Number(item.trnexpbdg)
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
prjseq: this.prjseq,
|
||||||
|
expenseList: expenseList
|
||||||
|
};
|
||||||
|
|
||||||
|
this.expenseEventSubmit(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseEventSubmit(event: any){
|
||||||
|
this.ExpenseEventSubmit.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
get projectTitle(): string {
|
||||||
|
return this.myTrnMst?.[0]?.trnprjnam ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
<!-- Container หลัก: Fixed มุมขวาล่าง -->
|
||||||
|
<div class="fixed bottom-5 right-5 z-50 flex flex-col items-end space-y-4 font-sans">
|
||||||
|
|
||||||
|
<!-- หน้าต่างแชท -->
|
||||||
|
<div *ngIf="isOpen"
|
||||||
|
[style.width.px]="isMaximized ? 600 : chatWidth"
|
||||||
|
[style.height.px]="isMaximized ? 800 : chatHeight"
|
||||||
|
class="bg-white rounded-xl shadow-2xl flex flex-col overflow-hidden border border-gray-200 animate-fade-in-up relative transition-all duration-100 ease-out"
|
||||||
|
[class.max-w-[90vw]]="isMaximized"
|
||||||
|
[class.max-h-[80vh]]="isMaximized">
|
||||||
|
|
||||||
|
<!-- 1. ส่วนจุดดึงขยาย (Resize Handle) -->
|
||||||
|
<div *ngIf="!isMaximized"
|
||||||
|
(mousedown)="startResizing($event)"
|
||||||
|
class="absolute top-0 left-0 w-6 h-6 z-50 cursor-nw-resize flex items-start justify-start p-1 opacity-0 hover:opacity-100 transition-opacity group">
|
||||||
|
<div class="w-2 h-2 border-t-2 border-l-2 border-red-400 rounded-tl-sm"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-red-800 p-3 flex justify-between items-center text-white shadow-md shrink-0 cursor-default select-none">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-8 h-8 bg-red-700 rounded-full flex items-center justify-center text-xs font-bold border border-red-600">
|
||||||
|
AI
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-400 border-2 border-red-800 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col leading-tight">
|
||||||
|
<span class="font-bold text-sm">ผู้ช่วยวิเคราะห์ข้อมูล</span>
|
||||||
|
<span class="text-xs text-red-100">ตอบกลับทันที</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<button (click)="toggleMaximize()" class="hover:bg-red-700 p-1 rounded transition text-red-100 hover:text-white" title="ขยาย/ย่อ">
|
||||||
|
<svg *ngIf="!isMaximized" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
||||||
|
</svg>
|
||||||
|
<svg *ngIf="isMaximized" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9l-6-6M15 9V4.5M15 9h4.5M15 9l6-6M9 15v4.5M9 15H4.5M9 15l-6 6M15 15v4.5M15 15h4.5M15 15l6 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button (click)="toggleChat()" class="hover:bg-red-700 p-1 rounded transition text-red-100 hover:text-white">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Body -->
|
||||||
|
<div class="flex-1 p-4 overflow-y-auto bg-slate-50 space-y-3" #scrollContainer>
|
||||||
|
<div *ngFor="let msg of messages"
|
||||||
|
class="flex w-full"
|
||||||
|
[ngClass]="{'justify-end': msg.isUser, 'justify-start': !msg.isUser}">
|
||||||
|
|
||||||
|
<div *ngIf="!msg.isUser" class="w-6 h-6 bg-red-100 rounded-full shrink-0 mr-2 flex items-center justify-center text-xs text-red-800 font-bold self-end mb-1">
|
||||||
|
AI
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [ngClass]="{
|
||||||
|
'bg-red-800 text-white rounded-tl-2xl rounded-tr-2xl rounded-bl-2xl': msg.isUser,
|
||||||
|
'bg-white text-gray-800 border border-gray-200 rounded-tl-2xl rounded-tr-2xl rounded-br-2xl': !msg.isUser
|
||||||
|
}"
|
||||||
|
class="max-w-[85%] px-4 py-2 text-sm shadow-sm wrap-break-words relative group">
|
||||||
|
{{ msg.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer / Input [Reactive Form] -->
|
||||||
|
<!-- Bind [formGroup] ที่ div นี้เพื่อให้ input ใช้งาน formControlName ได้ -->
|
||||||
|
<div class="p-3 bg-white border-t border-gray-200 flex items-center space-x-2 shrink-0" [formGroup]="chatForm">
|
||||||
|
<button class="text-gray-400 hover:text-red-800 transition">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ใช้ formControlName แทน ngModel -->
|
||||||
|
<input type="text"
|
||||||
|
formControlName="message"
|
||||||
|
(keyup.enter)="sendMessage()"
|
||||||
|
placeholder="พิมพ์ข้อความ..."
|
||||||
|
class="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:bg-white transition text-gray-700 placeholder-gray-400">
|
||||||
|
|
||||||
|
<!-- ใช้ chatForm.invalid ในการ disable ปุ่ม -->
|
||||||
|
<button (click)="sendMessage()"
|
||||||
|
[disabled]="chatForm.invalid"
|
||||||
|
class="text-red-950 hover:text-red-800 p-2 transition disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Launcher Button -->
|
||||||
|
<button (click)="toggleChat()"
|
||||||
|
class="group w-14 h-14 bg-red-800 hover:bg-red-700 text-white rounded-full shadow-lg shadow-red-800/30 flex items-center justify-center transition-all transform hover:scale-110 focus:outline-none ring-4 ring-red-50 hover:ring-red-100 active:scale-95">
|
||||||
|
|
||||||
|
<svg *ngIf="isOpen" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg *ngIf="!isOpen" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 transition-transform group-hover:-rotate-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay กันกดส่วนอื่นตอนกำลังลาก -->
|
||||||
|
<div *ngIf="isResizing" class="fixed inset-0 z-60 cursor-nw-resize"></div>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { GeneralService } from './../../services/generalservice';
|
||||||
|
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
|
||||||
|
import { IChat } from '../../interfaces/main.interface';
|
||||||
|
import { Component, HostListener, OnDestroy } from '@angular/core';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chat-widget-component',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './chat-widget-component.html',
|
||||||
|
styleUrl: './chat-widget-component.css',
|
||||||
|
})
|
||||||
|
export class ChatWidgetComponent implements OnDestroy {
|
||||||
|
// --- State ---
|
||||||
|
isOpen = false;
|
||||||
|
isMaximized = false;
|
||||||
|
isLoading = false; // Added to track API state
|
||||||
|
first = true;
|
||||||
|
|
||||||
|
// --- Form & Data ---
|
||||||
|
chatForm!: FormGroup;
|
||||||
|
messages: IChat[] = [];
|
||||||
|
private subscriptions: Subscription = new Subscription();
|
||||||
|
|
||||||
|
// --- Resize State ---
|
||||||
|
chatWidth = 320;
|
||||||
|
chatHeight = 384;
|
||||||
|
isResizing = false;
|
||||||
|
private startX = 0;
|
||||||
|
private startY = 0;
|
||||||
|
private startWidth = 0;
|
||||||
|
private startHeight = 0;
|
||||||
|
|
||||||
|
constructor(private generalService: GeneralService) {
|
||||||
|
this.setupFormControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Best Practice: Unsubscribe to prevent memory leaks
|
||||||
|
this.subscriptions.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFormControl() {
|
||||||
|
this.chatForm = new FormGroup({
|
||||||
|
message: new FormControl('', [Validators.required]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChat() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
this.isMaximized = false;
|
||||||
|
|
||||||
|
// Only trigger welcome message logic if opening
|
||||||
|
if (this.isOpen) {
|
||||||
|
if (this.first) {
|
||||||
|
// Initial handshake with AI
|
||||||
|
this.OnAiChat({ methods: 'ind' });
|
||||||
|
|
||||||
|
// Add "Waiting" message only if it doesn't exist
|
||||||
|
const isAlreadyHave = this.messages.some((sub) => sub.text === 'รอAi ประมวณผลสักครู่');
|
||||||
|
if (!isAlreadyHave) {
|
||||||
|
this.messages.push({
|
||||||
|
text: 'รอAi ประมวณผลสักครู่',
|
||||||
|
isUser: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMaximize() {
|
||||||
|
this.isMaximized = !this.isMaximized;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage() {
|
||||||
|
if (this.chatForm.invalid) return;
|
||||||
|
|
||||||
|
const messageText = this.chatForm.get('message')?.value;
|
||||||
|
|
||||||
|
if (messageText && messageText.trim()) {
|
||||||
|
// 1. Add User Message to UI
|
||||||
|
this.messages.push({ text: messageText, isUser: true });
|
||||||
|
|
||||||
|
// 2. Clear the form input
|
||||||
|
this.chatForm.reset();
|
||||||
|
|
||||||
|
// 3. Set loading state
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
// 4. Send to API
|
||||||
|
this.OnAiChat({ methods: 'cht', message: messageText });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnAiChat(value: { methods: string; message?: string }) {
|
||||||
|
// Best Practice: Move URL to environment.ts in the future
|
||||||
|
const url = 'https://n8n.nuttakit.work/webhook/Ai';
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
methods: value.methods || 'cht',
|
||||||
|
message: value.message ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sub = this.generalService.postUrl(url, request).subscribe({
|
||||||
|
next: (result: any) => {
|
||||||
|
if (result.code === 200) {
|
||||||
|
// Push AI response
|
||||||
|
this.messages.push(result.data);
|
||||||
|
this.first = false;
|
||||||
|
} else {
|
||||||
|
// Handle API logic errors
|
||||||
|
// specific check for the method if it exists
|
||||||
|
if(this.generalService['trowApi']) {
|
||||||
|
this.generalService.trowApi(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error: any) => {
|
||||||
|
console.error('Chat Error:', error);
|
||||||
|
this.messages.push({ text: 'ขออภัย เกิดข้อผิดพลาดในการเชื่อมต่อ', isUser: false });
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscriptions.add(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Resize Logic (Preserved) ---
|
||||||
|
|
||||||
|
startResizing(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.isResizing = true;
|
||||||
|
this.startX = event.clientX;
|
||||||
|
this.startY = event.clientY;
|
||||||
|
this.startWidth = this.chatWidth;
|
||||||
|
this.startHeight = this.chatHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:mousemove', ['$event'])
|
||||||
|
onMouseMove(event: MouseEvent) {
|
||||||
|
if (!this.isResizing) return;
|
||||||
|
|
||||||
|
// Logic assumes resizing from Top-Left corner (expanding Up and Left)
|
||||||
|
const deltaX = this.startX - event.clientX;
|
||||||
|
const deltaY = this.startY - event.clientY;
|
||||||
|
|
||||||
|
this.chatWidth = Math.max(300, this.startWidth + deltaX);
|
||||||
|
this.chatHeight = Math.max(350, this.startHeight + deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:mouseup')
|
||||||
|
stopResizing() {
|
||||||
|
this.isResizing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,11 @@
|
|||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--shadow: 0 10px 30px rgba(11,26,43,0.08);
|
--shadow: 0 10px 30px rgba(11,26,43,0.08);
|
||||||
--glass: rgba(255,255,255,0.6);
|
--glass: rgba(255,255,255,0.6);
|
||||||
|
--success-color: #10b981; /* Green for success/confirm */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page layout */
|
/* Page layout (unchanged) */
|
||||||
.login-widget {
|
.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;
|
min-height: 100vh;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,8 +23,7 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Card (unchanged) */
|
||||||
/* Card */
|
|
||||||
.login-widget .card{
|
.login-widget .card{
|
||||||
width: 380px;
|
width: 380px;
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 40px);
|
||||||
@@ -37,16 +35,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
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);
|
max-height: calc(100vh - 56px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal/backdrop styles */
|
/* Modal/backdrop styles (unchanged) */
|
||||||
.login-backdrop{
|
.login-backdrop{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0; /* top:0; right:0; bottom:0; left:0; */
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.38);
|
background: rgba(0,0,0,0.38);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -54,43 +50,19 @@
|
|||||||
z-index: 1040;
|
z-index: 1040;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-modal{ width: 480px; max-width: 480px; }
|
.login-modal{ width: 480px; max-width: 480px; }
|
||||||
|
|
||||||
.modal-card{
|
.modal-card{
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 0; /* card children control internal padding */
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slightly larger brand area inside modal */
|
/* Brand area (unchanged) */
|
||||||
.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{
|
.brand{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 18px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid #eef2f5;
|
border-bottom: 1px solid #eef2f5;
|
||||||
}
|
}
|
||||||
@@ -116,16 +88,13 @@ button.primary{
|
|||||||
|
|
||||||
/* Form area */
|
/* Form area */
|
||||||
.form{
|
.form{
|
||||||
/* keep compact spacing inside the card */
|
|
||||||
/* width: 410px; */
|
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 0 2px;
|
padding: 6px 22px 22px 22px;
|
||||||
}
|
}
|
||||||
|
/* Field label wrapper (unchanged) */
|
||||||
/* Field label wrapper */
|
|
||||||
.field{
|
.field{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -135,11 +104,11 @@ button.primary{
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
/* Inputs (class 'input-field' added to HTML) */
|
||||||
/* Inputs */
|
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="text"]{
|
input[type="text"],
|
||||||
|
.input-field { /* เพิ่ม class input-field เพื่อให้สไตล์ถูกใช้กับ input ที่กำหนด */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
@@ -163,31 +132,30 @@ input:focus{
|
|||||||
transform: translateZ(0);
|
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 row */
|
||||||
.actions{
|
.actions{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 4px;
|
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{
|
button.primary{
|
||||||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
||||||
color: #000000;
|
color: #000000; /* ⬅️ แก้ไขเป็นสีดำตามคำขอ */
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -197,6 +165,7 @@ button.primary{
|
|||||||
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
||||||
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary:hover:not(:disabled){
|
button.primary:hover:not(:disabled){
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
||||||
@@ -207,45 +176,26 @@ button.primary:active{
|
|||||||
button.primary:disabled{
|
button.primary:disabled{
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
color: #000000; /* ข้อความ disabled ก็เป็นสีดำ */
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alternative options */
|
/* Secondary Button Style (สำหรับปุ่ม 'เปิด Modal', 'ส่งอีกครั้ง') */
|
||||||
.alt-options{
|
.primary.secondary-button {
|
||||||
display: flex;
|
background: transparent;
|
||||||
align-items: center;
|
color: var(--primary);
|
||||||
gap: 12px;
|
border: 1px solid var(--primary);
|
||||||
margin-top: 6px;
|
box-shadow: none;
|
||||||
flex-wrap: wrap;
|
transition: background-color .14s ease;
|
||||||
}
|
}
|
||||||
.biometric{
|
.primary.secondary-button:hover {
|
||||||
display: inline-flex;
|
background: rgba(0, 120, 212, 0.05);
|
||||||
align-items: center;
|
transform: none;
|
||||||
gap: 10px;
|
box-shadow: none;
|
||||||
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 (unchanged) */
|
||||||
.footer{
|
.footer{
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -262,13 +212,9 @@ button.primary:disabled{
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.footer a:hover{ text-decoration: underline; }
|
|
||||||
.divider{ color: #d0d6db; }
|
|
||||||
|
|
||||||
/* Focus styles for keyboard users */
|
/* Focus styles (unchanged) */
|
||||||
:focus{
|
:focus{ outline: none; }
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
:focus-visible{
|
:focus-visible{
|
||||||
outline: 3px solid rgba(0,120,212,0.12);
|
outline: 3px solid rgba(0,120,212,0.12);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -277,14 +223,13 @@ button.primary:disabled{
|
|||||||
|
|
||||||
/* Small screens */
|
/* Small screens */
|
||||||
@media (max-width:420px){
|
@media (max-width:420px){
|
||||||
|
.login-backdrop{ padding: 12px; }
|
||||||
|
.login-modal{ max-width: 100%; }
|
||||||
|
.modal-card .brand{ padding: 12px; }
|
||||||
.login-widget .card{
|
.login-widget .card{
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.brand h1{ font-size: 18px; }
|
.brand h1{ font-size: 18px; }
|
||||||
.brand .subtitle{
|
|
||||||
font-family: "Kanit";
|
|
||||||
font-weight: 1000;
|
|
||||||
font-style: normal; }
|
|
||||||
.biometric span, .primary{ font-size: 13px; }
|
.biometric span, .primary{ font-size: 13px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,86 @@
|
|||||||
<div class="login-backdrop">
|
<div class="login-backdrop">
|
||||||
<div class="login-modal d-flex align-items-center justify-content-center ">
|
<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="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||||
<div class="brand">
|
|
||||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
|
||||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
|
||||||
<h1 id="signin-title" class='kanit-bold'>ลืมรหัสผ่าน</h1>
|
|
||||||
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
|
|
||||||
</div>
|
|
||||||
<form [formGroup]="forgotFrm" class="form px-3 pb-3 login-mobile">
|
|
||||||
<label class="field">
|
|
||||||
<span class="label-text ">อีเมล์</span>
|
|
||||||
<input type="email" formControlName="email" class="px-2 " id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
@if (isSendOtp === true) {
|
<div class="brand">
|
||||||
<label class="field">
|
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
||||||
<span class="label-text">รหัสยืนยัน OTP</span>
|
<h1 id="signin-title" class="kanit-bold">ลืมรหัสผ่าน</h1>
|
||||||
<input type="email" formControlName="otp" autocomplete="otp" placeholder="123456" alt required/>
|
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
|
||||||
</label>
|
|
||||||
}
|
|
||||||
<!-- <div class="justify-end flex"> -->
|
|
||||||
<!-- <label class="stay-signed">
|
|
||||||
<input type="checkbox" formControlName="remember" />
|
|
||||||
<span>จดจำรหัสผ่าน</span>
|
|
||||||
</label> -->
|
|
||||||
<div class="flex flex-row gap-2 mt-4 justify-end">
|
|
||||||
<div class="flex-row hover:-translate-y-2.5 transform transition-all">
|
|
||||||
<button class="bg-[linear-gradient(180deg,var(--primary)_0%,var(--primary-600)_100%)]
|
|
||||||
text-black
|
|
||||||
border-0
|
|
||||||
px-3.5 py-2.5
|
|
||||||
rounded-md
|
|
||||||
font-semibold
|
|
||||||
cursor-pointer
|
|
||||||
text-[14px]
|
|
||||||
shadow-[0_6px_18px_var(--color-blue-500)]
|
|
||||||
transition
|
|
||||||
ease-in-out
|
|
||||||
duration-100
|
|
||||||
hover:scale-[1.02]
|
|
||||||
active:opacity-90" (click)="isModalOpen = true">
|
|
||||||
เปิด Modal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@if (isSendOtp === false) {
|
|
||||||
<div class="flex justify-end">
|
|
||||||
@if (isLoading === true) {
|
|
||||||
<button type="submit" class="primary cursor-progress!" disabled>
|
|
||||||
กำลังส่ง...
|
|
||||||
</button>
|
|
||||||
} @else {
|
|
||||||
<button type="submit" class="primary" (click)="onSubmin()">
|
|
||||||
{{ 'ยืนยันส่ง OTP รีเซ็ตรหัสผ่าน' }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} @else if(isSendOtp === true) {
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button type="button" class="primary" (click)="onSubmin()">
|
|
||||||
{{ 'ส่งอีกครั้ง' }}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="primary" (click)="onVerifySubmit()">
|
|
||||||
{{ 'รีเซ็ตรหัสผ่าน' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<!-- <button mat-raised-button color="primary" [disabled]="isLoading">
|
|
||||||
{{ isLoading ? 'กำลังส่ง...' : 'ส่งรหัส OTP' }}
|
<form [formGroup]="forgotFrm" class="form">
|
||||||
</button> -->
|
<label class="field">
|
||||||
<!-- } -->
|
<span class="label-text">อีเมล์</span>
|
||||||
<!-- </div> -->
|
<input type="email" formControlName="email" class="input-field" id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
|
||||||
</form>
|
</label>
|
||||||
@if(isModalOpen){
|
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50" [formGroup]="forgotFrm">
|
@if (isSendOtp === true) {
|
||||||
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-fit">
|
<label class="field">
|
||||||
<h2 class="text-xl font-bold mb-4">เปลี่ยนรหัสผ่าน</h2>
|
<span class="label-text">รหัสยืนยัน OTP</span>
|
||||||
<hr class="w-full h-1 bg-gray-300 rounded-sm shadow-neutral-400 md:my-1">
|
<input type="text" formControlName="otp" class="input-field" autocomplete="one-time-code" placeholder="123456" alt required/>
|
||||||
<div class="mb-4">
|
</label>
|
||||||
<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="actions justify-end-custom mt-4">
|
||||||
<div class="mb-4">
|
|
||||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ยืนยันรหัสผ่านใหม่</label>
|
<div class="flex-row hover-lift">
|
||||||
<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="กรอกยืนยันรหัสผ่านใหม่">
|
<button class="primary secondary-button" type="button" (click)="isModalOpen = true">
|
||||||
@if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
|
เปิด Modal
|
||||||
<span class="text-red-600 md">รหัสผ่านไม่ตรงกัน</span>
|
</button>
|
||||||
}
|
</div>
|
||||||
</div>
|
|
||||||
<!-- <hr class="w-full h-[] bg-gray-100 border-0 rounded-sm md:my-1 dark:bg-gray-700"> -->
|
@if (isSendOtp === false) {
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end">
|
||||||
<button class="bg-red-500 text-white px-4 py-2 rounded" (click)="isModalOpen = false">
|
@if (isLoading === true) {
|
||||||
ปิด
|
<button type="submit" class="primary" disabled>
|
||||||
</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>
|
||||||
ยืนยัน
|
} @else {
|
||||||
</button>
|
<button type="submit" class="primary" (click)="onSubmin()">
|
||||||
</div>
|
{{ 'ยืนยันส่ง 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
|
|
||||||
/* Page layout */
|
/* Page layout */
|
||||||
.login-widget {
|
.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;
|
min-height: 100vh;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -37,8 +35,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
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);
|
max-height: calc(100vh - 56px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@@ -46,7 +42,7 @@
|
|||||||
/* Modal/backdrop styles */
|
/* Modal/backdrop styles */
|
||||||
.login-backdrop{
|
.login-backdrop{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0; /* top:0; right:0; bottom:0; left:0; */
|
inset: 0;
|
||||||
background: rgba(0,0,0,0.38);
|
background: rgba(0,0,0,0.38);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -64,33 +60,10 @@
|
|||||||
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
|
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 */
|
||||||
.brand{
|
.brand{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 18px; /* Use padding from modal-card .brand */
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid #eef2f5;
|
border-bottom: 1px solid #eef2f5;
|
||||||
}
|
}
|
||||||
@@ -116,12 +89,11 @@ button.primary{
|
|||||||
|
|
||||||
/* Form area */
|
/* Form area */
|
||||||
.form{
|
.form{
|
||||||
/* keep compact spacing inside the card */
|
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 6px 0 2px;
|
padding: 6px 22px 2px 22px; /* Adjusted padding to match card padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Field label wrapper */
|
/* Field label wrapper */
|
||||||
@@ -185,8 +157,9 @@ input:focus{
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
button.primary{
|
button.primary{
|
||||||
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
/* ⭐️ แก้ไขตรงนี้: เปลี่ยนสีข้อความเป็นสีดำตามคำขอ */
|
||||||
color: #000000;
|
color: #000000;
|
||||||
|
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -196,6 +169,7 @@ button.primary{
|
|||||||
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
|
||||||
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary:hover:not(:disabled){
|
button.primary:hover:not(:disabled){
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
|
||||||
@@ -206,6 +180,7 @@ button.primary:active{
|
|||||||
button.primary:disabled{
|
button.primary:disabled{
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
color: #000000; /* ข้อความ Disabled ก็ยังเป็นสีดำ */
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +205,7 @@ button.primary:disabled{
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.biometric svg{ display: block; opacity: .95; }
|
.biometric svg{ display: block; opacity: .95; }
|
||||||
.biometric:hover{
|
.biometric:hover{
|
||||||
background: rgba(0,120,212,0.04);
|
background: rgba(0,120,212,0.04);
|
||||||
@@ -276,14 +252,14 @@ button.primary:disabled{
|
|||||||
|
|
||||||
/* Small screens */
|
/* Small screens */
|
||||||
@media (max-width:420px){
|
@media (max-width:420px){
|
||||||
|
.login-backdrop{ padding: 12px; }
|
||||||
|
.login-modal{ max-width: 100%; }
|
||||||
|
.modal-card .brand{ padding: 12px; }
|
||||||
|
|
||||||
.login-widget .card{
|
.login-widget .card{
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.brand h1{ font-size: 18px; }
|
.brand h1{ font-size: 18px; }
|
||||||
.brand .subtitle{
|
|
||||||
font-family: "Kanit";
|
|
||||||
font-weight: 1000;
|
|
||||||
font-style: normal; }
|
|
||||||
.biometric span, .primary{ font-size: 13px; }
|
.biometric span, .primary{ font-size: 13px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
<!-- Modal-like backdrop that covers the viewport -->
|
|
||||||
<div class="login-backdrop">
|
<div class="login-backdrop">
|
||||||
<div class="login-modal d-flex align-items-center justify-content-center">
|
<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="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||||
<div class="brand">
|
|
||||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
|
||||||
<img src="/logo.png" alt="Company logo" class="logo mb-2"/>
|
|
||||||
<h1 id="signin-title" class='kanit-bold'>เข้าสู่ระบบ</h1>
|
|
||||||
<p class="subtitle">บัญชีโปรแกรมจัดการแผนงานงบประมาณของท่าน</p>
|
|
||||||
</div>
|
|
||||||
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form px-3 pb-3">
|
|
||||||
<label class="field">
|
|
||||||
<span class="label-text">อีเมล์</span>
|
|
||||||
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<div class="brand">
|
||||||
<span class="label-text">รหัสผ่าน</span>
|
<img src="/logo.png" alt="Company logo" class="logo"/>
|
||||||
<input type="password" formControlName="password" autocomplete="current-password" required />
|
<h1 id="signin-title" class="kanit-bold">เข้าสู่ระบบ</h1>
|
||||||
</label>
|
<p class="subtitle">บัญชีโปรแกรมจัดการแผนงานงบประมาณของท่าน</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form">
|
||||||
<label class="stay-signed">
|
|
||||||
<input type="checkbox" formControlName="remember" />
|
<label class="field">
|
||||||
<span>จดจำรหัสผ่าน</span>
|
<span class="label-text">อีเมล์</span>
|
||||||
|
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required class="input-field" />
|
||||||
</label>
|
</label>
|
||||||
<!-- <fa-icon [icon]="faCoffee" /> -->
|
|
||||||
<button type="submit" class="primary" [disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
|
|
||||||
เข้าสู่ระบบ
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
<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="alt-options">
|
<div class="actions d-flex justify-content-between align-items-center mt-4">
|
||||||
<button type="button" class="biometric" (click)="useBiometric()">
|
<label class="stay-signed">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
<input type="checkbox" formControlName="remember" />
|
||||||
<path d="M12 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" fill="currentColor" opacity=".9"/>
|
<span>จดจำรหัสผ่าน</span>
|
||||||
<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"/>
|
</label>
|
||||||
<path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
<button type="submit" class="primary login-button"
|
||||||
</svg>
|
[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>
|
<span>เข้าสู่ระบบด้วย Windows Hello / Touch ID</span>
|
||||||
</button>
|
</button>
|
||||||
|
<a class="help-link mt-2" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="help-link" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
|
<div class="footer mt-5 text-center">
|
||||||
</div>
|
<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 class="footer">
|
|
||||||
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
|
|
||||||
<span class="divider">•</span>
|
|
||||||
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<section class="dashboard">
|
<section class="dashboard">
|
||||||
<header class="dashboard__hero">
|
<!-- <header class="dashboard__hero">
|
||||||
<div class="hero__text">
|
<div class="hero__text">
|
||||||
<p class="eyebrow">ภาพรวมบัญชี</p>
|
<p class="eyebrow">ภาพรวมบัญชี</p>
|
||||||
<h1>ยินดีต้อนรับกลับ, {{ ownerName }}</h1>
|
<h1>ยินดีต้อนรับกลับ, {{ ownerName }}</h1>
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero__actions">
|
<div class="hero__actions">
|
||||||
<button class="btn btn--primary">สร้างรายงานด่วน</button>
|
<button class="btn btn--primary">สร้างรายงานด่วน</button>
|
||||||
<!-- <button class="btn btn--ghost">อัปโหลดใบเสร็จ</button> -->
|
<button class="btn btn--ghost">อัปโหลดใบเสร็จ</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header> -->
|
||||||
|
|
||||||
<section class="dashboard__periods">
|
<!-- <section class="dashboard__periods">
|
||||||
<article class="period-card" *ngFor="let summary of periodSummaries">
|
<article class="period-card" *ngFor="let summary of periodSummaries">
|
||||||
<header class="period-card__header">
|
<header class="period-card__header">
|
||||||
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
|
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
|
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section> -->
|
||||||
|
|
||||||
<section class="dashboard__stats">
|
<!-- <section class="dashboard__stats">
|
||||||
<article class="stat-card" *ngFor="let card of kpiCards">
|
<article class="stat-card" *ngFor="let card of kpiCards">
|
||||||
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
|
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
|
||||||
<div class="stat-card__body">
|
<div class="stat-card__body">
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
|
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section> -->
|
||||||
|
<!--
|
||||||
<section class="ledger-grid">
|
<section class="ledger-grid">
|
||||||
<article class="panel quick-log">
|
<article class="panel quick-log">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>วันที่</span>
|
<span>วันที่</span>
|
||||||
<!-- <input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/> -->
|
<input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/>
|
||||||
|
|
||||||
<input type="datetime-local"/>
|
<input type="datetime-local"/>
|
||||||
</label>
|
</label>
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
<span>ยอดเงิน</span>
|
<span>ยอดเงิน</span>
|
||||||
<span>บันทึก</span>
|
<span>บันทึก</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- @for (idx of myActData; track i; let i = $index) {
|
@for (idx of myActData; track i; let i = $index) {
|
||||||
<div class="ledger-row">
|
<div class="ledger-row">
|
||||||
<div class="ledger-main">
|
<div class="ledger-main">
|
||||||
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
|
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
|
|
||||||
<span class="ledger-note">{{ idx.note }}</span>
|
<span class="ledger-note">{{ idx.note }}</span>
|
||||||
</div>
|
</div>
|
||||||
} -->
|
}
|
||||||
@for (idx of myActData; track idx.actseq; let i = $index) {
|
@for (idx of myActData; track idx.actseq; let i = $index) {
|
||||||
<div class="ledger-row">
|
<div class="ledger-row">
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section> -->
|
||||||
|
|
||||||
<section class="dashboard__grid">
|
<section class="dashboard__grid">
|
||||||
<!-- <article class="panel panel--main">
|
<!-- <article class="panel panel--main">
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="panel alerts-panel">
|
<!-- <article class="panel alerts-panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>การแจ้งเตือนสำคัญ</h2>
|
<h2>การแจ้งเตือนสำคัญ</h2>
|
||||||
@@ -252,9 +252,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="alert__tag">{{ alert.tag }}</span>
|
<span class="alert__tag">{{ alert.tag }}</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article> -->
|
||||||
|
|
||||||
<article class="panel tasks-panel">
|
<!-- <article class="panel tasks-panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>รายการยอดค้างจ่าย</h2>
|
<h2>รายการยอดค้างจ่าย</h2>
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
<span class="task__badge">{{ task.priority }}</span>
|
<span class="task__badge">{{ task.priority }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article> -->
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<div class="h-screen bg-gray-100 p-4 overflow-y-auto">
|
||||||
|
<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">
|
||||||
|
<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 *ngIf="userData.role === 'U'" 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/project')">
|
||||||
|
<!-- <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 *ngIf="userData.role === 'D'" 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/manager')">
|
||||||
|
<!-- <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 *ngIf="userData.role === 'D'" 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>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
) {}
|
||||||
|
|
||||||
|
userData: any = {
|
||||||
|
name: localStorage.getItem('usrthinam') + ' ' + localStorage.getItem('usrthilstnam'),
|
||||||
|
role: localStorage.getItem('usrrol'),
|
||||||
|
avatar: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
navigate(path: string) {
|
||||||
|
this.router.navigate([path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<div class="w-full p-6">
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-end mb-6 gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
||||||
|
<span class="w-2 h-8 bg-red-900 rounded-full"></span> รายการโครงการทั้งหมด
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 mt-1 text-sm pl-4">จัดการงบประมาณและตรวจสอบสถานะโครงการ</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-red-900 text-white text-sm uppercase tracking-wider leading-normal">
|
||||||
|
<th class="py-4 px-6 font-medium w-16 text-center">ลำดับ</th>
|
||||||
|
<th class="py-4 px-6 font-medium min-w-[200px]">ชื่อโครงการ / รหัส</th>
|
||||||
|
<th class="py-4 px-6 font-medium">ผู้รับผิดชอบ</th>
|
||||||
|
<th class="py-4 px-6 font-medium text-right">งบที่ขอ (บาท)</th>
|
||||||
|
<th class="py-4 px-6 font-medium text-right">อนุมัติ (บาท)</th>
|
||||||
|
<th class="py-4 px-6 font-medium text-center">สถานะ</th>
|
||||||
|
<th class="py-4 px-6 font-medium text-center">จัดการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="text-gray-600 text-sm">
|
||||||
|
@for(idx of myPrjMst; track idx.prjseq; let i = $index) {
|
||||||
|
<tr class="border-b border-gray-100 hover:bg-red-50/40 transition duration-200 group">
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-center text-gray-400 font-mono">
|
||||||
|
{{ i + 1 }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold text-gray-800 text-base group-hover:text-red-900 transition-colors">
|
||||||
|
{{ idx.prjnam }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-400 mt-1 font-mono bg-gray-100 inline-block px-1.5 py-0.5 rounded w-fit">
|
||||||
|
#{{ idx.prjseq }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 text-xs font-bold">
|
||||||
|
{{ idx.prjusrnam?.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
<span class="font-medium">{{ idx.prjusrnam }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<span class="font-mono text-gray-600">
|
||||||
|
{{ idx.prjwntbdg | number:'1.0-0' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-right">
|
||||||
|
<span class="font-mono font-bold text-lg"
|
||||||
|
[ngClass]="{
|
||||||
|
'text-green-600': (idx.prjacpbdg ?? 0) > 0,
|
||||||
|
'text-gray-300': !idx.prjacpbdg || idx.prjacpbdg === 0
|
||||||
|
}">
|
||||||
|
{{ (idx.prjacpbdg ?? 0) | number:'1.0-0' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold border"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-yellow-50 text-yellow-700 border-yellow-200': idx.prjcomstt === 'UAC',
|
||||||
|
'bg-green-50 text-green-700 border-green-200': idx.prjcomstt === 'BAP',
|
||||||
|
'bg-red-50 text-red-700 border-red-200': idx.prjcomstt === 'CN'
|
||||||
|
}">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-yellow-500': idx.prjcomstt === 'UAC',
|
||||||
|
'bg-green-500': idx.prjcomstt === 'BAP',
|
||||||
|
'bg-red-500': idx.prjcomstt === 'CN'
|
||||||
|
}"></span>
|
||||||
|
|
||||||
|
@if(idx.prjcomstt === 'UAC'){ รออนุมัติ }
|
||||||
|
@else if(idx.prjcomstt === 'BAP'){ อนุมัติแล้ว }
|
||||||
|
@else if(idx.prjcomstt === 'CN'){ ไม่อนุมัติ }
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="py-4 px-6 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
|
||||||
|
<button (click)="OnDocumentDownload(idx.prjseq ?? 0)"
|
||||||
|
class="text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:ring-4 focus:outline-none focus:ring-gray-100 font-medium rounded-lg text-xs px-3 py-2 text-center transition-all duration-200 shadow-sm flex items-center justify-center gap-1 whitespace-nowrap" title="ดาวน์โหลดเอกสารโครงการ">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
|
<span>เอกสาร</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button (click)="openBudgetDetail(idx)"
|
||||||
|
class="text-red-900 hover:text-white border border-red-900 hover:bg-red-900 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-2 text-center transition-all duration-200 shadow-sm flex items-center justify-center gap-1 whitespace-nowrap">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
จัดสรรงบ
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="py-12 text-center text-gray-400">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<svg class="w-12 h-12 text-gray-300 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
|
</svg>
|
||||||
|
<span>ไม่พบข้อมูลโครงการในระบบ</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 px-6 py-4 border-t border-gray-200 flex justify-between items-center text-xs text-gray-500">
|
||||||
|
<div>แสดง {{ myPrjMst.length }} รายการ</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
|
import { GeneralService } from '../../services/generalservice';
|
||||||
|
import { IPrjMst } from '../../interfaces/main.interface'
|
||||||
|
import { ProjectStateService } from '../../services/state/project-state.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-manager',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './main-manager.component.html',
|
||||||
|
styleUrl: './main-manager.component.css'
|
||||||
|
})
|
||||||
|
export class MainManagerComponent implements OnInit {
|
||||||
|
@Output() documentDownload = new EventEmitter<any>();
|
||||||
|
|
||||||
|
mode: string = 'i';
|
||||||
|
|
||||||
|
myPrjMst:IPrjMst[] = [];
|
||||||
|
|
||||||
|
totalBudget = 200000; // งบทั้งหมด
|
||||||
|
approvedBudget = 65000; // งบที่อนุมัติแล้ว
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
get remainingBudget() {
|
||||||
|
return this.totalBudget - this.approvedBudget;
|
||||||
|
}
|
||||||
|
|
||||||
|
approveProject(p: any) {
|
||||||
|
p.status = 'BAP';
|
||||||
|
this.approvedBudget += p.budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectProject(p: any) {
|
||||||
|
p.status = 'CN';
|
||||||
|
}
|
||||||
|
|
||||||
|
openBudgetDetail(idx: IPrjMst) {
|
||||||
|
this.router.navigate(['/main/manager/aproval'], {
|
||||||
|
state: {
|
||||||
|
prjseq: idx.prjseq,
|
||||||
|
prjnam: idx.prjnam
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private projectStateService: ProjectStateService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setupFormControl();
|
||||||
|
this.projectStateService.getStateResult().subscribe(data => {
|
||||||
|
if(data){
|
||||||
|
this.myPrjMst = data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
OnDocumentDownload(prjseq: number){
|
||||||
|
this.documentDownload.emit(prjseq);
|
||||||
|
}
|
||||||
|
setupFormControl(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onSaveSubmit(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onArrearsSubmit(){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<div class="flex items-start justify-center p-6">
|
||||||
|
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-3xl border border-gray-100">
|
||||||
|
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h2 class="text-3xl font-bold text-gray-800">สร้างโครงการใหม่</h2>
|
||||||
|
<p class="text-gray-500 mt-2">กรอกรายละเอียดเพื่อขออนุมัติงบประมาณ</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
|
||||||
|
<div [formGroup]="projectForm">
|
||||||
|
|
||||||
|
<div class="transition-all duration-300 ease-in-out border rounded-xl overflow-hidden"
|
||||||
|
[ngClass]="{
|
||||||
|
'border-red-500 shadow-md ring-2 ring-red-100 bg-white': currentStep === 1,
|
||||||
|
'border-gray-200 bg-gray-50 opacity-60': currentStep !== 1
|
||||||
|
}">
|
||||||
|
|
||||||
|
<div class="p-4 flex items-center justify-between"
|
||||||
|
[ngClass]="{ 'bg-red-50 text-red-700': currentStep === 1, 'text-gray-500': currentStep !== 1 }">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold shadow-sm transition-colors"
|
||||||
|
[ngClass]="{ 'bg-red-500 text-white': currentStep === 1, 'bg-gray-300 text-gray-600': currentStep !== 1 }">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold">ข้อมูลโครงการ</h3>
|
||||||
|
</div>
|
||||||
|
@if(currentStep > 1) {
|
||||||
|
<i class="fas fa-check-circle text-green-500 text-xl"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(currentStep === 1) {
|
||||||
|
<div class="p-6 pt-2 bg-white">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">ชื่อโครงการ <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" formControlName="prjnam"
|
||||||
|
class="w-full p-3 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 outline-none transition-all"
|
||||||
|
[ngClass]="{'border-red-500 bg-red-50': f['prjnam'] && f['prjnam'].invalid && f['prjnam'].touched}"
|
||||||
|
placeholder="ระบุชื่อโครงการของคุณ">
|
||||||
|
@if(f['prjnam'] && f['prjnam'].invalid && f['prjnam'].touched) {
|
||||||
|
<p class="text-red-500 text-xs mt-1">กรุณาระบุชื่อโครงการ</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">จำนวนเงิน (บาท) <span class="text-red-500">*</span></label>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500">฿</span>
|
||||||
|
<input type="number" formControlName="prjwntbdg"
|
||||||
|
class="w-full p-3 pl-8 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 outline-none transition-all"
|
||||||
|
[ngClass]="{'border-red-500 bg-red-50': f['prjwntbdg'] && f['prjwntbdg'].invalid && f['prjwntbdg'].touched}"
|
||||||
|
placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
@if(f['prjwntbdg'] && f['prjwntbdg'].invalid && f['prjwntbdg'].touched) {
|
||||||
|
<p class="text-red-500 text-xs mt-1">กรุณาระบุจำนวนเงิน</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-4">
|
||||||
|
<button (click)="goToStep(2)" class="px-8 py-2.5 bg-linear-to-r from-red-500 to-red-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg hover:from-red-600 hover:to-red-700 transform hover:-translate-y-0.5 transition-all duration-200">
|
||||||
|
ถัดไป <i class="fas fa-arrow-right ml-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="transition-all duration-300 ease-in-out border rounded-xl overflow-hidden mt-4"
|
||||||
|
[ngClass]="{
|
||||||
|
'border-red-500 shadow-md ring-2 ring-red-100 bg-white': currentStep === 2,
|
||||||
|
'border-gray-200 bg-gray-50 opacity-60': currentStep !== 2
|
||||||
|
}">
|
||||||
|
|
||||||
|
<div class="p-4 flex items-center justify-between"
|
||||||
|
[ngClass]="{ 'bg-red-50 text-red-700': currentStep === 2, 'text-gray-500': currentStep !== 2 }">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold shadow-sm transition-colors"
|
||||||
|
[ngClass]="{ 'bg-red-500 text-white': currentStep === 2, 'bg-gray-300 text-gray-600': currentStep !== 2 }">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold">เอกสารอ้างอิง</h3>
|
||||||
|
</div>
|
||||||
|
@if(currentStep > 2) {
|
||||||
|
<i class="fas fa-check-circle text-green-500 text-xl"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(currentStep === 2) {
|
||||||
|
<div class="p-6 pt-2 bg-white">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-gray-600 bg-blue-50 p-3 rounded-lg border border-blue-100 flex items-start">
|
||||||
|
<i class="fas fa-info-circle text-blue-500 mt-0.5 mr-2"></i>
|
||||||
|
สามารถแนบไฟล์เฉพาะไฟล์รูปภาพ, PDF หรือเอกสาร Word ที่เกี่ยวข้องเพื่อประกอบการพิจารณา
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="relative h-40 border-2 border-dashed border-gray-300 rounded-xl flex flex-col items-center justify-center text-gray-400 hover:bg-gray-50 hover:border-red-400 transition-all cursor-pointer group">
|
||||||
|
<input type="file" multiple (change)="onFileSelected($event)" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
|
||||||
|
<div class="group-hover:scale-110 transition-transform duration-200">
|
||||||
|
<i class="fas fa-cloud-upload-alt text-4xl mb-2 text-gray-300 group-hover:text-red-400"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium group-hover:text-gray-600">คลิกเพื่อเลือกไฟล์ หรือ ลากไฟล์มาวาง</span>
|
||||||
|
</div>
|
||||||
|
@if ( filePreviews.length > 0 ) {
|
||||||
|
<div class="space-y-2 mt-4">
|
||||||
|
@for (file of filePreviews; track $index; let i = $index) {
|
||||||
|
<div class="flex justify-between items-center bg-gray-50 p-3 rounded-lg border border-gray-200 hover:shadow-sm transition-shadow">
|
||||||
|
<div class="flex items-center space-x-3 overflow-hidden">
|
||||||
|
<i class="fas fa-file-alt text-red-400 text-lg"></i>
|
||||||
|
<span class="text-sm text-gray-700 truncate max-w-[200px]">{{ file.name }}</span>
|
||||||
|
<span class="text-xs text-gray-400">({{ (file.size / 1024).toFixed(2) }} KB)</span>
|
||||||
|
</div>
|
||||||
|
<button (click)="removeFile(i)" class="text-gray-400 hover:text-red-500 transition-colors p-1 rounded-full hover:bg-red-50">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex justify-between pt-6 border-t border-gray-100 mt-4">
|
||||||
|
<button (click)="goToStep(1)" class="px-6 py-2.5 text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 font-medium transition-colors">
|
||||||
|
ย้อนกลับ
|
||||||
|
</button>
|
||||||
|
<button (click)="goToStep(3)" class="px-8 py-2.5 bg-linear-to-r from-red-500 to-red-600 text-white font-semibold rounded-lg shadow-md hover:shadow-lg hover:from-red-600 hover:to-red-700 transform hover:-translate-y-0.5 transition-all duration-200">
|
||||||
|
ตรวจสอบข้อมูล <i class="fas fa-arrow-right ml-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="transition-all duration-300 ease-in-out border rounded-xl overflow-hidden mt-4"
|
||||||
|
[ngClass]="{
|
||||||
|
'border-red-500 shadow-md ring-2 ring-red-100 bg-white': currentStep === 3,
|
||||||
|
'border-gray-200 bg-gray-50 opacity-60': currentStep !== 3
|
||||||
|
}">
|
||||||
|
|
||||||
|
<div class="p-4 flex items-center justify-between"
|
||||||
|
[ngClass]="{ 'bg-red-50 text-red-700': currentStep === 3, 'text-gray-500': currentStep !== 3 }">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold shadow-sm transition-colors"
|
||||||
|
[ngClass]="{ 'bg-red-500 text-white': currentStep === 3, 'bg-gray-300 text-gray-600': currentStep !== 3 }">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold">ยืนยันการส่ง</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(currentStep === 3) {
|
||||||
|
<div class="p-6 pt-2 bg-white">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
|
<i class="fas fa-file-invoice text-2xl text-green-600"></i>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-lg font-bold text-gray-800">ตรวจสอบความถูกต้อง</h4>
|
||||||
|
<p class="text-gray-500 text-sm">กรุณาตรวจสอบข้อมูลก่อนทำการส่งขออนุมัติ</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 p-5 rounded-xl border border-gray-200 space-y-3">
|
||||||
|
<div class="flex justify-between border-b border-gray-200 pb-2">
|
||||||
|
<span class="text-gray-500 text-sm">ชื่อโครงการ</span>
|
||||||
|
<span class="font-semibold text-gray-800 text-right w-2/3">{{ f['prjnam'].value || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between border-b border-gray-200 pb-2">
|
||||||
|
<span class="text-gray-500 text-sm">จำนวนเงิน</span>
|
||||||
|
<span class="font-bold text-red-600 text-lg">{{ formatCurrency(f['prjwntbdg'].value) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between pt-1">
|
||||||
|
<span class="text-gray-500 text-sm">เอกสารแนบ</span>
|
||||||
|
<span class="font-medium text-gray-800">
|
||||||
|
<i class="fas fa-paperclip text-gray-400 mr-1"></i> {{ filePreviews.length }} ไฟล์
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between pt-4">
|
||||||
|
<button (click)="goToStep(2)" class="px-6 py-2.5 text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 font-medium transition-colors" [disabled]="isLoading">
|
||||||
|
ย้อนกลับ
|
||||||
|
</button>
|
||||||
|
<button (click)="onSubmit()" [disabled]="isLoading"
|
||||||
|
class="px-8 py-2.5 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-600 hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200 flex items-center disabled:opacity-70 disabled:cursor-not-allowed">
|
||||||
|
<span *ngIf="isLoading" class="mr-2 animate-spin"><i class="fas fa-circle-notch"></i></span>
|
||||||
|
<span *ngIf="!isLoading"><i class="fas fa-paper-plane mr-2"></i> ยืนยันและส่ง</span>
|
||||||
|
<span *ngIf="isLoading">กำลังส่ง...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { GeneralService } from './../../services/generalservice';
|
||||||
|
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-project-add',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './main-project-add.html',
|
||||||
|
styleUrl: './main-project-add.css',
|
||||||
|
})
|
||||||
|
export class MainProjectAdd implements OnInit {
|
||||||
|
// ไม่ต้องใช้ @Input() แล้ว Parent จะเข้าถึงผ่าน ViewChild
|
||||||
|
isLoading: boolean = false;
|
||||||
|
|
||||||
|
@Output() projectAddSave = new EventEmitter<any>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
currentStep: number = 1;
|
||||||
|
projectForm!: FormGroup;
|
||||||
|
// attachedFiles: any[] = [];
|
||||||
|
// ตัวแปรสำหรับเก็บไฟล์ที่ผ่านการตรวจสอบแล้ว
|
||||||
|
selectedFiles: File[] = [];
|
||||||
|
// ตัวแปรสำหรับเก็บ Preview (ถ้าต้องการแสดงผล)
|
||||||
|
filePreviews: any[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private generalService: GeneralService,
|
||||||
|
private toastr: ToastrService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setupFormControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFormControl(): void {
|
||||||
|
this.projectForm = new FormGroup({
|
||||||
|
prjnam: new FormControl('', [Validators.required, Validators.maxLength(200)]),
|
||||||
|
prjwntbdg: new FormControl('', [Validators.required, Validators.min(1)])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goToStep(step: number): void {
|
||||||
|
if (step === 2 && this.currentStep === 1) {
|
||||||
|
if (this.projectForm.invalid) {
|
||||||
|
this.projectForm.markAllAsTouched();
|
||||||
|
this.toastr.warning('กรุณากรอกข้อมูลให้ครบถ้วน', 'แจ้งเตือน');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.currentStep = step;
|
||||||
|
}
|
||||||
|
onFileSelected(event: Event): void {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (input.files && input.files.length > 0) {
|
||||||
|
// Reset ค่าเก่า (ถ้าต้องการให้เลือกใหม่ทับของเดิม) หรือจะใช้ concat ถ้าต้องการเพิ่มต่อท้าย
|
||||||
|
// this.selectedFiles = [];
|
||||||
|
// this.filePreviews = [];
|
||||||
|
|
||||||
|
Array.from(input.files).forEach((file: File) => {
|
||||||
|
|
||||||
|
// 1. Validate File Type (ตัวอย่าง: รับเฉพาะ Image และ PDF)
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
this.toastr.error('Invalid File Type', `ไฟล์ ${file.name} ไม่รองรับ (รองรับเฉพาะรูปภาพ, PDF, Word)`);
|
||||||
|
return; // ข้ามไฟล์นี้ไป
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate File Size (ตัวอย่าง: ไม่เกิน 5MB)
|
||||||
|
const maxSize = 5 * 1024 * 1024; // 5MB in bytes
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
this.toastr.error('File Too Large', `ไฟล์ ${file.name} มีขนาดใหญ่เกิน 5MB`);
|
||||||
|
return; // ข้ามไฟล์นี้ไป
|
||||||
|
}
|
||||||
|
|
||||||
|
//ผ่านการตรวจสอบ: เก็บไฟล์ลง Array เพื่อเตรียมส่ง Form Data
|
||||||
|
this.selectedFiles.push(file);
|
||||||
|
|
||||||
|
// 3. Generate Preview (Optional: สำหรับแสดงผลหน้าเว็บ)
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
this.filePreviews.push({
|
||||||
|
name: file.name,
|
||||||
|
size: this.formatBytes(file.size), // แปลง bytes เป็น KB/MB
|
||||||
|
type: file.type,
|
||||||
|
content: e.target?.result // Base64 String
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Function: แปลงขนาดไฟล์ให้อ่านง่าย
|
||||||
|
formatBytes(bytes: number, decimals = 2): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ฟังก์ชันลบไฟล์ที่เลือก (เผื่อใช้ใน UI)
|
||||||
|
removeFile(index: number): void {
|
||||||
|
this.selectedFiles.splice(index, 1);
|
||||||
|
this.filePreviews.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get f() { return this.projectForm.controls; }
|
||||||
|
|
||||||
|
formatCurrency(amount: any): string {
|
||||||
|
if (!amount) return '0.00 บาท';
|
||||||
|
return new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB' }).format(Number(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.projectForm.invalid) return;
|
||||||
|
let seq = localStorage.getItem('id');
|
||||||
|
const formData = new FormData();
|
||||||
|
const prjnam = this.projectForm.get('prjnam')?.value || '';
|
||||||
|
const prjwntbdg = this.projectForm.get('prjwntbdg')?.value;
|
||||||
|
formData.append('prjusrseq', seq ?? '');
|
||||||
|
formData.append('prjnam', prjnam);
|
||||||
|
formData.append('prjwntbdg', prjwntbdg ? prjwntbdg : '0.00');
|
||||||
|
formData.append('typ', 'prj')
|
||||||
|
|
||||||
|
for (let file of this.selectedFiles) {
|
||||||
|
formData.append('prjdoc', file); // Key ต้องชื่อ 'prjdoc' ตาม Middleware
|
||||||
|
}
|
||||||
|
this.projectAddSave.emit(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
199
ng-ttc-frontend/src/app/component/main-project/main-project.html
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<div class="mt-6">
|
||||||
|
<!-- <header class="flex justify-end items-center py-2 px-2">
|
||||||
|
|
||||||
|
</header> -->
|
||||||
|
|
||||||
|
<body class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-4">
|
||||||
|
|
||||||
|
<div class="lg:col-span-2 space-y-6">
|
||||||
|
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700 mb-4">โครงการของฉัน</h2>
|
||||||
|
|
||||||
|
<button class="flex items-center space-x-1 px-4 py-2 mb-4 text-sm font-medium text-white bg-red-900 rounded-lg shadow-md hover:bg-red-950 transition duration-150" (click)="navigate('/main/project/add')">
|
||||||
|
<span class="text-xl leading-none">+</span>
|
||||||
|
<span>เพิ่มโครงการ</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- โครงการ -->
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full text-sm border-collapse">
|
||||||
|
<thead class="bg-red-900 text-white">
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4 text-left rounded-tl-lg w-16">ลำดับ</th>
|
||||||
|
<th class="py-3 px-4 text-left">ชื่อโครงการ</th>
|
||||||
|
<th class="py-3 px-4 text-left">สถานะ</th>
|
||||||
|
<th class="py-3 px-4 text-center rounded-tr-lg w-20">ดำเนินการ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<!-- เนื้อหา -->
|
||||||
|
<tbody>
|
||||||
|
@for (item of myPrjMst; track item.prjseq; let i = $index) {
|
||||||
|
<tr class="border-b hover:bg-gray-50 transition duration-100">
|
||||||
|
<td class="py-3 px-4 text-gray-700">{{ i + 1 }}</td>
|
||||||
|
<td class="py-3 px-4 font-medium text-gray-800">{{ item.prjnam }}</td>
|
||||||
|
<td class="py-3 px-4 font-medium"
|
||||||
|
[ngClass]="{
|
||||||
|
'text-yellow-600': item.prjcomstt === 'UAC',
|
||||||
|
'text-green-600': item.prjcomstt === 'BAP',
|
||||||
|
'text-red-600': item.prjcomstt === 'CN'
|
||||||
|
}">
|
||||||
|
@if(item.prjcomstt === 'UAC'){ รออนุมัติ }
|
||||||
|
@else if(item.prjcomstt === 'BAP'){ จัดสรรงบประมาณแล้ว }
|
||||||
|
@else if(item.prjcomstt === 'CN'){ ไม่อนุมัติ }
|
||||||
|
@else { {{ item.prjcomstt }} }
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-center flex flex-row justify-center gap-2">
|
||||||
|
<button class="px-3 py-1 text-xs font-semibold text-white bg-red-900 rounded-md hover:bg-red-950">แก้ไข</button>
|
||||||
|
<button class="px-3 py-1 text-xs font-semibold text-white rounded-md transition-colors" [ngClass]="isSubscribed(item) ? 'bg-gray-500 hover:bg-gray-600' : 'bg-red-900 hover:bg-red-950'" (click)="onSubscribePrj(item)">{{ isSubscribed(item) ? 'ยกเลิก' : 'ติดตาม' }}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="py-6 text-center text-gray-500 bg-gray-50">ไม่พบข้อมูลโครงการ</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- ความคืบหน้าโครงการ -->
|
||||||
|
@for (idx of subscribeData; track $index) {
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700 mb-6">ความคืบหน้าโครงการ: {{ idx.prjnam }}</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex flex-col items-center mr-4">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-green-500 text-white flex items-center justify-center text-sm font-bold shadow-md">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-16 bg-green-500"></div> </div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-green-700">1. ยื่นคำขอโครงการ</h3>
|
||||||
|
<p class="text-sm text-gray-500">วันที่: {{ (idx.prjacpdtm ?? '-') | dtmtodatetime}}</p>
|
||||||
|
<p class="text-sm text-gray-700 mt-1">เอกสารและรายละเอียดโครงการครบถ้วน</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex flex-col items-center mr-4">
|
||||||
|
<div [ngClass]="idx.prjcomstt == 'BAP' ? 'bg-green-500 border-green-700' : 'bg-blue-500 border-blue-700'"
|
||||||
|
class="w-8 h-8 rounded-full text-white flex items-center justify-center text-sm font-bold border-2 shadow-xl">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-16 bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold" [ngClass]="idx.prjcomstt == 'BAP' ? 'text-green-700' : 'text-blue-700'">
|
||||||
|
2. พิจารณางบประมาณ
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500">กำหนดแล้วเสร็จ: {{ (idx.trnacpdtm ?? '-') | dtmtodatetime }}</p>
|
||||||
|
|
||||||
|
@if(idx.prjcomstt === 'BAP') {
|
||||||
|
<p class="text-sm text-green-600 mt-1 font-medium">ได้รับอนุมัติงบประมาณเรียบร้อยแล้ว</p>
|
||||||
|
} @else if(idx.prjcomstt === 'CN') {
|
||||||
|
<p class="text-sm text-red-600 mt-1 font-medium">โครงการไม่ผ่านการอนุมัติ</p>
|
||||||
|
} @else {
|
||||||
|
<p class="text-sm text-gray-700 mt-1 font-medium">รอฝ่ายแผนและงบประมาณอนุมัติยอดจัดสรร</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button class="mt-2 text-xs text-blue-500 hover:text-blue-700">ดูรายละเอียดการเงิน</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex flex-col items-center mr-4">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gray-300 text-gray-700 flex items-center justify-center text-sm font-bold">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-16 bg-gray-300"></div> </div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-500">3. อนุมัติโดยผู้บริหาร</h3>
|
||||||
|
<p class="text-sm text-gray-400">ยังไม่ระบุวันที่</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">รอขั้นตอนก่อนหน้าเสร็จสิ้น</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex flex-col items-center mr-4">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gray-300 text-gray-700 flex items-center justify-center text-sm font-bold">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-500">4. จัดสรรงบประมาณสำเร็จ</h3>
|
||||||
|
<p class="text-sm text-gray-400">ยังไม่ระบุวันที่</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
|
<p class="text-center text-gray-500">กรุณาเลือกโครงการที่จะแสดง</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-1 space-y-6">
|
||||||
|
|
||||||
|
<!-- <div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700 mb-4">ปฏิทินแจ้งเตือน</h2>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<div class="h-16 w-1/3 bg-gray-200 rounded-lg"></div>
|
||||||
|
<div class="h-16 w-1/3 bg-gray-200 rounded-lg"></div>
|
||||||
|
<div class="h-16 w-1/3 bg-gray-200 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-700 mb-4">กล่องข้อความ</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-11/12"></div>
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-10/12"></div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
|
<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>
|
||||||
|
</body>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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 { IPrjMst } from '../../interfaces/main.interface'; // เพิ่ม Import Interface
|
||||||
|
import { ProjectStateService } from '../../services/state/project-state.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-project',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './main-project.html',
|
||||||
|
styleUrl: './main-project.css',
|
||||||
|
})
|
||||||
|
export class MainProject implements OnInit {
|
||||||
|
|
||||||
|
myPrjMst: IPrjMst[] = []; // ตัวแปรเก็บข้อมูลโครงการ
|
||||||
|
subscribeData: IPrjMst[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private generalService: GeneralService,
|
||||||
|
private dashboardStateService: DashboardStateService,
|
||||||
|
private projectStateService: ProjectStateService, // Inject ProjectStateService
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.projectStateService.getStateResult().subscribe((result: IPrjMst[] | null) => {
|
||||||
|
if (result) {
|
||||||
|
this.myPrjMst = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// this.onSearchPrj(); // เรียกดึงข้อมูลเมื่อเริ่มทำงาน
|
||||||
|
}
|
||||||
|
|
||||||
|
// // ฟังก์ชันดึงข้อมูลโครงการ
|
||||||
|
// onSearchPrj(): void {
|
||||||
|
// const uri = '/api/ttc/projectsearch?column=user';
|
||||||
|
// const request = {}; // body ว่างตามที่ระบุ
|
||||||
|
|
||||||
|
// this.generalService.postRequest(uri, request).subscribe({
|
||||||
|
// next: (result: any) => {
|
||||||
|
// if (result.code === '200') {
|
||||||
|
// // สำเร็จ: นำข้อมูลใส่ตัวแปร
|
||||||
|
// this.myPrjMst = result.data;
|
||||||
|
// } else {
|
||||||
|
// this.generalService.trowApi(result);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// error: (error: any) => {
|
||||||
|
// this.generalService.trowApi(error);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
onSubscribePrj(item: any) {
|
||||||
|
// 1. เช็คว่า item นี้ เป็นตัวเดิมที่ติดตามอยู่แล้วหรือไม่?
|
||||||
|
const isAlreadySubscribed = this.subscribeData.some(sub => sub.prjseq === item.prjseq);
|
||||||
|
|
||||||
|
if (isAlreadySubscribed) {
|
||||||
|
// กรณีเดิม: ถ้ากดซ้ำตัวเดิม -> ให้ยกเลิกการติดตาม (เคลียร์ Array)
|
||||||
|
this.subscribeData = [];
|
||||||
|
} else {
|
||||||
|
// กรณีใหม่: ถ้ากดตัวใหม่ -> ให้แทนที่ตัวเก่าทันที (มีแค่ตัวเดียวเสมอ)
|
||||||
|
this.subscribeData = [item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Function: เช็คสถานะเพื่อเปลี่ยนสีปุ่ม
|
||||||
|
isSubscribed(item: any): boolean {
|
||||||
|
return this.subscribeData.some(sub => sub.prjseq === item.prjseq);
|
||||||
|
}
|
||||||
|
navigate(path: string) {
|
||||||
|
this.router.navigate([path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">สรุปรายงาน</p>
|
<p class="eyebrow">สรุปรายงาน</p>
|
||||||
<h1>รายงานรายรับรายจ่าย</h1>
|
<h1>รายงานรายรับรายจ่าย</h1>
|
||||||
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
|
<!-- <p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="report__actions">
|
<div class="report__actions">
|
||||||
<button class="btn btn--ghost">ส่งออกเป็น Excel</button>
|
<button class="btn btn--ghost">ส่งออกเป็น Excel</button>
|
||||||
@@ -11,21 +11,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="summary-grid">
|
<!-- <section class="summary-grid">
|
||||||
<article class="summary-card" *ngFor="let card of summaryCards">
|
<article class="summary-card" *ngFor="let card of summaryCards">
|
||||||
<p class="summary-card__label">{{ card.label }}</p>
|
<p class="summary-card__label">{{ card.label }}</p>
|
||||||
<h2>{{ card.value }}</h2>
|
<h2>{{ card.value }}</h2>
|
||||||
<p class="summary-card__detail">{{ card.detail }}</p>
|
<p class="summary-card__detail">{{ card.detail }}</p>
|
||||||
<span class="summary-card__tone" [ngClass]="'tone-' + card.tone"></span>
|
<span class="summary-card__tone" [ngClass]="'tone-' + card.tone"></span>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section> -->
|
||||||
|
|
||||||
<section class="report__content">
|
<section class="report__content">
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>สมุดรายวัน</h2>
|
<h2>รายงาน</h2>
|
||||||
<p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p>
|
<!-- <p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p> -->
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--compact btn--ghost">กรองข้อมูล</button>
|
<button class="btn btn--compact btn--ghost">กรองข้อมูล</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Print Preview</p>
|
<p class="eyebrow">Print Preview</p>
|
||||||
<h2>รายงานรายรับรายจ่าย</h2>
|
<h2>รายงานรายรับรายจ่าย</h2>
|
||||||
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
|
<!-- <p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-modal__actions">
|
<div class="preview-modal__actions">
|
||||||
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
|
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
|
||||||
|
|||||||
@@ -1,65 +1,89 @@
|
|||||||
<div
|
<div class="h-full bg-white border-r border-gray-200 flex flex-col shadow-sm transition-all duration-300">
|
||||||
class="h-screen bg-linear-to-b from-amber-950 to-amber-900 text-gray-100 shadow-2xl flex flex-col relative transition-all duration-300 ease-in-out"
|
|
||||||
[@sidebarState]="isOpen ? 'expanded' : 'collapsed'">
|
|
||||||
|
|
||||||
<button
|
<!-- ส่วน Logo เดิม (เอาออก หรือ Comment ไว้) -->
|
||||||
(click)="toggleSidebar()"
|
<!--
|
||||||
class="absolute -right-3 top-6 bg-amber-700 hover:bg-amber-600 text-white rounded-full p-2 shadow-md transition-all duration-300">
|
<div class="h-16 flex items-center justify-center border-b border-gray-100 shrink-0">
|
||||||
<i class="fas" [ngClass]="isOpen ? 'fa-angle-left' : 'fa-angle-right'"></i>
|
...
|
||||||
</button>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<div class="flex items-center gap-3 p-5">
|
<!-- User Profile Summary (ปรับให้เป็นส่วนบนสุดแทน หรือเอาออกถ้าจะย้ายไป Navbar) -->
|
||||||
<h3 *ngIf="isOpen" class="text-2xl font-bold transition-all duration-300">
|
<!-- ถ้าจะเอาออกด้วย ให้ลบส่วนนี้ทิ้งครับ แต่ถ้ายังอยากเก็บไว้ ก็ปล่อยไว้ -->
|
||||||
Global Sidebar
|
<div *ngIf="!isCollapsed" class="p-4 border-b border-gray-50 bg-gray-50/50 shrink-0 transition-all duration-300">
|
||||||
</h3>
|
<div class="flex items-center gap-3">
|
||||||
</div>
|
<div
|
||||||
|
class="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center text-red-500 font-bold border border-red-200 shadow-sm shrink-0">
|
||||||
|
<span *ngIf="!userData?.avatar">{{ (userData?.name?.charAt(0) || 'U') | uppercase }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<p class="text-sm font-semibold text-gray-700 truncate">{{ userData?.name || 'User Name' }}</p>
|
||||||
|
<p class="text-xs text-gray-500 truncate">
|
||||||
|
{{ userData?.role === 'U' ? 'ผู้ยืนโครงการ' : 'ผู้อนุมัติโครงการ' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="border-t border-amber-700 mx-4 my-4 opacity-70">
|
<!-- Navigation Menu (เพิ่ม padding-top ถ้าเอา Header ออก) -->
|
||||||
|
<nav class="flex-1 overflow-y-auto py-4 px-2 space-y-1 custom-scrollbar pt-6">
|
||||||
|
|
||||||
<ul class="flex flex-col gap-2 px-2 grow">
|
<!-- Dashboard -->
|
||||||
|
<a routerLink="/main" routerLinkActive="bg-red-50 text-red-600 shadow-sm ring-1 ring-red-100"
|
||||||
|
[routerLinkActiveOptions]="{exact: true}"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-all group"
|
||||||
|
[title]="isCollapsed ? 'Dashboard' : ''">
|
||||||
|
<div class="w-6 flex justify-center shrink-0">
|
||||||
|
<i class="fas fa-th-large text-lg group-hover:scale-110 transition-transform"></i>
|
||||||
|
</div>
|
||||||
|
<span *ngIf="!isCollapsed" class="font-medium whitespace-nowrap">หน้าหลัก</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
<!-- Section Header -->
|
||||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
<div *ngIf="!isCollapsed"
|
||||||
(click)="navigate('/main/dashboard')">
|
class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider whitespace-nowrap">
|
||||||
<i class="fas fa-tachometer-alt text-xl group-hover:scale-110 transition-transform"></i>
|
Management
|
||||||
<span *ngIf="isOpen" class="text-lg font-medium">Dashboard</span>
|
</div>
|
||||||
</li>
|
<div *ngIf="isCollapsed" class="h-4"></div>
|
||||||
|
|
||||||
<!-- <li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
<!-- Projects -->
|
||||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
<a *ngIf="userData?.role === 'U'" routerLink="/main/project" routerLinkActive="bg-red-50 text-red-600 shadow-sm ring-1 ring-red-100"
|
||||||
(click)="navigate('/main/profile')">
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-all group"
|
||||||
<i class="fas fa-user-circle text-xl group-hover:scale-110 transition-transform"></i>
|
[title]="isCollapsed ? 'Projects' : ''">
|
||||||
<span *ngIf="isOpen" class="text-lg font-medium">Profile</span>
|
<div class="w-6 flex justify-center shrink-0">
|
||||||
</li> -->
|
<i class="fas fa-folder text-lg group-hover:scale-110 transition-transform"></i>
|
||||||
|
</div>
|
||||||
|
<span *ngIf="!isCollapsed" class="font-medium whitespace-nowrap">โครงการของฉัน</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
<!-- Budgets -->
|
||||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
<!-- <a routerLink="/main/budget" routerLinkActive="bg-red-50 text-red-600 shadow-sm ring-1 ring-red-100"
|
||||||
(click)="navigate('/main/report')">
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-all group"
|
||||||
<i class="fas fa-chart-bar text-xl group-hover:scale-110 transition-transform"></i>
|
[title]="isCollapsed ? 'Budgets' : ''">
|
||||||
<span *ngIf="isOpen" class="text-lg font-medium">Report</span>
|
<div class="w-6 flex justify-center shrink-0">
|
||||||
</li>
|
<i class="fas fa-file-invoice-dollar text-lg group-hover:scale-110 transition-transform"></i>
|
||||||
|
</div>
|
||||||
|
<span *ngIf="!isCollapsed" class="font-medium whitespace-nowrap">Budgets</span>
|
||||||
|
</a> -->
|
||||||
|
|
||||||
|
<!-- Users -->
|
||||||
|
<a *ngIf="userData?.role === 'D'" routerLink="/main/manager"
|
||||||
|
routerLinkActive="bg-red-50 text-red-600 shadow-sm ring-1 ring-red-100"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-all group"
|
||||||
|
[title]="isCollapsed ? 'Users' : ''">
|
||||||
|
<div class="w-6 flex justify-center shrink-0">
|
||||||
|
<i class="fas fa-users-cog text-lg group-hover:scale-110 transition-transform"></i>
|
||||||
|
</div>
|
||||||
|
<span *ngIf="!isCollapsed" class="font-medium whitespace-nowrap">จัดสรรงบประมาณ</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
<!-- Footer / Logout -->
|
||||||
|
<div class="p-4 border-t border-gray-200 bg-gray-50 shrink-0">
|
||||||
|
<button (click)="logout()"
|
||||||
|
class="flex items-center justify-center gap-2 w-full px-4 py-2 text-sm font-medium text-white bg-red-500 rounded-lg hover:bg-red-600 transition-colors shadow-sm hover:shadow">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
<span *ngIf="!isCollapsed">Logout</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer mt-auto
|
|
||||||
hover:bg-red-700 hover:shadow-lg transition-all duration-300 ease-in-out"
|
|
||||||
(click)="logout()">
|
|
||||||
<i class="fas fa-sign-out-alt text-xl group-hover:scale-110 transition-transform"></i>
|
|
||||||
<span *ngIf="isOpen" class="text-lg font-medium text-red-200">Logout</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
*ngIf="isMobile && showOverlay"
|
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300"
|
|
||||||
(click)="toggleSidebar()">
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
|
|
||||||
<div class="flex-1 bg-gray-100 text-gray-900 overflow-y-auto">
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,25 @@
|
|||||||
import { Component, HostListener, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sidebar',
|
selector: 'app-sidebar',
|
||||||
standalone: false,
|
standalone: false,
|
||||||
templateUrl: './sidebar.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrls: ['./sidebar.component.css'],
|
styleUrls: ['./sidebar.component.css']
|
||||||
animations: [
|
|
||||||
trigger('sidebarState', [
|
|
||||||
state('expanded', style({
|
|
||||||
width: '220px',
|
|
||||||
opacity: 1
|
|
||||||
})),
|
|
||||||
state('collapsed', style({
|
|
||||||
width: '70px',
|
|
||||||
opacity: 0.95
|
|
||||||
})),
|
|
||||||
transition('expanded <=> collapsed', [
|
|
||||||
animate('300ms ease-in-out')
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class SidebarComponent implements OnInit {
|
export class SidebarComponent implements OnInit {
|
||||||
isOpen = true; // ขยายไหม
|
@Input() isCollapsed: boolean = false; // รับค่าสถานะย่อ/ขยาย
|
||||||
isMobile = false; // ตรวจอุปกรณ์
|
|
||||||
showOverlay = false; // สำหรับ mobile overlay
|
|
||||||
|
|
||||||
constructor(private router: Router) {}
|
userData: any = {
|
||||||
|
name: localStorage.getItem('usrthinam') + ' ' + localStorage.getItem('usrthilstnam'),
|
||||||
|
role: localStorage.getItem('usrrol'),
|
||||||
|
avatar: ''
|
||||||
|
};
|
||||||
|
|
||||||
ngOnInit() {
|
constructor(private router: Router) { }
|
||||||
this.checkDevice();
|
|
||||||
window.addEventListener('resize', () => this.checkDevice());
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('window:resize')
|
ngOnInit(): void {
|
||||||
checkDevice() {
|
// โหลด User Data ถ้ามี
|
||||||
this.isMobile = window.innerWidth <= 768;
|
|
||||||
if (this.isMobile) {
|
|
||||||
this.isOpen = false; // ซ่อน sidebar ตอนเข้า mobile
|
|
||||||
} else {
|
|
||||||
this.isOpen = true; // เปิดไว้ตลอดใน desktop
|
|
||||||
this.showOverlay = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSidebar() {
|
|
||||||
if (this.isMobile) {
|
|
||||||
this.showOverlay = !this.showOverlay;
|
|
||||||
this.isOpen = this.showOverlay;
|
|
||||||
} else {
|
|
||||||
this.isOpen = !this.isOpen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(path: string) {
|
|
||||||
this.router.navigate([path]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>theme-switcher works!</p>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemeService } from '../../services/theme.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-theme-switcher',
|
||||||
|
template: `
|
||||||
|
<div class="flex items-center gap-2 p-2 bg-white rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<span class="text-sm font-medium text-gray-600">Theme:</span>
|
||||||
|
|
||||||
|
<!-- ปุ่มสีแดง -->
|
||||||
|
<button (click)="changeTheme('theme-red')"
|
||||||
|
class="w-6 h-6 rounded-full bg-red-900 border-2 border-white shadow-sm hover:scale-110 transition-transform ring-1 ring-gray-200"
|
||||||
|
[class.ring-red-500]="currentTheme === 'theme-red'">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ปุ่มสีน้ำเงิน -->
|
||||||
|
<button (click)="changeTheme('theme-blue')"
|
||||||
|
class="w-6 h-6 rounded-full bg-blue-900 border-2 border-white shadow-sm hover:scale-110 transition-transform ring-1 ring-gray-200"
|
||||||
|
[class.ring-blue-500]="currentTheme === 'theme-blue'">
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ปุ่มสีเขียว -->
|
||||||
|
<button (click)="changeTheme('theme-green')"
|
||||||
|
class="w-6 h-6 rounded-full bg-green-900 border-2 border-white shadow-sm hover:scale-110 transition-transform ring-1 ring-gray-200"
|
||||||
|
[class.ring-green-500]="currentTheme === 'theme-green'">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class ThemeSwitcherComponent {
|
||||||
|
currentTheme: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private themeService: ThemeService
|
||||||
|
) {
|
||||||
|
this.currentTheme = this.themeService.getCurrentTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTheme(theme: string) {
|
||||||
|
this.themeService.setTheme(theme);
|
||||||
|
this.currentTheme = theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.token-timer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- <p>หมดอายุใน: {{ countdown }}</p> -->
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { JwtService } from '../../services/jwt.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-token-timer',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './token-timer.component.html',
|
||||||
|
styleUrls: ['./token-timer.component.css']
|
||||||
|
})
|
||||||
|
export class TokenTimerComponent implements OnInit {
|
||||||
|
countdown$: Observable<string | null>;
|
||||||
|
|
||||||
|
constructor(private jwtService: JwtService) {
|
||||||
|
this.countdown$ = this.jwtService.countdown$;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
}
|
||||||