Compare commits
21 Commits
origins
...
304ed9294c
| Author | SHA1 | Date | |
|---|---|---|---|
| 304ed9294c | |||
| 9b0edccde5 | |||
|
|
6e96c686a2 | ||
|
|
c4c19e96b6 | ||
| 1f3c33b78c | |||
| 5ce012b558 | |||
| 4b3e52ff43 | |||
| 8f9159a330 | |||
|
|
ccab40852c | ||
| 60662d88d4 | |||
| ee72ef6676 | |||
| 7b441c3600 | |||
| 139167be8a | |||
| 80edb10361 | |||
| 3cc4a4a632 | |||
| f27389da29 | |||
| 1664be0c8b | |||
| b3fa94f904 | |||
| 37ca45701b | |||
| 78ce686f97 | |||
| f25488370a |
9
.gitea/workflows/build-image.yml
Normal file
9
.gitea/workflows/build-image.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
run-name: Build Docker Image
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
Preparing Dependecies:
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
ls
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"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",
|
||||||
"ng2-charts": "^6.0.1",
|
"ng2-charts": "^6.0.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"@angular/cli": "^20.3.9",
|
"@angular/cli": "^20.3.9",
|
||||||
"@angular/compiler-cli": "^20.3.10",
|
"@angular/compiler-cli": "^20.3.10",
|
||||||
"@capacitor/cli": "latest",
|
"@capacitor/cli": "latest",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"electron": "^39.0.0",
|
"electron": "^39.0.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
@@ -84,7 +86,6 @@
|
|||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"ngx-toastr": "^19.1.0",
|
"ngx-toastr": "^19.1.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3"
|
||||||
"@types/jasmine": "~5.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ const routes: Routes = [
|
|||||||
|
|
||||||
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) },
|
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) },
|
||||||
|
|
||||||
{ path: 'c', component: LicensePrivacyTermsComponent},
|
{ path: 'license', component: LicensePrivacyTermsComponent},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'main',
|
path: 'main',
|
||||||
@@ -22,9 +22,18 @@ const routes: Routes = [
|
|||||||
(m) => m.MainControlModule
|
(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', pathMatch: 'full' },
|
||||||
|
|
||||||
{ path: '**', redirectTo: 'login' }
|
{ path: '**', redirectTo: 'login' }
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/
|
|||||||
// import { LoginPageComponent } from './component/login-page/login-page.component';
|
// import { LoginPageComponent } from './component/login-page/login-page.component';
|
||||||
// import { LoginContentComponent } from './content/login-content/login-content.component';
|
// import { LoginContentComponent } from './content/login-content/login-content.component';
|
||||||
|
|
||||||
|
|
||||||
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
||||||
|
// import { AccDateFormatPipe } from './pipe/dtmtodatetime.pipe';
|
||||||
|
// import { DtmtodatetimePipe } from './dtmtodatetime.pipe';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -30,6 +33,8 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
|||||||
SidebarContentComponent,
|
SidebarContentComponent,
|
||||||
SidebarComponent,
|
SidebarComponent,
|
||||||
LicensePrivacyTermsComponent,
|
LicensePrivacyTermsComponent,
|
||||||
|
// AccDateFormatPipe
|
||||||
|
// DtmtodatetimePipe,
|
||||||
// MainDashboardContentComponent,
|
// MainDashboardContentComponent,
|
||||||
// MainDashboardComponent,
|
// MainDashboardComponent,
|
||||||
// LoginForgotComponent,
|
// LoginForgotComponent,
|
||||||
@@ -51,6 +56,9 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
|||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
FontAwesomeModule
|
FontAwesomeModule
|
||||||
],
|
],
|
||||||
|
exports: [
|
||||||
|
// AccDateFormatPipe
|
||||||
|
],
|
||||||
providers: [provideCharts(withDefaultRegisterables())],
|
providers: [provideCharts(withDefaultRegisterables())],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -627,3 +627,20 @@
|
|||||||
text-align: left;
|
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;
|
||||||
|
}
|
||||||
@@ -77,7 +77,25 @@
|
|||||||
<div class="quick-log__grid">
|
<div class="quick-log__grid">
|
||||||
<label>
|
<label>
|
||||||
<span>หมวดหมู่</span>
|
<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>
|
||||||
<label>
|
<label>
|
||||||
<span>ยอดเงิน (฿)</span>
|
<span>ยอดเงิน (฿)</span>
|
||||||
@@ -100,29 +118,58 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
|
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ledger-table">
|
<div class="ledger-table" [class.is-scrollable]="myActData.length > 5">
|
||||||
<div class="ledger-row ledger-head">
|
<div class="ledger-row ledger-head">
|
||||||
<span>รายการ</span>
|
<span>รายการ</span>
|
||||||
<span>หมวดหมู่</span>
|
<span>หมวดหมู่</span>
|
||||||
<span>ยอดเงิน</span>
|
<span>ยอดเงิน</span>
|
||||||
<span>บันทึก</span>
|
<span>บันทึก</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ledger-row" *ngFor="let idx of ledgerEntries; let i = index;">
|
<!-- @for (idx of myActData; track i; let i = $index) {
|
||||||
<div class="ledger-main">
|
<div class="ledger-row">
|
||||||
<span class="pill" [ngClass]="idx.type == 'i' ? 'pill--income' : 'pill--expense'">
|
<div class="ledger-main">
|
||||||
{{ idx.type == 'i' ? 'รับ' : 'จ่าย' }}
|
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
|
||||||
</span>
|
{{ idx.type === 'i' ? 'รับ' : 'จ่าย' }}
|
||||||
<div>
|
</span>
|
||||||
<p class="ledger-title">{{ idx.title }}</p>
|
|
||||||
<p class="ledger-date">{{ idx.date }}</p>
|
<div>
|
||||||
|
<p class="ledger-title">{{ idx.title }}</p>
|
||||||
|
<p class="ledger-date">{{ idx.date }}</p>
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<span class="ledger-category">{{ idx.category }}</span>
|
} -->
|
||||||
<span class="ledger-amount" [ngClass]="idx.type == 'i' ? 'is-credit' : 'is-debit'">
|
@for (idx of myActData; track idx.actseq; let i = $index) {
|
||||||
{{ idx.amount }}
|
<div class="ledger-row">
|
||||||
</span>
|
|
||||||
<span class="ledger-note">{{ idx.note }}</span>
|
<div class="ledger-main">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@@ -153,18 +200,19 @@
|
|||||||
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
|
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pie-panel__content">
|
<div class="pie-panel__content">
|
||||||
<div class="pie-chart" [style.background]="expenseGradient">
|
<div class="pie-chart" [style.background]="ActSumDataGradient">
|
||||||
<div class="pie-chart__center">
|
<div class="pie-chart__center">
|
||||||
<p>รวมเดือนนี้</p>
|
<p>รวมเดือนนี้</p>
|
||||||
<strong>฿732K</strong>
|
<strong>{{myActSumData.summary.totalExpense}}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="pie-legend">
|
<ul class="pie-legend">
|
||||||
<li class="pie-legend__item" *ngFor="let part of expenseBreakdown">
|
<li class="pie-legend__item" *ngFor="let idx of myActSumData.pie.expense">
|
||||||
<span class="swatch" [style.background]="part.color"></span>
|
<span class="swatch" [style.background]="idx.color"></span>
|
||||||
<div>
|
<div>
|
||||||
<p class="pie-legend__label">{{ part.label }}</p>
|
<p class="pie-legend__label">{{ idx.label }}</p>
|
||||||
<p class="pie-legend__value">{{ part.value }}%</p>
|
<p class="pie-legend__value">{{ idx.percent }}%</p>
|
||||||
|
<p class="pie-legend__value">{{ idx.value }} บาท</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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 { DashboardStateService } from '../../services/state/dashboard-state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-dashboard',
|
selector: 'app-main-dashboard',
|
||||||
@@ -15,10 +17,32 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
isSubmitting: boolean = false;
|
isSubmitting: boolean = false;
|
||||||
arrearsForm!: FormGroup;
|
arrearsForm!: FormGroup;
|
||||||
saveFrm!: 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';
|
readonly ownerName = 'Nuttakit';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dashboardStateService: DashboardStateService
|
||||||
|
){}
|
||||||
|
|
||||||
readonly kpiCards = [
|
readonly kpiCards = [
|
||||||
{
|
{
|
||||||
label: 'รายรับรวม',
|
label: 'รายรับรวม',
|
||||||
@@ -50,14 +74,14 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
readonly revenueTrend = [
|
// readonly revenueTrend = [
|
||||||
{ label: 'ม.ค.', value: 52 },
|
// { label: 'ม.ค.', value: 52 },
|
||||||
{ label: 'ก.พ.', value: 61 },
|
// { label: 'ก.พ.', value: 61 },
|
||||||
{ label: 'มี.ค.', value: 73 },
|
// { label: 'มี.ค.', value: 73 },
|
||||||
{ label: 'เม.ย.', value: 68 },
|
// { label: 'เม.ย.', value: 68 },
|
||||||
{ label: 'พ.ค.', value: 82 },
|
// { label: 'พ.ค.', value: 82 },
|
||||||
{ label: 'มิ.ย.', value: 77 }
|
// { label: 'มิ.ย.', value: 77 }
|
||||||
];
|
// ];
|
||||||
|
|
||||||
readonly quickRatios = [
|
readonly quickRatios = [
|
||||||
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
|
{ label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' },
|
||||||
@@ -131,40 +155,40 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
readonly ledgerEntries = [
|
// readonly ledgerEntries = [
|
||||||
{
|
// {
|
||||||
type: 'i',
|
// type: 'i',
|
||||||
title: 'ค่าบริการที่ปรึกษา',
|
// title: 'ค่าบริการที่ปรึกษา',
|
||||||
category: 'บริการ',
|
// category: 'บริการ',
|
||||||
amount: '+฿85,000',
|
// amount: '+฿85,000',
|
||||||
date: 'วันนี้ · 10:15',
|
// date: 'วันนี้ · 10:15',
|
||||||
note: 'โครงการ Warehouse Automation'
|
// note: 'โครงการ Warehouse Automation'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: 'e',
|
// type: 'e',
|
||||||
title: 'ค่าเช่าออฟฟิศ',
|
// title: 'ค่าเช่าออฟฟิศ',
|
||||||
category: 'ค่าใช้จ่ายคงที่',
|
// category: 'ค่าใช้จ่ายคงที่',
|
||||||
amount: '-฿48,000',
|
// amount: '-฿48,000',
|
||||||
date: 'วันนี้ · 09:00',
|
// date: 'วันนี้ · 09:00',
|
||||||
note: 'สำนักงานพระราม 9'
|
// note: 'สำนักงานพระราม 9'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: 'i',
|
// type: 'i',
|
||||||
title: 'รับเงินมัดจำ',
|
// title: 'รับเงินมัดจำ',
|
||||||
category: 'สัญญาใหม่',
|
// category: 'สัญญาใหม่',
|
||||||
amount: '+฿120,000',
|
// amount: '+฿120,000',
|
||||||
date: 'เมื่อวาน',
|
// date: 'เมื่อวาน',
|
||||||
note: 'ลูกค้า Urbane CoWorking'
|
// note: 'ลูกค้า Urbane CoWorking'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: 'e',
|
// type: 'e',
|
||||||
title: 'ค่าวัตถุดิบ',
|
// title: 'ค่าวัตถุดิบ',
|
||||||
category: 'ต้นทุนโครงการ',
|
// category: 'ต้นทุนโครงการ',
|
||||||
amount: '-฿27,500',
|
// amount: '-฿27,500',
|
||||||
date: '12 มิ.ย.',
|
// date: '12 มิ.ย.',
|
||||||
note: 'สั่งผ่าน Blue Supply'
|
// note: 'สั่งผ่าน Blue Supply'
|
||||||
}
|
// }
|
||||||
];
|
// ];
|
||||||
|
|
||||||
readonly expenseBreakdown = [
|
readonly expenseBreakdown = [
|
||||||
{ label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' },
|
{ label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' },
|
||||||
@@ -174,12 +198,28 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
|
{ label: 'อื่นๆ', value: 8, color: '#e11d48' }
|
||||||
];
|
];
|
||||||
|
|
||||||
readonly expenseGradient = this.buildExpenseGradient();
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setupFormControl();
|
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(){
|
setupFormControl(){
|
||||||
this.arrearsForm = new FormGroup({
|
this.arrearsForm = new FormGroup({
|
||||||
// email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
|
// email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
|
||||||
@@ -206,15 +246,19 @@ export class MainDashboardComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildExpenseGradient(): string {
|
private buildExpenseGradient(): string {
|
||||||
|
if (!this.myActSumData?.pie?.expense?.length) return '';
|
||||||
|
|
||||||
let current = 0;
|
let current = 0;
|
||||||
const segments = this.expenseBreakdown
|
const segments = this.myActSumData.pie.expense
|
||||||
.map(part => {
|
.map(part => {
|
||||||
const start = current;
|
const start = current;
|
||||||
const end = current + part.value;
|
const percent = parseFloat(part.percent); // แปลงจาก string → number
|
||||||
|
const end = current + percent;
|
||||||
current = end;
|
current = end;
|
||||||
return `${part.color} ${start}% ${end}%`;
|
return `${part.color} ${start}% ${end}%`;
|
||||||
})
|
})
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
return `conic-gradient(${segments})`;
|
return `conic-gradient(${segments})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
accounting-ng-nuttakit/src/app/config/caching.config.ts
Normal file
12
accounting-ng-nuttakit/src/app/config/caching.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const CACHEABLE_URLS = {
|
||||||
|
GET: [
|
||||||
|
// Add GET URIs here that you want to cache
|
||||||
|
// e.g., '/api/data'
|
||||||
|
],
|
||||||
|
POST: [
|
||||||
|
'/api/web/accountingSetup',
|
||||||
|
'/api/nigga'
|
||||||
|
// Add POST URIs here that you want to cache
|
||||||
|
// e.g., '/api/search'
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -71,7 +71,7 @@ export class LoginContentComponent implements OnInit {
|
|||||||
if (this.loginPageComponent) {
|
if (this.loginPageComponent) {
|
||||||
this.loginPageComponent.message = errorMessage;
|
this.loginPageComponent.message = errorMessage;
|
||||||
}
|
}
|
||||||
this.generalService.trowApi(error.error || { message_th: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' });
|
this.generalService.trowApi(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { DashboardStateService } from './../../services/state/dashboard-state.service';
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ChartConfiguration, ChartOptions } from 'chart.js';
|
import { ChartConfiguration, ChartOptions } from 'chart.js';
|
||||||
import { BaseChartDirective } from 'ng2-charts';
|
import { BaseChartDirective } from 'ng2-charts';
|
||||||
import { GeneralService } from '../../services/generalservice';
|
import { GeneralService } from '../../services/generalservice';
|
||||||
|
import { IDropAct, IStateDrop, IActData, IActSumData } from '../../interfaces/dashboard.interface';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-dashboard-content',
|
selector: 'app-main-dashboard-content',
|
||||||
@@ -11,85 +14,149 @@ import { GeneralService } from '../../services/generalservice';
|
|||||||
})
|
})
|
||||||
export class MainDashboardContentComponent implements OnInit {
|
export class MainDashboardContentComponent implements OnInit {
|
||||||
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
@ViewChild(BaseChartDirective) chart?: BaseChartDirective;
|
||||||
|
myDropAct!: IStateDrop;
|
||||||
public lineChartData: ChartConfiguration<'line'>['data'] = {
|
myActData: IActData[] = [];
|
||||||
labels: [],
|
myActSumData: IActSumData = {
|
||||||
datasets: [
|
summary: {
|
||||||
{
|
totalIncome: '',
|
||||||
data: [],
|
totalExpense: '',
|
||||||
label: 'Revenue',
|
netProfit: '',
|
||||||
fill: true,
|
profitRate: '',
|
||||||
tension: 0.5,
|
adjustedProfitRate: '',
|
||||||
borderColor: 'rgba(75,192,192,1)',
|
period: ''
|
||||||
backgroundColor: 'rgba(75,192,192,0.2)'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
public lineChartOptions: ChartOptions<'line'> = {
|
|
||||||
responsive: true,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: {
|
pie: {
|
||||||
legend: {
|
income: [],
|
||||||
display: true,
|
expense: []
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Revenue Summary - Last 6 Months'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private generalService: GeneralService) {}
|
constructor(
|
||||||
|
private generalService: GeneralService,
|
||||||
|
private dashboardStateService: DashboardStateService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.fetchChartData();
|
let token = localStorage.getItem('access_token')
|
||||||
|
this.OnSearchAct(token, true);
|
||||||
|
this.OnSetupDashboard(token, true);
|
||||||
|
this.OnSearchSum(token, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchChartData(): void {
|
OnSearchAct(value: any, setupFirst: boolean): void {
|
||||||
// NOTE: Using a placeholder endpoint as the actual one was not provided.
|
const uri = '/api/web/accountingSearch';
|
||||||
const uri = '/api/dashboard/summary-last-6-months';
|
let request = {
|
||||||
|
token: value
|
||||||
this.generalService.getRequest(uri).subscribe({
|
}
|
||||||
|
this.generalService.postRequest(uri, request).subscribe({
|
||||||
next: (result: any) => {
|
next: (result: any) => {
|
||||||
if (result.code === '200' && result.data) {
|
if (result.code === '200') {
|
||||||
this.processChartData(result.data);
|
this.generalService.trowApi(result);
|
||||||
} else {
|
this.myActData = result.data;
|
||||||
console.warn('Could not fetch chart data:', result.message_th);
|
this.dashboardStateService.setStateAccountResult(this.myActData);
|
||||||
// Optionally, display placeholder data or an error message
|
}else{
|
||||||
this.setupPlaceholderData();
|
this.generalService.trowApi(result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: any) => {
|
error: (error: any) => {
|
||||||
console.error('Error fetching chart data:', error);
|
this.generalService.trowApi(error);
|
||||||
// Display placeholder data on error to show the graph structure
|
},
|
||||||
this.setupPlaceholderData();
|
complete: () => {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
processChartData(data: any[]): void {
|
|
||||||
const labels = data.map(item => item.month);
|
|
||||||
const revenues = data.map(item => item.revenue);
|
|
||||||
|
|
||||||
this.lineChartData.labels = labels;
|
|
||||||
this.lineChartData.datasets[0].data = revenues;
|
|
||||||
|
|
||||||
this.chart?.update();
|
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: () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPlaceholderData(): void {
|
OnSearchSum(value: any, setupFirst: boolean): void {
|
||||||
// This function is called if the API fails, to show a sample graph.
|
const uri = '/api/web/accountingSum';
|
||||||
const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
|
let request = {
|
||||||
const revenues = [1200, 1900, 3000, 5000, 2300, 3200]; // Sample data
|
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: () => {
|
||||||
|
|
||||||
this.lineChartData.labels = labels;
|
}
|
||||||
this.lineChartData.datasets[0].data = revenues;
|
});
|
||||||
|
|
||||||
this.chart?.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchChartData(): void {
|
||||||
|
// // NOTE: Using a placeholder endpoint as the actual one was not provided.
|
||||||
|
// const uri = '/api/dashboard/summary-last-6-months';
|
||||||
|
|
||||||
|
// this.generalService.getRequest(uri).subscribe({
|
||||||
|
// next: (result: any) => {
|
||||||
|
// if (result.code === '200' && result.data) {
|
||||||
|
// this.processChartData(result.data);
|
||||||
|
// } else {
|
||||||
|
// console.warn('Could not fetch chart data:', result.message_th);
|
||||||
|
// // Optionally, display placeholder data or an error message
|
||||||
|
// this.setupPlaceholderData();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// error: (error: any) => {
|
||||||
|
// console.error('Error fetching chart data:', error);
|
||||||
|
// // Display placeholder data on error to show the graph structure
|
||||||
|
// this.setupPlaceholderData();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// processChartData(data: any[]): void {
|
||||||
|
// const labels = data.map(item => item.month);
|
||||||
|
// const revenues = data.map(item => item.revenue);
|
||||||
|
|
||||||
|
// this.lineChartData.labels = labels;
|
||||||
|
// this.lineChartData.datasets[0].data = revenues;
|
||||||
|
|
||||||
|
// this.chart?.update();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setupPlaceholderData(): void {
|
||||||
|
// // This function is called if the API fails, to show a sample graph.
|
||||||
|
// const labels = ['January', 'February', 'March', 'April', 'May', 'June'];
|
||||||
|
// const revenues = [1200, 1900, 3000, 5000, 2300, 3200]; // Sample data
|
||||||
|
|
||||||
|
// this.lineChartData.labels = labels;
|
||||||
|
// this.lineChartData.datasets[0].data = revenues;
|
||||||
|
|
||||||
|
// this.chart?.update();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component';
|
import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component';
|
||||||
|
import { MainReportComponent } from '../../component/main-report/main-report.component';
|
||||||
// import { MainReportComponent } from '../../component/main-report/main-report.component';
|
// import { MainReportComponent } from '../../component/main-report/main-report.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: 'dashboard', component: MainDashboardContentComponent },
|
{ path: 'dashboard', component: MainDashboardContentComponent },
|
||||||
|
{ path: 'report', component: MainReportComponent },
|
||||||
// children: [
|
// children: [
|
||||||
// {
|
// {
|
||||||
// path: 'dashboard',
|
// path: 'dashboard',
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { ReactiveFormsModule } from '@angular/forms';
|
|||||||
|
|
||||||
import { MainDashboardComponent } from '../../component/main-dashboard/main-dashboard.component';
|
import { MainDashboardComponent } from '../../component/main-dashboard/main-dashboard.component';
|
||||||
import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component';
|
import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component';
|
||||||
|
import { AccDateFormatPipe } from '../../pipe/dtmtodatetime.pipe';
|
||||||
|
import { MainReportComponent } from '../../component/main-report/main-report.component';
|
||||||
|
|
||||||
// import { MainReportComponent } from '../../component/main-report/main-report.component';
|
// import { MainReportComponent } from '../../component/main-report/main-report.component';
|
||||||
|
|
||||||
@@ -16,7 +18,9 @@ import { MainDashboardContentComponent } from '../../content/main-dashboard-cont
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
MainDashboardComponent,
|
MainDashboardComponent,
|
||||||
MainDashboardContentComponent
|
MainDashboardContentComponent,
|
||||||
|
MainReportComponent,
|
||||||
|
AccDateFormatPipe
|
||||||
// MainReportComponent
|
// MainReportComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@@ -24,6 +28,9 @@ import { MainDashboardContentComponent } from '../../content/main-dashboard-cont
|
|||||||
MainControlRoutingModule,
|
MainControlRoutingModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
// BrowserAnimationsModule
|
// BrowserAnimationsModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AccDateFormatPipe
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class MainControlModule { }
|
export class MainControlModule { }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// }
|
||||||
24
accounting-ng-nuttakit/src/app/pipe/dtmtodatetime.pipe.ts
Normal file
24
accounting-ng-nuttakit/src/app/pipe/dtmtodatetime.pipe.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,12 +1,30 @@
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
export const authGuard: CanActivateFn = (route, state) => {
|
export const authGuard: CanActivateFn = (route, state) => {
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
const accessToken = localStorage.getItem('access_token');
|
const accessToken = localStorage.getItem('access_token');
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
return true;
|
try {
|
||||||
|
const decodedToken: any = jwtDecode(accessToken);
|
||||||
|
const currentTime = Date.now() / 1000;
|
||||||
|
|
||||||
|
if (decodedToken.exp < currentTime) {
|
||||||
|
// Token expired
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
router.navigate(['/login']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Error decoding token
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
router.navigate(['/login']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
router.navigate(['/login']);
|
router.navigate(['/login']);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse
|
||||||
|
} from '@angular/common/http';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { CachingService } from './caching.service';
|
||||||
|
import { CACHEABLE_URLS } from '../config/caching.config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CachingInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(private cache: CachingService) {}
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
if (!this.isCacheable(req)) {
|
||||||
|
return next.handle(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedResponse = this.cache.get(this.getCacheKey(req));
|
||||||
|
if (cachedResponse) {
|
||||||
|
return of(cachedResponse.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle(req).pipe(
|
||||||
|
tap(event => {
|
||||||
|
if (event instanceof HttpResponse) {
|
||||||
|
this.cache.put(this.getCacheKey(req), event.clone());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCacheable(req: HttpRequest<any>): boolean {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return CACHEABLE_URLS.GET.some(url => req.urlWithParams.includes(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
return CACHEABLE_URLS.POST.some(url => req.urlWithParams.includes(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCacheKey(req: HttpRequest<any>): string {
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
return req.urlWithParams + JSON.stringify(req.body);
|
||||||
|
}
|
||||||
|
return req.urlWithParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
accounting-ng-nuttakit/src/app/services/caching.service.ts
Normal file
41
accounting-ng-nuttakit/src/app/services/caching.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CachingService {
|
||||||
|
private cache = new Map<string, [Date, HttpResponse<any>]>();
|
||||||
|
private cacheDurationInMs = 600000; // 5 minutes
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
get(key: string): HttpResponse<any> | null {
|
||||||
|
const tuple = this.cache.get(key);
|
||||||
|
if (!tuple) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = tuple[0];
|
||||||
|
const httpResponse = tuple[1];
|
||||||
|
|
||||||
|
// Don't observe expired keys
|
||||||
|
const now = new Date();
|
||||||
|
if (expires && expires.getTime() < now.getTime()) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
put(key: string, value: HttpResponse<any>): void {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setMilliseconds(expires.getMilliseconds() + this.cacheDurationInMs);
|
||||||
|
this.cache.set(key, [expires, value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,8 +54,14 @@ export class GeneralService {
|
|||||||
return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe(
|
return this.http.post(fullUrl, payload, this.getHttpOptions()).pipe(
|
||||||
map((res: any) => res),
|
map((res: any) => res),
|
||||||
catchError((error: any) => {
|
catchError((error: any) => {
|
||||||
|
const response = error?.error;
|
||||||
console.error('❌ [POST Request 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(
|
return this.http.get(fullUrl, this.getHttpOptions()).pipe(
|
||||||
map((res: any) => res),
|
map((res: any) => res),
|
||||||
catchError((error: any) => {
|
catchError((error: any) => {
|
||||||
|
const response = error?.error;
|
||||||
console.error('❌ [GET Request 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 ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์'
|
||||||
|
})); })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: true,
|
||||||
apiBaseUrl: 'https://api.nuttakit.work'
|
apiBaseUrl: 'https://api.nuttakit.work'
|
||||||
};
|
};
|
||||||
|
|||||||
17
ng-ttc-frontend/.editorconfig
Normal file
17
ng-ttc-frontend/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
43
ng-ttc-frontend/.gitignore
vendored
Normal file
43
ng-ttc-frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
/.angular
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_moduless
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
4
ng-ttc-frontend/.vscode/extensions.json
vendored
Normal file
4
ng-ttc-frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||||
|
"recommendations": ["angular.ng-template"]
|
||||||
|
}
|
||||||
20
ng-ttc-frontend/.vscode/launch.json
vendored
Normal file
20
ng-ttc-frontend/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ng serve",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: start",
|
||||||
|
"url": "http://localhost:4200/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ng test",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: test",
|
||||||
|
"url": "http://localhost:9876/debug.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
ng-ttc-frontend/.vscode/tasks.json
vendored
Normal file
42
ng-ttc-frontend/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "test",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
ng-ttc-frontend/README.md
Normal file
59
ng-ttc-frontend/README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# NgTtcFrontend
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.0.7.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
115
ng-ttc-frontend/angular.json
Normal file
115
ng-ttc-frontend/angular.json
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"ng-ttc-frontend": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"standalone": false
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"standalone": false
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"standalone": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/ng-ttc-frontend",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "ng-ttc-frontend:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "ng-ttc-frontend:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
14870
ng-ttc-frontend/package-lock.json
generated
Normal file
14870
ng-ttc-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
ng-ttc-frontend/package.json
Normal file
38
ng-ttc-frontend/package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "ng-ttc-frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^19.0.0",
|
||||||
|
"@angular/common": "^19.0.0",
|
||||||
|
"@angular/compiler": "^19.0.0",
|
||||||
|
"@angular/core": "^19.0.0",
|
||||||
|
"@angular/forms": "^19.0.0",
|
||||||
|
"@angular/platform-browser": "^19.0.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||||
|
"@angular/router": "^19.0.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^19.0.7",
|
||||||
|
"@angular/cli": "^19.0.7",
|
||||||
|
"@angular/compiler-cli": "^19.0.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.4.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ng-ttc-frontend/public/favicon.ico
Normal file
BIN
ng-ttc-frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
10
ng-ttc-frontend/src/app/app-routing.module.ts
Normal file
10
ng-ttc-frontend/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
const routes: Routes = [];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
||||||
0
ng-ttc-frontend/src/app/app.component.css
Normal file
0
ng-ttc-frontend/src/app/app.component.css
Normal file
1
ng-ttc-frontend/src/app/app.component.html
Normal file
1
ng-ttc-frontend/src/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<router-outlet />
|
||||||
11
ng-ttc-frontend/src/app/app.component.ts
Normal file
11
ng-ttc-frontend/src/app/app.component.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
standalone: false,
|
||||||
|
styleUrl: './app.component.css'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'ng-ttc-frontend';
|
||||||
|
}
|
||||||
18
ng-ttc-frontend/src/app/app.module.ts
Normal file
18
ng-ttc-frontend/src/app/app.module.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppRoutingModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiUrl: 'http://api.nuttakit.work'
|
||||||
|
};
|
||||||
4
ng-ttc-frontend/src/environments/environment.ts
Normal file
4
ng-ttc-frontend/src/environments/environment.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
apiUrl: 'http://localhost:8000'
|
||||||
|
};
|
||||||
13
ng-ttc-frontend/src/index.html
Normal file
13
ng-ttc-frontend/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>NgTtcFrontend</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
ng-ttc-frontend/src/main.ts
Normal file
7
ng-ttc-frontend/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule, {
|
||||||
|
ngZoneEventCoalescing: true,
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
1
ng-ttc-frontend/src/styles.css
Normal file
1
ng-ttc-frontend/src/styles.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
15
ng-ttc-frontend/tsconfig.app.json
Normal file
15
ng-ttc-frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
ng-ttc-frontend/tsconfig.json
Normal file
27
ng-ttc-frontend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ng-ttc-frontend/tsconfig.spec.json
Normal file
15
ng-ttc-frontend/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user