Compare commits

...

14 Commits

Author SHA1 Message Date
5ce012b558 workflow: test p.1
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 1m25s
2025-11-16 21:51:06 +07:00
4b3e52ff43 Merge branch 'main' of http://10.9.0.0/ttc/micro-frontend 2025-11-16 21:50:13 +07:00
8f9159a330 workflow: test p.1 2025-11-16 21:48:42 +07:00
x2Skyz
ccab40852c -แก้ไขระบบ trow api และ ยิง 2025-11-16 21:45:57 +07:00
60662d88d4 workflow: initial workflow placeholder file 2025-11-16 21:33:05 +07:00
ee72ef6676 -scroll เมื่อ ชุดข้อมูลมากกว่า 5 2025-11-14 12:29:59 +07:00
7b441c3600 -ระ บบ pie chart และคำนวณ สี 2025-11-14 10:10:55 +07:00
139167be8a - 2025-11-13 20:23:55 +07:00
80edb10361 - 2025-11-13 19:00:31 +07:00
3cc4a4a632 -เชื่อมโยง api search กับ frontend
-ปรับปรุงระบบ state
-เพิ่ม ระบบ pipe dtmtodatetime
2025-11-13 18:00:51 +07:00
f27389da29 -ตั้งชื่อ caching ผิด 2025-11-13 16:25:13 +07:00
1664be0c8b dropdown 2025-11-13 15:37:50 +07:00
b3fa94f904 -interface
-service

-เพิ่มเทคนิค การส่ง ผ่านข้อมูล
2025-11-13 14:53:37 +07:00
37ca45701b -เพิ่มการรองรับ interfaces/dashboard.interface.ts 2025-11-13 14:20:15 +07:00
20 changed files with 1244 additions and 120 deletions

View File

@@ -0,0 +1,9 @@
name: Build Docker Image
run-name: Build Docker Image
on: [push]
jobs:
Preparing Dependecies:
steps:
- run: |
ls

View File

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

View File

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

View File

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

View File

@@ -627,3 +627,20 @@
text-align: left;
}
}
.quick-log__form select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.ledger-table.is-scrollable {
max-height: 25rem;
overflow-y: auto;
padding-right: 0.5rem;
}

View File

