-accouting Wep
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 8m54s

This commit is contained in:
x2Skyz
2025-11-21 15:01:21 +07:00
parent 1c7729ab99
commit 45259f7b8d
21 changed files with 590 additions and 350 deletions

View File

@@ -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", "avon-reid-link-statistics.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"
] ]
} }
} }

View File

@@ -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",

View File

@@ -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,

View File

@@ -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;
align-items: center;
gap: 12px;
margin-top: 6px;
flex-wrap: wrap;
}
.biometric{
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: transparent; background: transparent;
color: var(--primary); color: var(--primary);
border: 1px solid rgba(0,120,212,0.14); border: 1px solid var(--primary);
border-radius: 6px; box-shadow: none;
cursor: pointer; transition: background-color .14s ease;
font-weight: 600;
font-size: 13px;
} }
.biometric svg{ display: block; opacity: .95; } .primary.secondary-button:hover {
.biometric:hover{ background: rgba(0, 120, 212, 0.05);
background: rgba(0,120,212,0.04); transform: none;
box-shadow: none;
} }
/* Help link */
.help-link{
margin-left: auto;
font-size: 13px;
color: var(--primary);
text-decoration: none;
}
.help-link:hover{ text-decoration: underline; }
/* Footer */ /* Footer (unchanged) */
.footer{ .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; }
} }

View File

@@ -1,52 +1,38 @@
<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"> <div class="brand">
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
<img src="logo.png" alt="Company logo" class="logo mb-2"/> <img src="logo.png" alt="Company logo" class="logo mb-2"/>
<h1 id="signin-title" class='kanit-bold'>ลืมรหัสผ่าน</h1> <h1 id="signin-title" class="kanit-bold">ลืมรหัสผ่าน</h1>
<p class="subtitle">โปรดกรอก Email ของท่าน</p> <p class="subtitle">โปรดกรอก Email ของท่าน</p>
</div> </div>
<form [formGroup]="forgotFrm" class="form px-3 pb-3 login-mobile">
<form [formGroup]="forgotFrm" class="form">
<label class="field"> <label class="field">
<span class="label-text">อีเมล์</span> <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 /> <input type="email" formControlName="email" class="input-field" id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
</label> </label>
@if (isSendOtp === true) { @if (isSendOtp === true) {
<label class="field"> <label class="field">
<span class="label-text">รหัสยืนยัน OTP</span> <span class="label-text">รหัสยืนยัน OTP</span>
<input type="email" formControlName="otp" autocomplete="otp" placeholder="123456" alt required/> <input type="text" formControlName="otp" class="input-field" autocomplete="one-time-code" placeholder="123456" alt required/>
</label> </label>
} }
<!-- <div class="justify-end flex"> -->
<!-- <label class="stay-signed"> <div class="actions justify-end-custom mt-4">
<input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span> <div class="flex-row hover-lift">
</label> --> <button class="primary secondary-button" type="button" (click)="isModalOpen = true">
<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 เปิด Modal
</button> </button>
</div> </div>
@if (isSendOtp === false) { @if (isSendOtp === false) {
<div class="flex justify-end"> <div class="flex justify-end">
@if (isLoading === true) { @if (isLoading === true) {
<button type="submit" class="primary cursor-progress!" disabled> <button type="submit" class="primary" disabled>
กำลังส่ง... กำลังส่ง...
</button> </button>
} @else { } @else {
@@ -57,7 +43,7 @@
</div> </div>
} @else if(isSendOtp === true) { } @else if(isSendOtp === true) {
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button type="button" class="primary" (click)="onSubmin()"> <button type="button" class="primary secondary-button" (click)="onSubmin()">
{{ 'ส่งอีกครั้ง' }} {{ 'ส่งอีกครั้ง' }}
</button> </button>
<button type="submit" class="primary" (click)="onVerifySubmit()"> <button type="submit" class="primary" (click)="onVerifySubmit()">
@@ -66,37 +52,66 @@
</div> </div>
} }
</div> </div>
<!-- <button mat-raised-button color="primary" [disabled]="isLoading">
{{ isLoading ? 'กำลังส่ง...' : 'ส่งรหัส OTP' }}
</button> -->
<!-- } -->
<!-- </div> -->
</form> </form>
@if(isModalOpen){ @if(isModalOpen){
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50" [formGroup]="forgotFrm"> <div class="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true" [formGroup]="forgotFrm">
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-fit">
<h2 class="text-xl font-bold mb-4">เปลี่ยนรหัสผ่าน</h2> <div class="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity" aria-hidden="true"></div>
<hr class="w-full h-1 bg-gray-300 rounded-sm shadow-neutral-400 md:my-1">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">รหัสผ่านใหม่</label> <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<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="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>
<div class="mb-4"> <div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ืนยันรหัสผ่านใหม่</label> <h3 class="text-xl font-bold leading-6 text-gray-900" id="modal-title">เปลี่ยนรหัสผ่าน</h3>
<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="กรอกยืนยันรหัสผ่านใหม่"> <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 ){ @if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
<span class="text-red-600 md">รหัสผ่านไม่ตรงกัน</span> <div class="flex items-center text-red-600 text-sm mt-2 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>รหัสผ่านไม่ตรงกัน</span>
</div>
} }
</div> </div>
<!-- <hr class="w-full h-[] bg-gray-100 border-0 rounded-sm md:my-1 dark:bg-gray-700"> --> </div>
<div class="flex justify-end gap-2"> </div>
<button class="bg-red-500 text-white px-4 py-2 rounded" (click)="isModalOpen = false">
ปิด <div class="bg-gray-50 px-6 py-4 sm:flex sm:flex-row-reverse sm:px-8 rounded-b-2xl">
<button type="button" class="inline-flex w-full justify-center rounded-xl bg-red-900 px-5 py-3 text-sm font-semibold text-white shadow-sm hover:bg-red-950 sm:ml-3 sm:w-auto transition-colors" (click)="onSetNewPassword()">
ยืนยันการเปลี่ยนแปลง
</button> </button>
<button class="bg-green-500 text-white px-4 py-2 rounded shadow-[0_1px_18px_var(--color-green-300)] hover:-translate-y-1.5 hover:shadow-[0_6px_18px_var(--color-green-500)] transition-all duration-500 ease-in-out" (click)="onSetNewPassword()"> <button type="button" class="mt-3 inline-flex w-full justify-center rounded-xl bg-white px-5 py-3 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors" (click)="isModalOpen = false">
ยืนยัน ยกเลิก
</button> </button>
</div> </div>
</div>
</div> </div>
</div> </div>
} }

