Compare commits

...

14 Commits

Author SHA1 Message Date
7c51525841 [update] - add token exprire 2024-10-04 16:54:41 +07:00
6b1be7c793 [update] - test mirror 2024-10-04 15:32:50 +07:00
cb7eb81dc9 [test] - test mirror 2024-10-04 15:05:41 +07:00
925963e384 [test] - test morror 2024-10-04 14:56:33 +07:00
dcf55db57e [update] - chagne api 2024-09-30 11:58:38 +07:00
2a401eec51 [update] - change name in lsit 2024-09-20 15:48:19 +07:00
80b9172602 [hotfx] - adjust html 2024-09-20 15:31:18 +07:00
2cbac0027b [update] - adjust text 2024-09-19 14:44:44 +07:00
5b9faf6cbf [faet] - add mroe detail in kyc list and popup 2024-09-11 15:46:03 +07:00
fbf5b5d2e6 [feat] - add delete user 2024-08-21 16:46:55 +07:00
3cc3d72eb5 [feat] - add user lsit , convert to coop and lock unlock user 2024-08-19 17:05:10 +07:00
57b9371d2a [feat] - add delete kyc 2024-08-13 17:14:28 +07:00
288beac65d [update] - fix login 2024-08-13 14:35:35 +07:00
Chanikan Yenta
199b1975b4 [Edit] /assets 2024-06-25 11:04:09 +07:00
41 changed files with 14190 additions and 5261 deletions

View File

@@ -25,3 +25,4 @@ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To u
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
git push --mirror https://gitea.71dev.com/Phet/mirror-cathay.git

55
package-lock.json generated
View File

@@ -18,9 +18,11 @@
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"@auth0/angular-jwt": "^5.2.0",
"@ng-select/ng-select": "^10.0.3",
"bootstrap-icons": "^1.10.3",
"date-fns": "^2.29.3",
"f": "^1.4.0",
"rxjs": "~7.8.0",
"sweetalert2": "^11.7.3",
"tslib": "^2.3.0",
@@ -728,6 +730,17 @@
"integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==",
"dev": true
},
"node_modules/@auth0/angular-jwt": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-5.2.0.tgz",
"integrity": "sha512-9FS2L0QwGNlxA/zgeehCcsR9CZscouyXkoIj1fODM36A8BLfdzg9k9DWAXUQ2Drjk0AypGAFzeNZR4vsLMhdeQ==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">=14.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
@@ -6451,6 +6464,11 @@
"node": ">=4"
}
},
"node_modules/f": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/f/-/f-1.4.0.tgz",
"integrity": "sha512-THMsoTpFuIFoQ538jpbW7x/9pVaE0jdKN2O+nd7xpeUT17lECGX8M+QOaZ186ipc3XSzSFOytaXFidItHix44Q=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -13149,6 +13167,14 @@
"integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==",
"dev": true
},
"@auth0/angular-jwt": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-5.2.0.tgz",
"integrity": "sha512-9FS2L0QwGNlxA/zgeehCcsR9CZscouyXkoIj1fODM36A8BLfdzg9k9DWAXUQ2Drjk0AypGAFzeNZR4vsLMhdeQ==",
"requires": {
"tslib": "^2.0.0"
}
},
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
@@ -15357,8 +15383,7 @@
"version": "15.2.2",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.2.tgz",
"integrity": "sha512-xHd5CC0Wi0a/CKfKoOC4Bwb1FVjy0esj22eQAkVh0iDKeiAQH4UG/VRmsdSHvto1z0IzGbMSt4hRbv4h2aYIdw==",
"dev": true,
"requires": {}
"dev": true
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
@@ -15957,8 +15982,7 @@
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true,
"requires": {}
"dev": true
},
"acorn-node": {
"version": "1.8.2",
@@ -17549,6 +17573,11 @@
"tmp": "^0.0.33"
}
},
"f": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/f/-/f-1.4.0.tgz",
"integrity": "sha512-THMsoTpFuIFoQ538jpbW7x/9pVaE0jdKN2O+nd7xpeUT17lECGX8M+QOaZ186ipc3XSzSFOytaXFidItHix44Q=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -18086,8 +18115,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true,
"requires": {}
"dev": true
},
"ieee754": {
"version": "1.2.1",
@@ -18763,8 +18791,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz",
"integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==",
"dev": true,
"requires": {}
"dev": true
},
"karma-source-map-support": {
"version": "1.4.0",
@@ -20260,8 +20287,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true,
"requires": {}
"dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
@@ -21515,8 +21541,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"requires": {}
"dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
@@ -21943,8 +21968,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"requires": {}
"dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
@@ -22143,8 +22167,7 @@
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"dev": true,
"requires": {}
"dev": true
},
"xtend": {
"version": "4.0.2",

View File

@@ -20,9 +20,11 @@
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"@auth0/angular-jwt": "^5.2.0",
"@ng-select/ng-select": "^10.0.3",
"bootstrap-icons": "^1.10.3",
"date-fns": "^2.29.3",
"f": "^1.4.0",
"rxjs": "~7.8.0",
"sweetalert2": "^11.7.3",
"tslib": "^2.3.0",

8141
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,16 @@ export const MENU: MENU[] = [
badge: '',
roles: [ROLE_ADMIN],
},
{
name: 'User',
link: 'manage/user',
permission: 'user',
type: 'link',
icon: '',
params: [],
badge: '',
roles: [ROLE_ADMIN],
},
]
},
{

View File

@@ -33,6 +33,10 @@ const routes: Routes = [
path: 'kyc',
loadChildren: () => import('./pages/manage/kyc/kyc.module').then(m => m.KycModule)
},
{
path: "user",
loadChildren: () => import('./pages/manage/users/users.module').then(m => m.UsersModule)
}
]
},
{

View File

@@ -1,3 +1,4 @@
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
@Component({
@@ -5,4 +6,6 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html',
styleUrls: []
})
export class AppComponent {}
export class AppComponent {
}

View File

@@ -17,13 +17,13 @@ export class AppGuard implements CanActivate, CanActivateChild {
}
isLogin() {
const user = this.app.auth();
if (!user) {
this.router.navigate(['/auth/login'], { replaceUrl: true });
localStorage.clear()
return false;
}
return true;
// const user = this.app.auth();
// if (!user) {
// this.router.navigate(['/auth/login']);
// return false;
// }
// return true;
}
}

