-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": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
},
"configurations": {
@@ -86,7 +84,7 @@
"builder": "@angular/build:dev-server",
"options": {
"host": "0.0.0.0",
"allowedHosts": ["accounting.nuttakit.work", "localhost"]
"allowedHosts": ["accounting.nuttakit.work", "localhost", "avon-reid-link-statistics.trycloudflare.com"]
},
"configurations": {
"production": {
@@ -116,12 +114,10 @@
}
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
}
}

View File

@@ -58,7 +58,6 @@
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@tailwindcss/postcss": "^4.1.16",
"bootstrap": "^5.3.8",
"chart.js": "^4.5.1",
"dotenv": "^17.2.3",
"jwt-decode": "^4.0.0",

View File

@@ -23,6 +23,7 @@ import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { LoginRegisterComponent } from './component/login-register/login-register.component';
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
@@ -33,6 +34,7 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
SidebarContentComponent,
SidebarComponent,
LicensePrivacyTermsComponent,
// LoginRegisterComponent,
// AccDateFormatPipe
// DtmtodatetimePipe,
// MainDashboardContentComponent,

View File

@@ -8,12 +8,11 @@
--radius: 8px;
--shadow: 0 10px 30px rgba(11,26,43,0.08);
--glass: rgba(255,255,255,0.6);
--success-color: #10b981; /* Green for success/confirm */
}
/* Page layout */
/* Page layout (unchanged) */
.login-widget {
/* Fill the viewport and center the card. Do not let the page itself
scroll; the card gets an internal max-height instead. */
min-height: 100vh;
height: 100vh;
display: flex;
@@ -24,8 +23,7 @@
color: var(--text);
}
/* Card */
/* Card (unchanged) */
.login-widget .card{
width: 380px;
max-width: calc(100% - 40px);
@@ -37,16 +35,14 @@
display: flex;
flex-direction: column;
gap: 14px;
/* Constrain the card so it never forces the page to scroll. If content
grows, the card will scroll internally. */
max-height: calc(100vh - 56px);
overflow: auto;
}
/* Modal/backdrop styles */
/* Modal/backdrop styles (unchanged) */
.login-backdrop{
position: fixed;
inset: 0; /* top:0; right:0; bottom:0; left:0; */
inset: 0;
background: rgba(0,0,0,0.38);
display: flex;
align-items: center;
@@ -54,43 +50,19 @@
z-index: 1040;
padding: 24px;
}
.login-modal{ width: 480px; max-width: 480px; }
.modal-card{
border-radius: 12px;
padding: 0; /* card children control internal padding */
padding: 0;
overflow: hidden;
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
}
/* Slightly larger brand area inside modal */
.modal-card .brand{ padding: 18px; }
/* Make the primary button pill-shaped and slightly larger */
button.primary{
color: #000;
border-radius: 999px;
padding: 10px 18px;
font-size: 15px;
}
/* Make biometric and other action buttons visually lighter */
.biometric{
border-radius: 999px;
padding: 8px 12px;
}
/* On small screens reduce modal padding and width to avoid overflow */
@media (max-width: 420px){
.login-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
}
/* Brand area */
/* Brand area (unchanged) */
.brand{
text-align: center;
padding: 18px;
padding-bottom: 4px;
border-bottom: 1px solid #eef2f5;
}
@@ -116,16 +88,13 @@ button.primary{
/* Form area */
.form{
/* keep compact spacing inside the card */
/* width: 410px; */
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 6px 0 2px;
padding: 6px 22px 22px 22px;
}
/* Field label wrapper */
/* Field label wrapper (unchanged) */
.field{
display: flex;
flex-direction: column;
@@ -135,11 +104,11 @@ button.primary{
font-size: 13px;
color: var(--muted);
}
/* Inputs */
/* Inputs (class 'input-field' added to HTML) */
input[type="email"],
input[type="password"],
input[type="text"]{
input[type="text"],
.input-field { /* เพิ่ม class input-field เพื่อให้สไตล์ถูกใช้กับ input ที่กำหนด */
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
@@ -163,31 +132,30 @@ input:focus{
transform: translateZ(0);
}
/* Checkbox / stay signed */
.stay-signed{
display: inline-flex;
gap: 8px;
align-items: center;
font-size: 13px;
color: var(--muted);
}
.stay-signed input[type="checkbox"]{
width: 16px;
height: 16px;
accent-color: var(--primary);
}
/* Actions row */
.actions{
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 4px;
flex-direction: row-reverse;
}
/* Custom class for justify-end when using flex-row-reverse */
.actions.justify-end-custom {
justify-content: flex-start;
}
/* Custom class for lift hover effect (used for 'เปิด Modal' button) */
.hover-lift:hover {
transform: translateY(-2.5px);
transition: transform .2s ease;
}
/* PRIMARY BUTTON - แก้ไขสีข้อความให้เป็นสีดำ */
button.primary{
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
color: #000000;
color: #000000; /* ⬅️ แก้ไขเป็นสีดำตามคำขอ */
border: none;
padding: 10px 14px;
border-radius: 6px;
@@ -197,6 +165,7 @@ button.primary{
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
}
button.primary:hover:not(:disabled){
transform: translateY(-1px);
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
@@ -207,45 +176,26 @@ button.primary:active{
button.primary:disabled{
opacity: 0.55;
cursor: not-allowed;
color: #000000; /* ข้อความ disabled ก็เป็นสีดำ */
box-shadow: none;
}
/* Alternative options */
.alt-options{
display: flex;
align-items: center;
gap: 12px;
margin-top: 6px;
flex-wrap: wrap;
/* Secondary Button Style (สำหรับปุ่ม 'เปิด Modal', 'ส่งอีกครั้ง') */
.primary.secondary-button {
background: transparent;
color: var(--primary);
border: 1px solid var(--primary);
box-shadow: none;
transition: background-color .14s ease;
}
.biometric{
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: transparent;
color: var(--primary);
border: 1px solid rgba(0,120,212,0.14);
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
}
.biometric svg{ display: block; opacity: .95; }
.biometric:hover{
background: rgba(0,120,212,0.04);
.primary.secondary-button:hover {
background: rgba(0, 120, 212, 0.05);
transform: none;
box-shadow: none;
}
/* Help link */
.help-link{
margin-left: auto;
font-size: 13px;
color: var(--primary);
text-decoration: none;
}
.help-link:hover{ text-decoration: underline; }
/* Footer */
/* Footer (unchanged) */
.footer{
display: flex;
justify-content: center;
@@ -262,13 +212,9 @@ button.primary:disabled{
text-decoration: none;
font-weight: 600;
}
.footer a:hover{ text-decoration: underline; }
.divider{ color: #d0d6db; }
/* Focus styles for keyboard users */
:focus{
outline: none;
}
/* Focus styles (unchanged) */
:focus{ outline: none; }
:focus-visible{
outline: 3px solid rgba(0,120,212,0.12);
outline-offset: 2px;
@@ -277,14 +223,13 @@ button.primary:disabled{
/* Small screens */
@media (max-width:420px){
.login-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
.login-widget .card{
padding: 18px;
width: 100%;
}
.brand h1{ font-size: 18px; }
.brand .subtitle{
font-family: "Kanit";
font-weight: 1000;
font-style: normal; }
.biometric span, .primary{ font-size: 13px; }
}

View File

@@ -1,105 +1,120 @@
<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="brand">
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
<h1 id="signin-title" class='kanit-bold'>ลืมรหัสผ่าน</h1>
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
</div>
<form [formGroup]="forgotFrm" class="form px-3 pb-3 login-mobile">
<label class="field">
<span class="label-text ">อีเมล์</span>
<input type="email" formControlName="email" class="px-2 " id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
</label>
@if (isSendOtp === true) {
<label class="field">
<span class="label-text">รหัสยืนยัน OTP</span>
<input type="email" formControlName="otp" autocomplete="otp" placeholder="123456" alt required/>
</label>
}
<!-- <div class="justify-end flex"> -->
<!-- <label class="stay-signed">
<input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span>
</label> -->
<div class="flex flex-row gap-2 mt-4 justify-end">
<div class="flex-row hover:-translate-y-2.5 transform transition-all">
<button class="bg-[linear-gradient(180deg,var(--primary)_0%,var(--primary-600)_100%)]
text-black
border-0
px-3.5 py-2.5
rounded-md
font-semibold
cursor-pointer
text-[14px]
shadow-[0_6px_18px_var(--color-blue-500)]
transition
ease-in-out
duration-100
hover:scale-[1.02]
active:opacity-90" (click)="isModalOpen = true">
เปิด Modal
</button>
</div>
@if (isSendOtp === false) {
<div class="flex justify-end">
@if (isLoading === true) {
<button type="submit" class="primary cursor-progress!" disabled>
กำลังส่ง...
</button>
} @else {
<button type="submit" class="primary" (click)="onSubmin()">
{{ 'ยืนยันส่ง OTP รีเซ็ตรหัสผ่าน' }}
</button>
}
</div>
} @else if(isSendOtp === true) {
<div class="flex justify-end gap-2">
<button type="button" class="primary" (click)="onSubmin()">
{{ 'ส่งอีกครั้ง' }}
</button>
<button type="submit" class="primary" (click)="onVerifySubmit()">
{{ 'รีเซ็ตรหัสผ่าน' }}
</button>
</div>
}
<div class="brand">
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
<h1 id="signin-title" class="kanit-bold">ลืมรหัสผ่าน</h1>
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
</div>
<!-- <button mat-raised-button color="primary" [disabled]="isLoading">
{{ isLoading ? 'กำลังส่ง...' : 'ส่งรหัส OTP' }}
</button> -->
<!-- } -->
<!-- </div> -->
</form>
@if(isModalOpen){
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50" [formGroup]="forgotFrm">
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-fit">
<h2 class="text-xl font-bold mb-4">เปลี่ยนรหัสผ่าน</h2>
<hr class="w-full h-1 bg-gray-300 rounded-sm shadow-neutral-400 md:my-1">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">รหัสผ่านใหม่</label>
<input type="password" id="newPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" formControlName="newPassword" placeholder="กรอกรหัสผ่านใหม่">
<form [formGroup]="forgotFrm" class="form">
<label class="field">
<span class="label-text">อีเมล์</span>
<input type="email" formControlName="email" class="input-field" id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
</label>
@if (isSendOtp === true) {
<label class="field">
<span class="label-text">รหัสยืนยัน OTP</span>
<input type="text" formControlName="otp" class="input-field" autocomplete="one-time-code" placeholder="123456" alt required/>
</label>
}
<div class="actions justify-end-custom mt-4">
<div class="flex-row hover-lift">
<button class="primary secondary-button" type="button" (click)="isModalOpen = true">
เปิด Modal
</button>
</div>
@if (isSendOtp === false) {
<div class="flex justify-end">
@if (isLoading === true) {
<button type="submit" class="primary" disabled>
กำลังส่ง...
</button>
} @else {
<button type="submit" class="primary" (click)="onSubmin()">
{{ 'ยืนยันส่ง OTP รีเซ็ตรหัสผ่าน' }}
</button>
}
</div>
} @else if(isSendOtp === true) {
<div class="flex justify-end gap-2">
<button type="button" class="primary secondary-button" (click)="onSubmin()">
{{ 'ส่งอีกครั้ง' }}
</button>
<button type="submit" class="primary" (click)="onVerifySubmit()">
{{ 'รีเซ็ตรหัสผ่าน' }}
</button>
</div>
}
</div>
</form>
@if(isModalOpen){
<div class="fixed inset-0 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 class="mb-4">
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ยืนยันรหัสผ่านใหม่</label>
<input type="password" id="confirmPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" formControlName="confirmPassword" placeholder="กรอกยืนยันรหัสผ่านใหม่">
@if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
<span class="text-red-600 md">รหัสผ่านไม่ตรงกัน</span>
}
</div>
<!-- <hr class="w-full h-[] bg-gray-100 border-0 rounded-sm md:my-1 dark:bg-gray-700"> -->
<div class="flex justify-end gap-2">
<button class="bg-red-500 text-white px-4 py-2 rounded" (click)="isModalOpen = false">
ปิด
</button>
<button class="bg-green-500 text-white px-4 py-2 rounded shadow-[0_1px_18px_var(--color-green-300)] hover:-translate-y-1.5 hover:shadow-[0_6px_18px_var(--color-green-500)] transition-all duration-500 ease-in-out" (click)="onSetNewPassword()">
ยืนยัน
</button>
<div 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 type="button" class="mt-3 inline-flex w-full justify-center rounded-xl bg-white px-5 py-3 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto transition-colors" (click)="isModalOpen = false">
ยกเลิก
</button>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
</div>

View File

@@ -12,8 +12,6 @@
/* Page layout */
.login-widget {
/* Fill the viewport and center the card. Do not let the page itself
scroll; the card gets an internal max-height instead. */
min-height: 100vh;
height: 100vh;
display: flex;
@@ -37,8 +35,6 @@
display: flex;
flex-direction: column;
gap: 14px;
/* Constrain the card so it never forces the page to scroll. If content
grows, the card will scroll internally. */
max-height: calc(100vh - 56px);
overflow: auto;
}
@@ -46,7 +42,7 @@
/* Modal/backdrop styles */
.login-backdrop{
position: fixed;
inset: 0; /* top:0; right:0; bottom:0; left:0; */
inset: 0;
background: rgba(0,0,0,0.38);
display: flex;
align-items: center;
@@ -64,33 +60,10 @@
box-shadow: 0 20px 50px rgba(2,6,23,0.4);
}
/* Slightly larger brand area inside modal */
.modal-card .brand{ padding: 18px; }
/* Make the primary button pill-shaped and slightly larger */
button.primary{
color: #000;
border-radius: 999px;
padding: 10px 18px;
font-size: 15px;
}
/* Make biometric and other action buttons visually lighter */
.biometric{
border-radius: 999px;
padding: 8px 12px;
}
/* On small screens reduce modal padding and width to avoid overflow */
@media (max-width: 420px){
.login-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
}
/* Brand area */
.brand{
text-align: center;
padding: 18px; /* Use padding from modal-card .brand */
padding-bottom: 4px;
border-bottom: 1px solid #eef2f5;
}
@@ -116,12 +89,11 @@ button.primary{
/* Form area */
.form{
/* keep compact spacing inside the card */
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 6px 0 2px;
padding: 6px 22px 2px 22px; /* Adjusted padding to match card padding */
}
/* Field label wrapper */
@@ -185,8 +157,9 @@ input:focus{
margin-top: 4px;
}
button.primary{
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
/* ⭐️ แก้ไขตรงนี้: เปลี่ยนสีข้อความเป็นสีดำตามคำขอ */
color: #000000;
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%);
border: none;
padding: 10px 14px;
border-radius: 6px;
@@ -196,6 +169,7 @@ button.primary{
box-shadow: 0 6px 18px rgba(0,120,212,0.12);
transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease;
}
button.primary:hover:not(:disabled){
transform: translateY(-1px);
box-shadow: 0 10px 24px rgba(0,120,212,0.14);
@@ -206,6 +180,7 @@ button.primary:active{
button.primary:disabled{
opacity: 0.55;
cursor: not-allowed;
color: #000000; /* ข้อความ Disabled ก็ยังเป็นสีดำ */
box-shadow: none;
}
@@ -230,6 +205,7 @@ button.primary:disabled{
font-weight: 600;
font-size: 13px;
}
.biometric svg{ display: block; opacity: .95; }
.biometric:hover{
background: rgba(0,120,212,0.04);
@@ -276,14 +252,14 @@ button.primary:disabled{
/* Small screens */
@media (max-width:420px){
.login-backdrop{ padding: 12px; }
.login-modal{ max-width: 100%; }
.modal-card .brand{ padding: 12px; }
.login-widget .card{
padding: 18px;
width: 100%;
}
.brand h1{ font-size: 18px; }
.brand .subtitle{
font-family: "Kanit";
font-weight: 1000;
font-style: normal; }
.biometric span, .primary{ font-size: 13px; }
}

View File

@@ -1,55 +1,55 @@
<!-- Modal-like backdrop that covers the viewport -->
<div class="login-backdrop">
<div class="login-modal d-flex align-items-center justify-content-center">
<div class="login-modal d-flex align-items-center justify-content-center bg-white rounded-2xl">
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
<div class="brand">
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
<h1 id="signin-title" class='kanit-bold'>เข้าสู่ระบบ</h1>
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของคุณ</p>
</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">
<span class="label-text">รหัสผ่าน</span>
<input type="password" formControlName="password" autocomplete="current-password" required />
</label>
<div class="brand">
<img src="/logo.png" alt="Company logo" class="logo"/>
<h1 id="signin-title" class="kanit-bold">เข้าสู่ระบบ</h1>
<p class="subtitle">บัญชีโปรแกรมจัดการแผนงานงบประมาณของท่าน</p>
</div>
<div class="actions">
<label class="stay-signed">
<input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span>
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form">
<label class="field">
<span class="label-text">อีเมล์</span>
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required class="input-field" />
</label>
<!-- <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">
<button type="button" class="biometric" (click)="useBiometric()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M12 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" fill="currentColor" opacity=".9"/>
<path d="M6.2 10.9A6 6 0 0 1 12 6a6 6 0 0 1 5.8 4.9M12 22v-2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div class="actions d-flex justify-content-between align-items-center mt-4">
<label class="stay-signed">
<input type="checkbox" formControlName="remember" />
<span>จดจำรหัสผ่าน</span>
</label>
<button type="submit" class="primary login-button"
[disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
เข้าสู่ระบบ
</button>
</div>
<div class="alt-options mt-4 text-center">
<button type="button" class="biometric btn-icon" (click)="useBiometric()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" class="icon-svg">
<path d="M12 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" fill="currentColor" opacity=".9"/>
<path d="M6.2 10.9A6 6 0 0 1 12 6a6 6 0 0 1 5.8 4.9M12 22v-2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>เข้าสู่ระบบด้วย Windows Hello / Touch ID</span>
</button>
</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>
<div class="footer mt-5 text-center">
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
<span class="divider mx-2"></span>
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
</div>
</form>
<div 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>

View File

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

View File

@@ -0,0 +1,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;
}
.quick-log__form select,
.quick-log__form textarea {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 0.75rem 1rem;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.quick-log__form select:focus,
.quick-log__form textarea:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
outline: none;
}
.quick-log__form input:focus,
.quick-log__form textarea:focus {
border-color: #0ea5e9;
@@ -643,4 +660,4 @@
max-height: 25rem;
overflow-y: auto;
padding-right: 0.5rem;
}
}

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

View File

@@ -1,7 +1,7 @@
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 { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData, QuickRatio} from '../../interfaces/dashboard.interface'
import { DashboardStateService } from '../../services/state/dashboard-state.service';
@Component({
@@ -11,20 +11,21 @@ import { DashboardStateService } from '../../services/state/dashboard-state.serv
styleUrl: './main-dashboard.component.css'
})
export class MainDashboardComponent implements OnInit {
@Output() saveEventSubmit = new EventEmitter<any>();
mode: string = 'i';
isModalOpen: boolean = false;
isSubmitting: boolean = false;
arrearsForm!: FormGroup;
saveFrm!: FormGroup;
myActData: IActData[] = [];
quickRatios: QuickRatio[] = [];
// myDropAct: IStateDrop[] = [];
myDropAct: IStateDrop = { income: [], expense: [] };
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: '',
netProfit: 0,
profitRate: '',
adjustedProfitRate: '',
period: ''
@@ -34,10 +35,11 @@ export class MainDashboardComponent implements OnInit {
expense: []
}
};
ActSumDataGradient: any
readonly ownerName = 'Nuttakit';
ownerName = localStorage.getItem('username') || 'ชนกนันต์';
constructor(
private dashboardStateService: DashboardStateService
@@ -83,12 +85,19 @@ export class MainDashboardComponent implements OnInit {
// { label: 'มิ.ย.', value: 77 }
// ];
readonly quickRatios = [
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
{ label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
{ label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
];
// readonly quickRatios = [
// { label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
// { label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' },
// { label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' }
// ];
// ฟังก์ชันนี้ควรเรียกหลังจากได้รับข้อมูล myActSumData แล้ว (เช่นใน subscribe หรือ ngOnChanges)
// เพิ่มใน Class Component
isNumber(val: any): boolean {
return typeof val === 'number';
}
readonly periodSummaries = [
{
label: 'รายปี',
@@ -217,8 +226,10 @@ export class MainDashboardComponent implements OnInit {
if (data) {
this.myActSumData = data;
this.ActSumDataGradient = this.buildExpenseGradient()
this.updateQuickRatios();
}
});
}
setupFormControl(){
this.arrearsForm = new FormGroup({
@@ -230,15 +241,66 @@ export class MainDashboardComponent implements OnInit {
});
this.saveFrm = new FormGroup({
actacpdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]),
actacpdtm: new FormControl('',[Validators.required]),
actqty: new FormControl('',[Validators.required]),
actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]),
actcmt: new FormControl('',[Validators.maxLength(200)])
});
}
onSaveSubmit(){
updateQuickRatios() {
const summary = this.myActSumData.summary;
// แปลงค่า netProfit เป็นตัวเลขเพื่อเช็คเงื่อนไข (รองรับทั้ง string และ number)
const netProfitVal = parseFloat(String(summary.netProfit));
const profitRateVal = parseFloat(summary.profitRate.replace('%', '')); // ลบ % ออกก่อนเช็ค
this.quickRatios = [
{
label: 'รายรับรวม',
value: summary.totalIncome,
colorClass: 'text-green-600' // รายรับสีเขียวเสมอ (หรือจะใช้สีดำ text-gray-700 ก็ได้)
},
{
label: 'รายจ่ายรวม',
value: summary.totalExpense,
colorClass: 'text-red-500' // รายจ่ายสีแดงอ่อน หรือสีปกติ
},
{
label: 'คงเหลือสุทธิ', // หรือ กำไรสุทธิ
value: netProfitVal, // ส่งเป็นตัวเลขไปให้ Pipe format
// ถ้า >= 0 สีน้ำเงิน, ถ้าติดลบ สีแดง
colorClass: netProfitVal >= 0 ? 'text-blue-600' : 'text-red-600 font-bold'
},
{
label: 'อัตรากำไร',
value: summary.profitRate,
// เช็ค % ถ้าติดลบให้แดง
colorClass: profitRateVal >= 0 ? 'text-blue-600' : 'text-red-600'
},
{
label: 'ระยะเวลา',
value: summary.period,
colorClass: 'text-gray-500'
}
];
}
onSaveSubmit(){
const rawDate = this.saveFrm.get('actacpdtm')?.value; // ค่าดิบ: "2025-11-21T14:23"
let arysave = {
actacpdtm: rawDate ? rawDate.replace(/[-T:]/g, '') : '',
actqty: this.saveFrm.get('actqty')?.value,
actcat: this.saveFrm.get('actcat')?.value,
actcmt: this.saveFrm.get('actcmt')?.value,
acttyp: this.mode
}
this.SaveEventSubmit(arysave);
}
SaveEventSubmit(event: any){
this.saveEventSubmit.emit(event);
}
onArrearsSubmit(){

View File

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

View File

@@ -14,7 +14,7 @@ import { finalize } from 'rxjs/operators';
export class LoginContentComponent implements OnInit {
@ViewChild(LoginForgotComponent) loginForgotComponent!: LoginForgotComponent;
@ViewChild(LoginPageComponent) loginPageComponent!: LoginPageComponent;
mode: 'forgot-password' | 'default' = 'default';
mode: 'forgot-password' | 'register' | 'default' = 'default';
constructor(
private generalService: GeneralService,
@@ -27,6 +27,8 @@ export class LoginContentComponent implements OnInit {
if (param === 'forgot-password') {
this.mode = 'forgot-password';
}else if(param === 'register'){
this.mode = 'register';
} else {
// this.router.navigate(['/login']); // This can cause navigation loops
this.mode = 'default';
@@ -57,6 +59,7 @@ export class LoginContentComponent implements OnInit {
if (result.code === '200' && result.data?.token) {
this.generalService.trowApi(result);
localStorage.setItem('access_token', result.data.token);
localStorage.setItem('username', result.data.usrthinam);
this.router.navigate(['main/dashboard']);
} else {
const errorMessage = result.message_th || result.message || 'Sign-in failed.';
@@ -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){
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: {
totalIncome: '',
totalExpense: '',
netProfit: '',
netProfit: 0,
profitRate: '',
adjustedProfitRate: '',
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 {
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 { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { LoginForgotComponent } from '../../component/login-forgot/login-forgot.component';
import { LoginRegisterComponent } from '../../component/login-register/login-register.component';
// import { AppModule } from '../../app.module';
@NgModule({
declarations: [
LoginContentComponent,
LoginPageComponent,
LoginForgotComponent
LoginForgotComponent,
LoginRegisterComponent
],
imports: [
CommonModule,

View File

@@ -35,7 +35,7 @@ export interface IActSumData {
export interface IActSummary {
totalIncome: string;
totalExpense: string;
netProfit: string;
netProfit: number;
profitRate: string;
adjustedProfitRate: string;
period: string;
@@ -53,6 +53,12 @@ export interface IActCategory {
color: string;
}
export interface QuickRatio {
label: string;
value: string | number;
colorClass: string; // ตัวเก็บชื่อ class สี
}
// ข้อมูลสินค้าหลัก
// export interface IProduct {
// id: string;

View File

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