@@ -77,7 +77,25 @@
<div class="quick-log__grid">
<label>
<span>หมวดหมู่</span>
<input type="text" placeholder="เลือกหมวดหมู่" />
@if(mode == 'i'){
<select class="w-full h-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white">
<option value="">ไม่เลือก</option>
@for (item of myDropAct.income; track item.dtlcod) {
<option [value]="item.dtlcod">
{{ item.dtlnam }}
</option>
}
</select>
}@else if(mode == 'e'){
<select class="w-full h-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white">
<option value="">ไม่เลือก</option>
@for (item of myDropAct.expense; track item.dtlcod) {
<option [value]="item.dtlcod">
{{ item.dtlnam }}
</option>
}
</select>
}
</label>
<label>
<span>ยอดเงิน (฿)</span>
@@ -100,29 +118,58 @@
</div>
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
</div>
<div class="ledger-table">
<div class="ledger-table" [class.is-scrollable]="myActData.length > 5">
<div class="ledger-row ledger-head">
<span>รายการ</span>
<span>หมวดหมู่</span>
<span>ยอดเงิน</span>
<span>บันทึก</span>
</div>
<div class="ledger-row" *ngFor="let idx of ledgerEntries; let i = index;">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.type == 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.type == 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.title }}</p>
<p class="ledger-date">{{ idx.date }}</p>
<!-- @for (idx of myActData; track i; let i = $index) {
<div class="ledger-row">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.type === 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.title }}</p>
<p class="ledger-date">{{ idx.date }}</p>
</div>
</div>
<span class="ledger-category">{{ idx.category }}</span>
<span class="ledger-amount" [ngClass]="idx.type === 'i' ? 'is-credit' : 'is-debit'">
{{ idx.amount }}
</span>
<span class="ledger-note">{{ idx.note }}</span>
</div>
<span class="ledger-category">{{ idx.category }}</span>
<span class="ledger-amount" [ngClass]="idx.type == 'i' ? 'is-credit' : 'is-debit'">
{{ idx.amount }}
</span>
<span class="ledger-note">{{ idx.note }}</span>
</div>
} -->
@for (idx of myActData; track idx.actseq; let i = $index) {
<div class="ledger-row">
<div class="ledger-main">
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
{{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}
</span>
<div>
<p class="ledger-title">{{ idx.acttypnam }}</p>
<p class="ledger-date">{{ idx.actacpdtm ?? '' | dtmtodatetime}}</p>
</div>
</div>
<span class="ledger-category">{{ idx.actcatnam }}</span>
<span class="ledger-amount" [ngClass]="idx.acttyp === 'i' ? 'is-credit' : 'is-debit'">
{{ idx.actqty }}
</span>
<span class="ledger-note">{{ idx.actcmt }}</span>
</div>
}
</div>
</article>
</section>
@@ -153,18 +200,19 @@
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
</div>
<div class="pie-panel__content">
<div class="pie-chart" [style.background]="expenseGradient">
<div class="pie-chart" [style.background]="ActSumDataGradient">
<div class="pie-chart__center">
<p>รวมเดือนนี้</p>
<strong>฿732K</strong>
</div>
</div>
<ul class="pie-legend">
<li class="pie-legend__item" *ngFor="let part of expenseBreakdown">
<span class="swatch" [style.background]="part.color"></span>
<li class="pie-legend__item" *ngFor="let idx of myActSumData.pie.expense">
<span class="swatch" [style.background]="idx.color"></span>
<div>
<p class="pie-legend__label">{{ part.label }}</p>
<p class="pie-legend__value">{{ part.value }}%</p>
<p class="pie-legend__label">{{ idx.label }}</p>
<p class="pie-legend__value">{{ idx.percent }}%</p>
<p class="pie-legend__value">{{ idx.value }} บาท</p>
</div>
</li>
</ul>

View File

@@ -1,6 +1,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { GeneralService } from '../../services/generalservice';
import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData } from '../../interfaces/dashboard.interface'
import { DashboardStateService } from '../../services/state/dashboard-state.service';
@Component({
selector: 'app-main-dashboard',
@@ -15,10 +17,32 @@ export class MainDashboardComponent implements OnInit {
isSubmitting: boolean = false;
arrearsForm!: FormGroup;
saveFrm!: FormGroup;
myActData: IActData[] = [];
// myDropAct: IStateDrop[] = [];
myDropAct: IStateDrop = { income: [], expense: [] };
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: '',
profitRate: '',
adjustedProfitRate: '',
period: ''
},
pie: {
income: [],
expense: []
}
};
ActSumDataGradient: any
readonly ownerName = 'Nuttakit';
constructor(
private dashboardStateService: DashboardStateService
){}
readonly kpiCards = [
{
label: 'รายรับรวม',
@@ -50,14 +74,14 @@ export class MainDashboardComponent implements OnInit {
}
];
readonly revenueTrend = [
{ label: 'ม.ค.', value: 52 },
{ label: 'ก.พ.', value: 61 },
{ label: 'มี.ค.', value: 73 },
{ label: 'เม.ย.', value: 68 },
{ label: 'พ.ค.', value: 82 },
{ label: 'มิ.ย.', value: 77 }
];
// readonly revenueTrend = [
// { label: 'ม.ค.', value: 52 },
// { label: 'ก.พ.', value: 61 },
// { label: 'มี.ค.', value: 73 },
// { label: 'เม.ย.', value: 68 },
// { label: 'พ.ค.', value: 82 },
// { label: 'มิ.ย.', value: 77 }
// ];
readonly quickRatios = [
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
@@ -131,40 +155,40 @@ export class MainDashboardComponent implements OnInit {
}
];
readonly ledgerEntries = [
{
type: 'i',
title: 'ค่าบริการที่ปรึกษา',
category: 'บริการ',
amount: '+฿85,000',
date: 'วันนี้ · 10:15',
note: 'โครงการ Warehouse Automation'
},
{
type: 'e',
title: 'ค่าเช่าออฟฟิศ',
category: 'ค่าใช้จ่ายคงที่',
amount: '-฿48,000',
date: 'วันนี้ · 09:00',
note: 'สำนักงานพระราม 9'
},
{
type: 'i',
title: 'รับเงินมัดจำ',
category: 'สัญญาใหม่',
amount: '+฿120,000',
date: 'เมื่อวาน',
note: 'ลูกค้า Urbane CoWorking'
},
{
type: 'e',
title: 'ค่าวัตถุดิบ',
category: 'ต้นทุนโครงการ',
amount: '-฿27,500',
date: '12 มิ.ย.',
note: 'สั่งผ่าน Blue Supply'
}
];
// readonly ledgerEntries = [
// {
// type: 'i',
// title: 'ค่าบริการที่ปรึกษา',
// category: 'บริการ',
// amount: '+฿85,000',
// date: 'วันนี้ · 10:15',
// note: 'โครงการ Warehouse Automation'
// },
// {
// type: 'e',
// title: 'ค่าเช่าออฟฟิศ',
// category: 'ค่าใช้จ่ายคงที่',
// amount: '-฿48,000',
// date: 'วันนี้ · 09:00',
// note: 'สำนักงานพระราม 9'
// },
// {
// type: 'i',
// title: 'รับเงินมัดจำ',
// category: 'สัญญาใหม่',
// amount: '+฿120,000',
// date: 'เมื่อวาน',
// note: 'ลูกค้า Urbane CoWorking'
// },
// {
// type: 'e',
// title: 'ค่าวัตถุดิบ',
// category: 'ต้นทุนโครงการ',
// amount: '-฿27,500',
// date: '12 มิ.ย.',
// note: 'สั่งผ่าน Blue Supply'
// }
// ];
readonly expenseBreakdown = [
{ label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' },
@@ -174,12 +198,28 @@ export class MainDashboardComponent implements OnInit {
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
];
readonly expenseGradient = this.buildExpenseGradient();
ngOnInit(): void {
this.setupFormControl();
this.dashboardStateService.getStateResult().subscribe(data => {
if (data) {
this.myDropAct = data;
}
});
// ผลลับท์ ของ รายการ
this.dashboardStateService.getStateAccountResult().subscribe(data => {
if (data) {
this.myActData = data;
}
});
// ผลลัพการ คำนวณ ของ ปัญชี ต่างๆ
this.dashboardStateService.getStateSumResult().subscribe(data => {
if (data) {
this.myActSumData = data;
this.ActSumDataGradient = this.buildExpenseGradient()
}
});
}
setupFormControl(){
this.arrearsForm = new FormGroup({
// email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
@@ -206,15 +246,19 @@ export class MainDashboardComponent implements OnInit {
}
private buildExpenseGradient(): string {
if (!this.myActSumData?.pie?.expense?.length) return '';
let current = 0;
const segments = this.expenseBreakdown
const segments = this.myActSumData.pie.expense
.map(part => {
const start = current;
const end = current + part.value;
const percent = parseFloat(part.percent); // แปลงจาก string → number
const end = current + percent;
current = end;
return `${part.color} ${start}% ${end}%`;
})
.join(', ');
return `conic-gradient(${segments})`;
}
}

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,8 @@ export const CACHEABLE_URLS = {
// e.g., '/api/data'
],
POST: [
'/api/web/accountingSearch'
'/api/web/accountingSetup',
'/api/nigga'
// Add POST URIs here that you want to cache
// e.g., '/api/search'
]

View File

@@ -71,7 +71,7 @@ export class LoginContentComponent implements OnInit {
if (this.loginPageComponent) {
this.loginPageComponent.message = errorMessage;
}
this.generalService.trowApi(error.error || { message_th: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' });
this.generalService.trowApi(error);
}
});
}

View File

@@ -1,7 +1,10 @@
import { DashboardStateService } from './../../services/state/dashboard-state.service';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ChartConfiguration, ChartOptions } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { GeneralService } from '../../services/generalservice';
import { IDropAct, IStateDrop, IActData, IActSumData } from '../../interfaces/dashboard.interface';
@Component({
selector: 'app-main-dashboard-content',
@@ -11,44 +14,33 @@ import { GeneralService } from '../../services/generalservice';
})
export class MainDashboardContentComponent implements OnInit {
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
public lineChartData: ChartConfiguration<'line'>['data'] = {
labels: [],
datasets: [
{
data: [],
label: 'Revenue',
fill: true,
tension: 0.5,
borderColor: 'rgba(75,192,192,1)',
backgroundColor: 'rgba(75,192,192,0.2)'
}
]
};
public lineChartOptions: ChartOptions<'line'> = {
responsive: true,
scales: {
y: {
beginAtZero: true
}
myDropAct!: IStateDrop;
myActData: IActData[] = [];
myActSumData: IActSumData = {
summary: {
totalIncome: '',
totalExpense: '',
netProfit: '',
profitRate: '',
adjustedProfitRate: '',
period: ''
},
plugins: {
legend: {
display: true,
},
title: {
display: true,
text: 'Revenue Summary - Last 6 Months'
}
pie: {
income: [],
expense: []
}
};
constructor(private generalService: GeneralService) {}
constructor(
private generalService: GeneralService,
private dashboardStateService: DashboardStateService
) {}
ngOnInit(): void {
let token = localStorage.getItem('access_token')
this.OnSearchAct(token, true);
this.OnSetupDashboard(token, true);
this.OnSearchSum(token, true);
}
OnSearchAct(value: any, setupFirst: boolean): void {
@@ -60,11 +52,64 @@ export class MainDashboardContentComponent implements OnInit {
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
console.log(`✅ OTP ส่งไปที่ ${value.email}`);
this.myActData = result.data;
this.dashboardStateService.setStateAccountResult(this.myActData);
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
OnSetupDashboard(value: any, setupFirst: boolean): void {
const uri = '/api/web/accountingSetup';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
this.myDropAct = result.data
this.dashboardStateService.setStateResult(this.myDropAct)
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
}
});
}
OnSearchSum(value: any, setupFirst: boolean): void {
const uri = '/api/web/accountingSum';
let request = {
token: value
}
this.generalService.postRequest(uri, request).subscribe({
next: (result: any) => {
if (result.code === '200') {
this.generalService.trowApi(result);
this.myActSumData = result.data
this.dashboardStateService.setStateSumResult(this.myActSumData);
}else{
this.generalService.trowApi(result);
}
},
error: (error: any) => {
this.generalService.trowApi(error);
},
complete: () => {
@@ -94,24 +139,24 @@ export class MainDashboardContentComponent implements OnInit {
// });
// }
processChartData(data: any[]): void {
const labels = data.map(item => item.month);
const revenues = data.map(item => item.revenue);
// processChartData(data: any[]): void {
// const labels = data.map(item => item.month);
// const revenues = data.map(item => item.revenue);
this.lineChartData.labels = labels;
this.lineChartData.datasets[0].data = revenues;
// this.lineChartData.labels = labels;
// this.lineChartData.datasets[0].data = revenues;
this.chart?.update();
}
// this.chart?.update();
// }
setupPlaceholderData(): void {
// This function is called if the API fails, to show a sample graph.
const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
const revenues = [1200, 1900, 3000, 5000, 2300, 3200]; // Sample data
// setupPlaceholderData(): void {
// // This function is called if the API fails, to show a sample graph.
// const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
// const revenues = [1200, 1900, 3000, 5000, 2300, 3200]; // Sample data
this.lineChartData.labels = labels;
this.lineChartData.datasets[0].data = revenues;
// this.lineChartData.labels = labels;
// this.lineChartData.datasets[0].data = revenues;
this.chart?.update();
}
// this.chart?.update();
// }
}

View File

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

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MainReportComponent } from '../../component/main-report/main-report.component';
const routes: Routes = [
{
path: '',
component: MainReportComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ReportControlRoutingModule { }

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MainReportComponent } from '../../component/main-report/main-report.component';
import { ReportControlRoutingModule } from './report-control-routing.module';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
MainReportComponent
],
imports: [
CommonModule,
ReportControlRoutingModule,
ReactiveFormsModule
]
})
export class ReportControlModule { }

View File

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

View File

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

View File

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

View File

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