View File

@@ -9,6 +9,7 @@ import {AppService} from "./app.service";
import { AppGuard } from "./app.guard";
import { AppRequestInterceptor } from "./app.request.interceptor";
import { AppSharedModule } from "./app.shared";
import { JwtModule } from '@auth0/angular-jwt';
@NgModule({
@@ -24,7 +25,8 @@ import {AppSharedModule} from "./app.shared";
FormsModule,
ReactiveFormsModule,
HttpClientModule,
AppSharedModule
AppSharedModule,
JwtModule
],
providers: [
AppService,

View File

@@ -1,24 +1,44 @@
import { Injectable } from '@angular/core';
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import { HttpContextToken, HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Router } from '@angular/router';
import { AppService } from './app.service';
import { catchError, Observable, throwError } from 'rxjs';
import { environment } from 'src/environments/environment';
import { SathonCathayPayService } from './sathon-cathay-pay.service';
import { JwtHelperService } from '@auth0/angular-jwt';
export const SKIP_INTERCEP = new HttpContextToken<boolean>(() => false);
@Injectable()
export class AppRequestInterceptor implements HttpInterceptor {
constructor(
private router: Router,
private appService: AppService
) {
private appService: AppService,
// private sathonSV: SathonCathayPayService
) { }
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.context.get(SKIP_INTERCEP)) {
return next.handle(request)
}
const token = this.appService.token();
const appsathonAPpToken = this.appService.getsathonToken()
const helper = new JwtHelperService();
const isAppTokenExpired = helper.isTokenExpired(token);
const isSathonAppTokenExpired = helper.isTokenExpired(appsathonAPpToken)
if (isAppTokenExpired || isSathonAppTokenExpired) {
this.router.navigate(['/auth'], { replaceUrl: true });
localStorage.clear()
return
}
if (token) {
request = request.clone({
setHeaders: {Authorization: `Bearer ${token}`}
setHeaders: { Authorization: `Bearer ${this.generateToken(request.url)}` }
});
}
@@ -26,11 +46,17 @@ export class AppRequestInterceptor implements HttpInterceptor {
catchError((err) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this.router.navigate(['/auth']);
this.router.navigate(['/auth'], { replaceUrl: true });
}
}
return throwError(err);
})
)
}
generateToken(url: string) {
if (url.includes('71dev')) return this.appService.token()
if (url.includes('cathay-pay')) return this.appService.getsathonToken()
}
}

View File