View File

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

View File

@@ -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"> <div class="brand">
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> --> <img src="/logo.png" alt="Company logo" class="logo"/>
<img src="logo.png" alt="Company logo" class="logo mb-2"/> <h1 id="signin-title" class="kanit-bold">เข้าสู่ระบบ</h1>
<h1 id="signin-title" class='kanit-bold'>เข้าสู่ระบบ</h1> <p class="subtitle">บัญชีโปรแกรมจัดการแผนงานงบประมาณของท่าน</p>
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของคุณ</p>
</div> </div>
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form px-3 pb-3">
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form">
<label class="field"> <label class="field">
<span class="label-text">อีเมล์</span> <span class="label-text">อีเมล์</span>
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required /> <input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required class="input-field" />
</label> </label>
<label class="field"> <label class="field mt-3">
<span class="label-text">รหัสผ่าน</span> <span class="label-text">รหัสผ่าน</span>
<input type="password" formControlName="password" autocomplete="current-password" required /> <input type="password" formControlName="password" autocomplete="current-password" required class="input-field" />
</label> </label>
<div class="actions"> <div class="actions d-flex justify-content-between align-items-center mt-4">
<label class="stay-signed"> <label class="stay-signed">
<input type="checkbox" formControlName="remember" /> <input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span> <span>จดจำรหัสผ่าน</span>
</label> </label>
<!-- <fa-icon [icon]="faCoffee" /> --> <button type="submit" class="primary login-button"
<button type="submit" class="primary" [disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)"> [disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
เข้าสู่ระบบ เข้าสู่ระบบ
</button> </button>
</div> </div>
<div class="alt-options"> <div class="alt-options mt-4 text-center">
<button type="button" class="biometric" (click)="useBiometric()"> <button type="button" class="biometric btn-icon" (click)="useBiometric()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"> <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="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="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"/> <path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </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>
<a class="help-link" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
</div> </div>
<div class="footer"> <div class="footer mt-5 text-center">
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a> <a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
<span class="divider"></span> <span class="divider mx-2"></span>
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a> <a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -0,0 +1,67 @@
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4 sm:p-6">
<div class="bg-white p-8 sm:p-10 rounded-xl shadow-2xl w-full max-w-md border border-gray-200">
<h1 class="text-3xl font-bold text-gray-800 mb-2">สร้างบัญชีใหม่</h1>
<p class="text-gray-500 mb-8">กรอกข้อมูลเพื่อเริ่มต้นใช้งานระบบ</p>
<form class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="firstName" class="block text-sm font-medium text-gray-700 mb-1">ชื่อจริง</label>
<input type="text" id="firstName" name="firstName" required
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm"
placeholder="เช่น สมชาย">
</div>
<div>
<label for="lastName" class="block text-sm font-medium text-gray-700 mb-1">นามสกุล</label>
<input type="text" id="lastName" name="lastName" required
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm"
placeholder="เช่น ใจดี">
</div>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">อีเมล</label>
<input type="email" id="email" name="email" required
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm"
placeholder="you@example.com">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">รหัสผ่าน</label>
<input type="password" id="password" name="password" required
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm"
placeholder="********">
</div>
<div>
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ยืนยันรหัสผ่าน</label>
<input type="password" id="confirmPassword" name="confirmPassword" required
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-red-500 focus:border-red-500 shadow-sm"
placeholder="********">
</div>
<div class="flex items-start">
<input type="checkbox" id="terms" name="terms" required
class="mt-1 h-4 w-4 text-red-600 border-gray-300 rounded focus:ring-red-500">
<label for="terms" class="ml-2 block text-sm text-gray-900">
ฉันยอมรับ <a href="#" class="font-medium text-red-600 hover:text-red-500">ข้อตกลงและเงื่อนไข</a>
</label>
</div>
<div>
<button type="submit"
class="w-full py-3 bg-red-600 text-white font-semibold rounded-lg shadow-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150">
สมัครสมาชิก
</button>
</div>
</form>
<p class="mt-6 text-center text-sm text-gray-600">
เป็นสมาชิกอยู่แล้วใช่ไหม?
<a href="#" class="font-medium text-red-600 hover:text-red-500">เข้าสู่ระบบ</a>
</p>
</div>
</div>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-login-register',
standalone: false,
templateUrl: './login-register.component.html',
styleUrl: './login-register.component.css',
})
export class LoginRegisterComponent {
}

