- catching
All checks were successful
Build Docker Image / Preparing Dependecies (push) Successful in 5s

This commit is contained in:
2025-11-17 17:25:51 +07:00
parent 2e07e728dd
commit a475265764
12 changed files with 8772 additions and 6562 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,91 @@
{
"name": "ng-ttc-frontend",
"version": "0.0.0",
"version": "1.0.0",
"main": "electron/main.js",
"author": "Nuttakit",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"electron": "ng build --base-href ./ && electron .",
"dist": "ng build --configuration production && electron-builder"
},
"build": {
"appId": "accounting.nuttakit.work",
"productName": "accounting-nuttakit",
"asar": false,
"directories": {
"output": "dist_electron"
},
"files": [
"dist/ng-ttc-frontend/browser/**/*",
"electron/**/*",
"!node_modules/@capacitor/android/**/*"
],
"win": {
"target": [
"nsis",
"msi"
],
"icon": "public/favicon.ico"
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowElevation": true,
"runAfterFinish": false
}
},
"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",
"@tailwindcss/postcss": "^4.1.17",
"@angular/animations": "^20.3.10",
"@angular/common": "^20.3.10",
"@angular/compiler": "^20.3.10",
"@angular/core": "^20.3.10",
"@angular/forms": "^20.3.10",
"@angular/platform-browser": "^20.3.10",
"@angular/platform-browser-dynamic": "^20.3.10",
"@angular/router": "^20.3.10",
"@capacitor/android": "^7.4.4",
"@capacitor/angular": "^2.0.3",
"@capacitor/core": "latest",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-free": "^7.1.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@tailwindcss/postcss": "^4.1.16",
"bootstrap": "^5.3.8",
"chart.js": "^4.5.1",
"dotenv": "^17.2.3",
"jwt-decode": "^4.0.0",
"ng2-charts": "^6.0.1",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.17",
"tailwindcss": "^4.1.16",
"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",
"@angular/build": "^20.3.9",
"@angular/cli": "^20.3.9",
"@angular/compiler-cli": "^20.3.10",
"@capacitor/cli": "latest",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.4.0",
"cross-env": "^10.1.0",
"electron": "^39.0.0",
"electron-builder": "^26.0.12",
"jasmine-core": "~5.6.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"
"ngx-toastr": "^19.1.0",
"postcss": "^8.5.3",
"typescript": "~5.9.3"
}
}

View File

@@ -1,7 +1,45 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SidebarContentComponent } from './content/sidebar-content/sidebar-content.component';
import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component';
// import { authGuard } from './services/auth.guard';
const routes: Routes = [];
const routes: Routes = [
{ path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) },
{ path: 'license', component: LicensePrivacyTermsComponent},
{
path: 'main',
component: SidebarContentComponent,
canActivate: [
// authGuard
],
children: [
{
path: '',
loadChildren: () =>
import('./controls/main-control/main-control.module').then(
(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' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],

View 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'
]
};

View File

@@ -6,7 +6,7 @@ import { MainControlRoutingModule } from './main-control-routing.module';
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 { AccDateFormatPipe } from '../../pipe/dtmtodatetime.pipe';
import { MainReportComponent } from '../../component/main-report/main-report.component';
@@ -17,7 +17,7 @@ import { MainReportComponent } from '../../component/main-report/main-report.com
@NgModule({
declarations: [
MainDashboardComponent,
// MainDashboardComponent,
MainDashboardContentComponent,
MainReportComponent,
AccDateFormatPipe

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

@@ -0,0 +1,32 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { jwtDecode } from 'jwt-decode';
export const authGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
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 {
router.navigate(['/login']);
return false;
}
};

View File

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

View 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();
}
}

View File

@@ -0,0 +1,149 @@
import { ToastrService } from 'ngx-toastr';
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class GeneralService {
private baseUrl = environment.apiBaseUrl;
private mode = environment.production;
constructor(
private http: HttpClient,
private toastr: ToastrService
) {}
// Default header
private getHttpOptions() {
const token = localStorage.getItem('access_token');
const headers = new HttpHeaders({
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
});
return { headers };
}
// Log ต้นแบบ
// private logRequest(method: string, url: string, body?: any) {
// if (this.mode === 'development') {
// console.log(`📡 [${method}] ${url}`, body || '');
// }
// }
// Helper: wrap body ให้มี request ครอบเสมอ
private wrapRequestBody(body: any): any {
// ถ้ามี request อยู่แล้ว จะไม่ซ้ำ
if (body && body.request) {
return body;
}
return { request: body ?? {} };
}
// POST Request
postRequest(uri: string, body: any): Observable<any> {
const payload = this.wrapRequestBody(body);
const fullUrl = `${this.baseUrl}${uri}`;
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(() => ({
status: error.status,
code: response?.code ?? '500',
message: response?.message ?? 'Internal Server Error',
message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์'
}));
})
);
}
// GET Request
getRequest(uri: string): Observable<any> {
const fullUrl = `${this.baseUrl}${uri}`;
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(() => ({
status: error.status,
code: response?.code ?? '500',
message: response?.message ?? 'Internal Server Error',
message_th: response?.message_th ?? 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์'
})); })
);
}
// PUT Request
putRequest(uri: string, body: any): Observable<any> {
const payload = this.wrapRequestBody(body);
const fullUrl = `${this.baseUrl}${uri}`;
return this.http.put(fullUrl, payload, this.getHttpOptions()).pipe(
map((res: any) => res),
catchError((error: any) => {
console.error('❌ [PUT Request Error]:', error);
return throwError(() => error);
})
);
}
// DELETE Request
deleteRequest(uri: string, body?: any): Observable<any> {
const payload = this.wrapRequestBody(body);
const fullUrl = `${this.baseUrl}${uri}`;
return this.http.delete(fullUrl, { ...this.getHttpOptions(), body: payload }).pipe(
map((res: any) => res),
catchError((error: any) => {
console.error('❌ [DELETE Request Error]:', error);
return throwError(() => error);
})
);
}
// showToast(type: 'success' | 'error' | 'warning' | 'info', message: string, title?: string) {
// const options = {
// positionClass: 'toast-top-right',
// timeOut: 3000,
// progressBar: true,
// progressAnimation: 'decreasing',
// toastClass: 'ngx-toastr bg-white bg-opacity-90 shadow-lg border'
// };
// this.toastr[type](message, title || type.toUpperCase(), options);
// }
trowApi(result: any){
const code = result?.code ?? 500;
const msg = result?.message ?? 'unknow';
const msgTh = result?.message_th ?? 'unknow';
if(code == 200){
this.toastr.success(`${msgTh || msg}`,'success',{
positionClass: 'toast-top-right',
timeOut: 2500,
progressBar: true,
progressAnimation: 'decreasing',
toastClass:
'ngx-toastr success-toast bg-white bg-opacity-90 text-green-700 shadow-lg'
});
} else {
this.toastr.error(`${msgTh || msg}`,'error',{
positionClass: 'toast-top-right',
timeOut: 3500,
progressBar: true,
progressAnimation: 'decreasing',
toastClass:
'ngx-toastr error-toast bg-white bg-opacity-90 text-red-700 shadow-lg'
});
}
}
}

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