@@ -4,8 +4,6 @@ import {DOCUMENT, Location} from '@angular/common';
import { from, Observable } from 'rxjs';
import Swal, { SweetAlertResult } from 'sweetalert2'
import { EAction } from "./@config/app";
@Injectable()
export class AppService {
@@ -25,6 +23,14 @@ export class AppService {
localStorage.setItem('user', JSON.stringify(data));
}
setSathonToken(data) {
localStorage.setItem('sathontoken', data);
}
getsathonToken() {
return localStorage.getItem('sathontoken');
}
token() {
const token = localStorage.getItem('token');
if (!token) return null;
@@ -42,6 +48,7 @@ export class AppService {
async logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('sathontoken');
localStorage.clear();
// await lastValueFrom( this.get(this.LOGOUT_API) )
}

View File

@@ -7,6 +7,7 @@ import { lastValueFrom } from 'rxjs';
import { EAction, EText } from 'src/app/@config/app';
import { CathayAuthService } from 'src/app/core/service/auth/cathay-auth.service';
import { ROLE_ADMIN } from 'src/app/@config/menus';
import { SathonCathayPayService } from 'src/app/sathon-cathay-pay.service';
@Component({
@@ -27,7 +28,8 @@ export class LoginComponent implements OnInit {
private router: Router,
private appService: AppService,
private authService: AuthService,
private cathayAuthService: CathayAuthService
private cathayAuthService: CathayAuthService,
private sathonSV: SathonCathayPayService
) {
}
@@ -46,12 +48,15 @@ export class LoginComponent implements OnInit {
try {
// const result = await lastValueFrom(this.authService.login(this.dataForm));
let cathayResult: any = await lastValueFrom(this.cathayAuthService.login(this.cathayForm));
let isAdmin = cathayResult.token.userName == ROLE_ADMIN ? true : false
let sathonToken: any = await lastValueFrom(this.cathayAuthService.genToken());
let isAdmin = cathayResult.userName == ROLE_ADMIN ? true : false
cathayResult = {
...cathayResult,
isAdmin: isAdmin
}
this.appService.setToken(cathayResult.token.token)
this.appService.setToken(cathayResult.token)
this.appService.setSathonToken(sathonToken.token)
this.appService.setAuth(cathayResult);
if (isAdmin) {

View File

@@ -12,10 +12,7 @@ import { Router } from '@angular/router';
@Injectable()
export class TokenIntercepterInterceptor implements HttpInterceptor {
constructor(
private router: Router,
) {
}
constructor() { }
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const customReq = request.clone({

View File

@@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpContext } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SKIP_INTERCEP } from 'src/app/app.request.interceptor';
import { BaseService } from 'src/app/core/base/base-service';
@Injectable({

View File

@@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpContext, HttpContextToken } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SKIP_INTERCEP } from 'src/app/app.request.interceptor';
import { BaseService } from 'src/app/core/base/base-service';
import { environment } from 'src/environments/environment';
@@ -12,10 +13,15 @@ export class CathayAuthService extends BaseService{
public http: HttpClient
) {
super('', http)
super.fullUrl = `${this.API_URL}/v1/User`
super.fullUrl = `${this.API_URL}/v3/User`
}
login(payload: { 'mobileDeviceId': string, 'userName': string, 'password': string }) {
return this.http.post(`${this.fullUrl}/login`, payload)
return this.http.post(`${this.fullUrl}/login`, payload, { context: new HttpContext().set(SKIP_INTERCEP, true) })
}
// https://sathorn.cathay-pay.com/api/v3/User/login
genToken() {
return this.http.get(`${environment.APIURL}/api/common/user_login/token`, { context: new HttpContext().set(SKIP_INTERCEP, true) })
}
}

View File

@@ -35,9 +35,10 @@ export class PagesLayoutsComponent implements OnInit {
}
async initAuth() {
this.auth = this.app.auth();
console.log(this.auth.isAdmin)
// console.log(this.auth)
// console.log(this.auth.isAdmin)
this.menus = this.menus.map(r => {
if(this.auth.isAdmin){
if (this.auth?.isAdmin) {
if (r.roles.includes(ROLE_ADMIN)) return {
...r,
children: r.children.length ? r.children.filter(c => r.roles.includes(ROLE_ADMIN)) : []
@@ -50,6 +51,8 @@ export class PagesLayoutsComponent implements OnInit {
}
})
if (!this.permissionCheck) {
// const users = await lastValueFrom(this.app.get(`${API.users}/getById/${this.auth.id}`));
// this.permission = users.permission;

View File

@@ -1,4 +1,2 @@
<app-list
(edit)="edit($event)"
[kycList]="kyc$ | async">
<app-list (edit)="edit($event)" (OnDelete)="onDelete($event)" [kycList]="kyc$ | async">
</app-list>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { Observable, catchError, switchMap, tap, throwError } from 'rxjs';
import { Observable, catchError, concatMap, filter, switchMap, tap, throwError } from 'rxjs';
import { IDialogConfig, CDialogConfig } from 'src/app/@common/interface/Dialog';
import { EAction, EText } from 'src/app/@config/app';
import { KycService } from 'src/app/core/service/common/kyc.service';
@@ -7,6 +7,8 @@ import { DialogComponent } from '../../presenter/dialog/dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { AppService } from 'src/app/app.service';
@Component({
selector: 'app-kyc',
templateUrl: './kyc.container.html',
@@ -43,4 +45,28 @@ export class KycContainer {
);
return edit$.subscribe();
}
onDelete(uid) {
// console.log(uid)
this.appService.confirm(
EAction.DELETE
).pipe(
filter(event => event.isConfirmed),
switchMap(event => {
return this.kycService.deleteData(uid).pipe(
concatMap(() => {
return this.kyc$ = this.kycService.getAll().pipe(
catchError((err) => {
this.appService.message(EAction.ERROR, EText.ERROR);
return throwError(() => err)
}),
tap(() => this.cdr.detectChanges())
)
})
)
})
).subscribe()
}
}

View File

@@ -12,21 +12,37 @@
<div class="overflow-hidden px-8 ">
<div class=" overflow-hidden">
<img class="h-56 w-full object-contain" [src]="form.value.image_front_url ? form.value.image_front_url : '/assets/images/no_image.webp'" alt=""
style="border: solid 1px #2D9CDB ;"
(click)="viewImg1(form.value.image_front_url)">
<img class="h-56 w-full object-contain"
[src]="form.value.image_front_url ? form.value.image_front_url : 'assets/images/no_image.webp'"
alt="" style="border: solid 1px #2D9CDB ;" (click)="viewImg1(form.value.image_front_url)">
</div>
<div class="flex justify-center">ข้างหน้า</div>
</div>
<div class="overflow-hidden px-8 ml-2">
<div class=" overflow-hidden">
<img class="h-56 w-full object-contain" [src]="form.value.image_back_url ? form.value.image_back_url : '/assets/images/no_image.webp'" alt=""
style="border: solid 1px #2D9CDB ;"
(click)="viewImg2(form.value.image_back_url)">
<img class="h-56 w-full object-contain"
[src]="form.value.image_back_url ? form.value.image_back_url : 'assets/images/no_image.webp'" alt=""
style="border: solid 1px #2D9CDB ;" (click)="viewImg2(form.value.image_back_url)">
</div>
<div class="flex justify-center">ข้างหลัง</div>
</div>
<div class="overflow-hidden px-8 ml-2">
<div class=" overflow-hidden">
<img class="h-56 w-full object-contain"
[src]="form.value.image_person_with_card_url ? form.value.image_person_with_card_url : 'assets/images/no_image.webp'"
alt="" style="border: solid 1px #2D9CDB ;" (click)="viewImg2(form.value.image_person_with_card_url)">
</div>
<div class="flex justify-center">รูปถ่ายคู่กับบัตร</div>
</div>
<div class="overflow-hidden px-8 ml-2">
<div class=" overflow-hidden">
<img class="h-56 w-full object-contain"
[src]="form.value.signature_url ? form.value.signature_url : 'assets/images/no_image.webp'" alt=""
style="border: solid 1px #2D9CDB ;" (click)="viewImg2(form.value.signature_url)">
</div>
<div class="flex justify-center">ลายเซ็น</div>
</div>
</div>
</div>
</div>
@@ -56,8 +72,17 @@
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">เกิดวันที่</mat-label>
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">สัญชาติ</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="nationality" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">เกิดวันที่</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="birth_date" [matDatepicker]="birthdate" readonly />
<mat-datepicker-toggle [for]="birthdate" disabled matSuffix></mat-datepicker-toggle>
@@ -66,6 +91,58 @@
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">ที่อยู่ปัจจุบัน</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="current_address" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">อาชีพ</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="occupation" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">ชื่อสถานที่ทำงาน</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="name_of_workplace" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">ที่อยู่ที่ทำงาน</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="work_address" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">ที่อยู่ตามบัตรประชาชน</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="address_according_id_card" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
@@ -82,14 +159,16 @@
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">เลขหลังบัตร</mat-label>
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">เลขหลังบัตร</mat-label>
<mat-form-field class="col-span-7">
<input matInput formControlName="code_back_card" readonly type="text">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
</div>
</div>
<div class="dialog-footer">

View File

@@ -43,6 +43,7 @@ export class DialogComponent extends BaseForm implements OnInit {
kyc_uid: null,
id_card: null,
code_back_card: null,
current_address: null,
exp_date: null,
prefix_name: null,
first_name: null,
@@ -58,7 +59,16 @@ export class DialogComponent extends BaseForm implements OnInit {
created_datetime: null,
updated_by: null,
updated_datetime: null,
owner_agency_uid: null
owner_agency_uid: null,
occupation: null,
additional_occupation: null,
nationality: null,
max_card_no: null,
name_of_workplace: null,
work_address: null,
signature_url: null,
image_person_with_card_url: null,
address_according_id_card: null
})
}

View File

@@ -5,7 +5,8 @@
<div class="col-span-3 md:col-span-5 md:order-2">
<mat-form-field>
<i matTextPrefix class="bi bi-search"></i>
<input matInput placeholder="เลขบัตรประชาชน/เลขหลังบัตร" [(ngModel)]="query.card" (ngModelChange)="onFilterCard($event)">
<input matInput placeholder="เลขบัตรประชาชน/เลขหลังบัตร" [(ngModel)]="query.card"
(ngModelChange)="onFilterCard($event)">
</mat-form-field>
</div>
<div class="col-span-5 md:col-span-7 md:order-2">
@@ -20,14 +21,16 @@
<div class="card-body">
<div class="table-wrap">
<table class="" mat-table [dataSource]="kycList" matSort>
<tr mat-header-row *matHeaderRowDef="['1','2','3','4','6','8','9','10','11','12']"></tr>
<tr mat-row *matRowDef="let row; columns: ['1','2','3','4','6','8','9','10','11','12'];"></tr>
<tr mat-header-row *matHeaderRowDef="header"></tr>
<tr mat-row *matRowDef="let row; columns: header;"></tr>
<ng-container matColumnDef="1">
<th mat-header-cell *matHeaderCellDef class="tac">ลำดับ</th>
<td mat-cell *matCellDef="let item; let i = index;" width="100" class="tac">{{getIndex(i)}}</td>
</ng-container>
<ng-container matColumnDef="2">
<th mat-header-cell *matHeaderCellDef class="tac">เลขบัตรประชาชน</th>
<td mat-cell *matCellDef="let item" class="tac" style="min-width: 200px;">
@@ -80,12 +83,26 @@
<div> {{item.email}}</div>
</td>
</ng-container>
<ng-container matColumnDef="register_no">
<th mat-header-cell *matHeaderCellDef class="tal" width="150">เลขสมาชิก</th>
<td mat-cell *matCellDef="let item" class="tal whitespace-nowrap">
<div>{{item.max_card_no}} </div>
</td>
</ng-container>
<ng-container matColumnDef="create_date">
<th mat-header-cell *matHeaderCellDef class="tal" width="150">วันที่สมัคร</th>
<td mat-cell *matCellDef="let item" class="tal whitespace-nowrap">
<div> {{item.created_datetime | thaidate}}</div>
</td>
</ng-container>
<ng-container matColumnDef="11">
<th mat-header-cell *matHeaderCellDef class="tac" width="150">More Detail</th>
<td mat-cell *matCellDef="let item" class="tac">
<div class="action flex justify-center">
<div class="item cursor-pointer">
<i class="bi bi-pencil-square icon-edit mr-2" (click)="onEdit(item.kyc_uid)"></i>
<i class="bi bi-trash3 icon-delete mr-2" (click)="onDeleteKyc(item.kyc_uid)"></i>
</div>
</div>
</td>

View File

@@ -11,12 +11,15 @@ export class ListComponent extends BaseList implements OnChanges {
@Input() kycList: any = [];
@Output() edit = new EventEmitter();
@Output() search = new EventEmitter();
@Output() OnDelete = new EventEmitter<string>()
query = {
name: null,
card: null
}
name: string;
filterCard: Subject<string> = new Subject<string>();
header = ['1', 'create_date', '2', '3', '4', '6', '8', '9', '10', 'register_no', '11', '12']
constructor() {
super()
this.filterCard.pipe(
@@ -33,6 +36,11 @@ export class ListComponent extends BaseList implements OnChanges {
this.edit.emit(uid)
}
onDeleteKyc(uid: string) {
// console.log(uid)
this.OnDelete.emit(uid)
}
onFilterCard($event) {
const filterValue = this.query.card;
this.kycList.filter = filterValue.trim().toLowerCase();

View File

@@ -0,0 +1,3 @@
<app-user-list [userList]="userList$ | async" (edit)="edit($event)" (OnDeleteUser)="onDeleteUser($event)">
</app-user-list>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersContainer } from './users.container';
describe('UsersContainer', () => {
let component: UsersContainer;
let fixture: ComponentFixture<UsersContainer>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UsersContainer ]
})
.compileComponents();
fixture = TestBed.createComponent(UsersContainer);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,57 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { catchError, filter, Observable, switchMap, tap, throwError } from 'rxjs';
import { IDialogConfig, CDialogConfig } from 'src/app/@common/interface/Dialog';
import { EAction, EText } from 'src/app/@config/app';
import { SathonCathayPayService } from 'src/app/sathon-cathay-pay.service';
import { DialogComponent } from '../../../kyc/presenter/dialog/dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { AppService } from 'src/app/app.service';
import { ModalComponent } from '../../presenter/modal/modal.component';
@Component({
selector: 'app-users',
templateUrl: './users.container.html',
styleUrls: ['./users.container.scss']
})
export class UsersContainer {
userList$ = new Observable()
dialogConfig: IDialogConfig = CDialogConfig
constructor(
private sathonSV: SathonCathayPayService,
private dialog: MatDialog,
private appSV: AppService,
private cdr: ChangeDetectorRef
) {
this.userList$ = this.sathonSV.getAllUsers()
}
edit(user) {
this.dialogConfig.data.action = EAction.UPDATE;
this.dialogConfig.data = user
const dialogRef = this.dialog.open(ModalComponent, this.dialogConfig);
dialogRef.afterClosed().pipe(
filter(isRefresh => isRefresh),
tap(() => {
this.userList$ = this.sathonSV.getAllUsers()
this.cdr.detectChanges()
})
).subscribe()
}
onDeleteUser(user) {
this.appSV.confirm(EAction.DELETE).pipe(
filter(r => r.isConfirmed),
switchMap(() => this.sathonSV.deleteUser(user.id)),
tap(() => {
this.userList$ = this.sathonSV.getAllUsers()
this.cdr.detectChanges()
}),
catchError(err => {
this.appSV.message('error', 'เกิดข้อผิดพลาด')
return throwError(err)
})
).subscribe()
}
}

View File

@@ -0,0 +1,82 @@
<form class="dialog-main form-dialog" autocomplete="off">
<div class="dialog-main">
<div class="dialog-header flex justify-between">
<h2>More Detail</h2>
<mat-icon mat-dialog-close class="cursor-pointer">clear</mat-icon>
</div>
<div class="dialog-body">
<div class="grid grid-cols-12 gap-4 md:gap-2 ">
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label
class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">เลขประจำตัวประชาชน</mat-label>
<mat-form-field class="col-span-3">
<input matInput required type="text" [value]="dialog?.personalCardId" readonly>
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">ชื่อ นามสกุล</mat-label>
<mat-form-field class="col-span-3">
<input matInput required type="text" readonly [value]="dialog?.fullName">
</mat-form-field>
<span class="col-span-5"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">Username</mat-label>
<mat-form-field class="col-span-3">
<input matInput required type="text" readonly [value]="dialog?.userName">
</mat-form-field>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">เบอร์โทร</mat-label>
<mat-form-field class="col-span-3">
<input matInput required type="text" readonly [value]="dialog?.phoneNumber">
</mat-form-field>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">อีเมล</mat-label>
<mat-form-field class="col-span-3">
<input matInput required type="text" readonly [value]="dialog?.normalizedEmail">
</mat-form-field>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4">Lock/Unlock</mat-label>
<mat-slide-toggle [checked]="dialog.lockoutEnabled" (change)="changeUserLock($event)"></mat-slide-toggle>
<span class="col-span-1"></span>
</div>
</div>
<div class="col-span-full">
<div class="lg:flex lg:flex-col grid grid-cols-9 gap-2">
<mat-label class="col-span-1 lg:text-left lg:self-auto text-right self-center mr-4"></mat-label>
<button mat-raised-button class="btn btn-submit" (click)="save()">
<div style="white-space: nowrap;"> User เป็น Merchant</div>
</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- <pre>{{form.getRawValue() | json}}</pre> -->

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalComponent } from './modal.component';
describe('ModalComponent', () => {
let component: ModalComponent;
let fixture: ComponentFixture<ModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ModalComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,70 @@
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Router, ActivatedRoute } from '@angular/router';
import { tap, catchError, throwError, lastValueFrom, filter, switchMap } from 'rxjs';
import { IDialogConfigData } from 'src/app/@common/interface/Dialog';
import { EAction, EText } from 'src/app/@config/app';
import { AppService } from 'src/app/app.service';
import { BaseForm } from 'src/app/core/base/base-form';
import { PromotionService } from 'src/app/core/service/common/promotion.service';
import { UploadService } from 'src/app/core/service/common/upload.service';
import { DialogComponent } from '../../../kyc/presenter/dialog/dialog.component';
import { Location } from '@angular/common';
import { SathonCathayPayService } from 'src/app/sathon-cathay-pay.service';
@Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.scss']
})
export class ModalComponent implements OnInit {
IsRefresh: boolean = false
constructor(
public dialogRef: MatDialogRef<DialogComponent>,
@Inject(MAT_DIALOG_DATA) public dialog: any,
private sathonSV: SathonCathayPayService,
private appsv: AppService
) { }
ngOnInit(): void {
}
changeUserLock({ checked },) {
let userId = this.dialog.id
console.log(checked)
const msg = checked ? 'Unlock' : 'Lock'
this.sathonSV.lockUnlockUser(userId).pipe(
tap(event => {
this.appsv.message(EAction.SUCCESS, `${msg} สำเร็จแล้ว`)
this.IsRefresh = true
}),
tap(() => this.closeDialog()),
catchError(err => {
this.IsRefresh = false
return throwError(err)
})
).subscribe()
}
save() {
let userId = this.dialog.id
this.appsv.confirm(EAction.UPDATE).pipe(
filter(r => r.isConfirmed),
switchMap(() => this.sathonSV.convertToCoperate(userId)),
tap(() => this.IsRefresh = true),
tap(() => this.closeDialog()),
catchError(err => {
this.IsRefresh = false
this.appsv.message('error', 'เกิดข้อผิดพลาด')
return throwError(err)
})
).subscribe()
// this.sathonSV.convertToCoperate(userId).subscribe()
}
closeDialog() {
this.dialogRef.close(this.IsRefresh)
}
}

View File

@@ -0,0 +1,85 @@
<div class="card card-table">
<div class="card-filter text-right">
<div class="card-filter-section grid grid-cols-12 gap-4 md:gap-2 items-center">
<div class="col-span-3 md:col-span-5 md:order-2">
<mat-form-field>
<i matTextPrefix class="bi bi-search"></i>
<input matInput placeholder="เลขบัตรประชาชน/เลขหลังบัตร" [(ngModel)]="query.card"
(ngModelChange)="onFilterCard($event)">
</mat-form-field>
</div>
<div class="col-span-5 md:col-span-7 md:order-2">
<mat-form-field>
<i matTextPrefix class="bi bi-search"></i>
<input matInput placeholder="ชื่อ-นามสกุล" [(ngModel)]="query.name" (ngModelChange)="onFilterName($event)">
</mat-form-field>
</div>
</div>
</div>
<div class="card-body">
<div class="table-wrap">
<table class="" mat-table [dataSource]="userList" matSort>
<tr mat-header-row *matHeaderRowDef="userListKey"></tr>
<tr mat-row *matRowDef="let row; columns: userListKey;"></tr>
<ng-container matColumnDef="1">
<th mat-header-cell *matHeaderCellDef class="tac">ลำดับ</th>
<td mat-cell *matCellDef="let item; let i = index;" width="100" class="tac">{{getIndex(i)}}</td>
</ng-container>
<!-- <ng-container matColumnDef="personalCardId">
<th mat-header-cell *matHeaderCellDef class="tac">เลขบัตรประชาชน</th>
<td mat-cell *matCellDef="let item" class="tac" style="min-width: 200px;">
{{item.personalCardId}}
</td>
</ng-container> -->
<ng-container matColumnDef="fullName">
<th mat-header-cell *matHeaderCellDef class="tac" width="200">ชื่อ นามสกุล</th>
<td mat-cell *matCellDef="let item" class="tac whitespace-nowrap">{{item.fullName}}</td>
</ng-container>
<ng-container matColumnDef="userName">
<th mat-header-cell *matHeaderCellDef class="tal" width="150">ชื่อยูเซอร์</th>
<td mat-cell *matCellDef="let item" class="whitespace-nowrap">{{item.userName }}</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef class="tac" width="100">อีเมล</th>
<td mat-cell *matCellDef="let item" class="tac">{{item.email}}</td>
</ng-container>
<ng-container matColumnDef="phoneNumber">
<th mat-header-cell *matHeaderCellDef class="tac" width="100">เบอร์โทรศัพท์</th>
<td mat-cell *matCellDef="let item" class="tac">{{item.phoneNumber}}</td>
</ng-container>
<ng-container matColumnDef="userType">
<th mat-header-cell *matHeaderCellDef class="tac" width="100">ประเภทผู้ใช้งาน</th>
<td mat-cell *matCellDef="let item" class="tac">{{item.userType}}</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef class="tac" width="100">ไอดี</th>
<td mat-cell *matCellDef="let item" class="tac">{{item.id}}</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef class="tac" width="150">More Detail</th>
<td mat-cell *matCellDef="let item" class="tac">
<div class="action flex justify-center">
<div class="item cursor-pointer">
<i class="bi bi-pencil-square icon-edit mr-2" (click)="onEdit(item)"></i>
<i class="bi bi-trash3 icon-delete mr-2" (click)="onDeleteUser(item)"></i>
</div>
</div>
</td>
</ng-container>
</table>
</div>
<!-- <div *ngIf="dataSourceCount === 0" class="no-data"></div> -->
<mat-paginator [pageSizeOptions]="[5,10,20]" showFirstLastButtons></mat-paginator>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UserListComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,57 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Subject, debounceTime, distinctUntilChanged } from 'rxjs';
import { BaseList } from 'src/app/core/base/base-list';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent extends BaseList implements OnChanges {
@Input() userList: any = [];
@Output() edit = new EventEmitter();
@Output() search = new EventEmitter();
@Output() OnDeleteUser = new EventEmitter<string>()
query = {
name: null,
card: null
}
name: string;
filterCard: Subject<string> = new Subject<string>();
userListKey = ['1', 'fullName', 'userName', 'email', 'phoneNumber', 'userType', 'id', 'action']
constructor() {
super()
this.filterCard.pipe(
debounceTime(500),
distinctUntilChanged()
).subscribe(() => this.onSearch())
}
ngOnChanges(): void {
this.userList = this.updateMatTable(this.userList ? this.userList : [])
}
onEdit(user) {
this.edit.emit(user)
}
onDeleteUser(uid: string) {
this.OnDeleteUser.emit(uid)
}
onFilterCard($event) {
const filterValue = this.query.card;
this.userList.filter = filterValue.trim().toLowerCase();
}
onFilterName($event) {
const filterValue = this.query.name;
this.userList.filter = filterValue.trim().toLowerCase();
}
onSearch() {
const filterValue = this.query.card;
this.userList.filter = filterValue.trim().toLowerCase();
}
}

View File

@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-users-router',
template: '<router-outlet></router-outlet>',
})
export class UsersRouterContainer {
}

View File

@@ -0,0 +1,37 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { UsersRouterContainer } from './router/users-router/users-router.container';
import { UsersContainer } from './container/users/users.container';
import { ModalComponent } from './presenter/modal/modal.component';
import { UserListComponent } from './presenter/user-list/user-list.component';
import { AppSharedModule } from 'src/app/app.shared';
const routes: Routes = [
{
path: '',
component: UsersRouterContainer,
children: [
{
path: '',
component: UsersContainer
}
]
}
]
@NgModule({
declarations: [
UsersRouterContainer,
UsersContainer,
ModalComponent,
UserListComponent
],
imports: [
CommonModule,
RouterModule.forChild(routes),
AppSharedModule
]
})
export class UsersModule { }

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SathonCathayPayService } from './sathon-cathay-pay.service';
describe('SathonCathayPayService', () => {
let service: SathonCathayPayService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SathonCathayPayService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class SathonCathayPayService {
endpoint: string = environment.CATHAYAPIURL
constructor(
private http: HttpClient
) { }
getAllUsers(): Observable<[]> {
return this.http.get<[]>(`${this.endpoint}/v1/user`).pipe(map((d: any) => d.data))
}
deleteUser(userId) {
return this.http.delete<[]>(`${this.endpoint}/v1/user/${userId}`)
}
lockUnlockUser(id: string): Observable<any> {
const request = { userId: id }
return this.http.post(`${this.endpoint}/v2/Authentication/LockUnlock`, request)
}
convertToCoperate(id: string): Observable<any> {
const request = { id }
return this.http.post(`${this.endpoint}/v1/User/convert2coperateType`, request)
}
}

View File

@@ -1,6 +1,7 @@
export const environment = {
production: false,
hideForm: false,
// test mirror
APIURL: 'https://cathaypay.71dev.com/cathaypay-api',
CATHAYAPIURL: 'https://sathorn.cathay-pay.com/api'
};

View File

@@ -1,5 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cathaypay</title>
@@ -8,10 +9,13 @@
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@100;200;300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@100;200;300;400;500&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

9027
yarn.lock

File diff suppressed because it is too large Load Diff