View File

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

View File

@@ -19,14 +19,14 @@
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition"> <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-gray-500 text-sm">รายรับทั้งหมด</div>
<div class="text-3xl font-bold text-green-600 mt-1"> <div class="text-3xl font-bold text-green-600 mt-1">
{{ 11000 | number:'1.0-2' }} บาท {{ myActSumData.summary.totalIncome || 0 | number:'1.0-2' }} บาท
</div> </div>
</div> </div>
<div class="bg-white border border-gray-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition"> <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-gray-500 text-sm">รายจ่ายทั้งหมด</div>
<div class="text-3xl font-bold text-red-600 mt-1"> <div class="text-3xl font-bold text-red-600 mt-1">
{{ 1000 | number:'1.0-2' }} บาท {{ myActSumData.summary.totalExpense || 0 | number:'1.0-2' }} บาท
</div> </div>
</div> </div>
@@ -34,9 +34,9 @@
<div class="text-gray-500 text-sm">คงเหลือ</div> <div class="text-gray-500 text-sm">คงเหลือ</div>
<div <div
class="text-3xl font-bold mt-1" class="text-3xl font-bold mt-1"
[ngClass]="100 >= 0 ? 'text-blue-600' : 'text-red-600'" [ngClass]="myActSumData.summary.netProfit >= 0 ? 'text-blue-600' : 'text-red-600'"
> >
{{ 10000 | number:'1.0-2' }} บาท {{ myActSumData.summary.netProfit | number:'1.0-2' }} บาท
</div> </div>
</div> </div>
@@ -81,63 +81,129 @@
</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> </div>
<form class="quick-log__form">
<!-- เปลี่ยน form ด้านในเป็น div แทน เพื่อไม่ให้ form ซ้อน form -->
<div class="quick-log__form">
<!-- 1. ส่วนเลือกประเภท (รายรับ/รายจ่าย) -->
<label> <label>
<span>ประเภท</span> <span>ประเภท</span>
<div class="quick-log__toggle"> <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 == 'i' }" (click)="mode = 'i'; saveFrm.get('actcat')?.reset()">รายรับ</button>
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'">รายจ่าย</button> <button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'; saveFrm.get('actcat')?.reset()">รายจ่าย</button>
</div> </div>
</label> </label>
<label>
<span>วันที่</span>
<!-- <input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/> -->
<input type="datetime-local"/> <!-- 2. ส่วนวันที่ -->
</label> <div class="mb-2">
<div class="quick-log__grid">
<label> <label>
<span>หมวดหมู่</span> <span>วันที่ <span class="text-red-500">*</span></span>
<input type="datetime-local" formControlName="actacpdtm"
[class.border-red-500]="saveFrm.get('actacpdtm')?.invalid && (saveFrm.get('actacpdtm')?.value || saveFrm.get('actacpdtm')?.touched)"/>
</label>
<!-- Validate วันที่ -->
@if (saveFrm.get('actacpdtm')?.invalid && (saveFrm.get('actacpdtm')?.value || saveFrm.get('actacpdtm')?.touched)) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>กรุณาระบุวันที่</span>
</div>
}
</div>
<div class="quick-log__grid">
<!-- 3. ส่วนหมวดหมู่ -->
<div class="flex flex-col">
<label class="h-full">
<span>หมวดหมู่ <span class="text-red-500">*</span></span>
@if(mode == 'i'){ @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"
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="">ไม่เลือก</option> <option value="">ไม่เลือก</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"
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="">ไม่เลือก</option> <option value="">ไม่เลือก</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>
<label>
<span>ยอดเงิน (฿)</span> <!-- Validate หมวดหมู่ -->
<input type="number" placeholder="0.00" /> @if (saveFrm.get('actcat')?.invalid && (saveFrm.get('actcat')?.dirty || saveFrm.get('actcat')?.touched)) {
</label> <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>
<!-- 4. ส่วนยอดเงิน -->
<div class="flex flex-col">
<label>
<span>ยอดเงิน (฿) <span class="text-red-500">*</span></span>
<input type="number" formControlName="actqty" placeholder="0.00"
[class.border-red-500]="saveFrm.get('actqty')?.invalid && (saveFrm.get('actqty')?.dirty || saveFrm.get('actqty')?.touched)"/>
</label>
<!-- Validate ยอดเงิน -->
@if (saveFrm.get('actqty')?.invalid && (saveFrm.get('actqty')?.dirty || saveFrm.get('actqty')?.touched)) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>กรุณาระบุยอดเงิน</span>
</div>
}
</div>
</div>
<!-- 5. ส่วนบันทึกเพิ่มเติม -->
<div class="mb-2">
<label> <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>
<!-- Validate ความยาวตัวอักษร (ถ้ามี) -->
@if (saveFrm.get('actcmt')?.hasError('maxlength')) {
<div class="flex items-center text-red-600 text-xs mt-1 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3 h-3 mr-1">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
<span>พิมพ์ได้สูงสุด 200 ตัวอักษร</span>
</div>
}
</div>
<button type="button" class="btn btn--primary" (click)="onSaveSubmit()">บันทึกรายการ</button>
</div>
</form> </form>
</article>
<article class="panel ledger-panel"> <article class="panel ledger-panel">
<div class="panel__header"> <div class="panel__header">
@@ -201,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">
@@ -248,7 +314,7 @@
</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>
@@ -265,8 +331,38 @@
</div> </div>
</div> </div>
</div> </div>
</article> </article> -->
<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"> <!-- <article class="panel alerts-panel">
<div class="panel__header"> <div class="panel__header">
<div> <div>

View File

@@ -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,10 +35,11 @@ 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
@@ -83,12 +85,19 @@ 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)
// เพิ่มใน Class Component
isNumber(val: any): boolean {
return typeof val === 'number';
}
readonly periodSummaries = [ readonly periodSummaries = [
{ {
label: 'รายปี', label: 'รายปี',
@@ -217,8 +226,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 +241,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(){

View File

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

View File

@@ -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,28 @@ export class LoginContentComponent implements OnInit {
}); });
} }
onRegisterSubmit(value: any){
const uri = '/api/login/register';
const request = {
username: value.username,
password: value.password,
email: value.email
};
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') { }
else {
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
}
});
}
onOtpSendSubmit(value: any){ onOtpSendSubmit(value: any){
let uri = '/api/login/otp/send'; let uri = '/api/login/otp/send';

View File

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

View File

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

View File

@@ -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,

View File

@@ -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,12 @@ export interface IActCategory {
color: string; color: string;
} }
export interface QuickRatio {
label: string;
value: string | number;
colorClass: string; // ตัวเก็บชื่อ class สี
}
// ข้อมูลสินค้าหลัก // ข้อมูลสินค้าหลัก
// export interface IProduct { // export interface IProduct {
// id: string; // id: string;

View File

@@ -1,4 +1,4 @@
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: 'http://localhost:8000' apiBaseUrl: 'https://magnitude-dawn-generic-refuse.trycloudflare.com'
}; };