commit c838b2a979903a76ee79e248f2c6de7bf2d64939 Author: ttc@2026 Date: Tue Nov 11 10:52:30 2025 +0700 first diff --git a/@knowleadge/สร้างโปรเจ็ค.txt b/@knowleadge/สร้างโปรเจ็ค.txt new file mode 100644 index 0000000..7844c16 --- /dev/null +++ b/@knowleadge/สร้างโปรเจ็ค.txt @@ -0,0 +1,142 @@ + n nSTEP 1 — สร้างโปรเจ็กต์ใหม่ + +ng new accounting-ng-nuttakit --no-standalone + +STEP 2 + +npm install bootstrap +npm install @angular/material @angular/cdk + +และเพิ่มใน angular.json + + +"styles": [ + "node_modules/bootstrap/dist/css/bootstrap.min.css", + "src/styles.scss" +] + + +STEP 3 สร้าง Module แรก + ng generate module main --route main --module ../app-routing.module.ts + ng g module controls/login-control --routing + +STEP 4 + +ng generate component modules/goods/goods-content +ng generate component modules/goods/goods-page +ng generate service modules/goods/goods-state + +STEP 5 + +shared/interfaces/product.interface.ts + + +export interface Product { + id: number; + name: string; + price: number; + category: string; + imgUrl: string; +} + + +goods-state.service.ts + +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Product } from 'src/app/shared/interfaces/product.interface'; + +@Injectable({ providedIn: 'root' }) +export class GoodsStateService { + private goodsState = new BehaviorSubject([]); + goodsState$ = this.goodsState.asObservable(); + + setState(data: Product[]) { + this.goodsState.next(data); + } + + getStateResult() { + return this.goodsState$; + } +} + + +STEP 10 — สร้าง Content Component (ตัวควบคุมหลัก) + +import { Component, OnInit } from '@angular/core'; +import { GoodsStateService } from './goods-state.service'; +import { Product } from 'src/app/shared/interfaces/product.interface'; + +@Component({ + selector: 'app-goods-content', + templateUrl: './goods-content.component.html', +}) +export class GoodsContentComponent implements OnInit { + products: Product[] = []; + + constructor(private goodsState: GoodsStateService) {} + + ngOnInit(): void { + this.goodsState.getStateResult().subscribe(data => { + this.products = data; + }); + this.mockData(); // ตัวอย่าง data + } + + private mockData() { + const mock: Product[] = [ + { id: 1, name: 'Phone', price: 9000, category: 'Electronics', imgUrl: 'phone.jpg' }, + { id: 2, name: 'Laptop', price: 25000, category: 'Electronics', imgUrl: 'laptop.jpg' } + ]; + this.goodsState.setState(mock); + } +} + +goods-page.component.html + + + +
+
+
+ +
+
{{ item.name }}
+

{{ item.price | currency:'THB' }}

+
+
+
+
+ + + +app-routing.module.ts + + + +const routes: Routes = [ + { + path: 'goods', + canActivate: [AuthControl], + loadChildren: () => import('./modules/goods/goods.module').then(m => m.GoodsModule) + }, + { path: '**', redirectTo: '/goods' } +]; + + + +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ +MODULE +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + +# สร้างโฟลเดอร์ control +mkdir src/app/controls/login-control +cd src/app/controls/login-control + +# สร้าง module พร้อม routing +ng g module controls/login-control --routing + +# (optional) guard +ng g guard controls/login-control/login + diff --git a/accounting-ng-nuttakit/.editorconfig b/accounting-ng-nuttakit/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/accounting-ng-nuttakit/.editorconfig @@ -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 diff --git a/accounting-ng-nuttakit/.gitignore b/accounting-ng-nuttakit/.gitignore new file mode 100644 index 0000000..aa7d07f --- /dev/null +++ b/accounting-ng-nuttakit/.gitignore @@ -0,0 +1,46 @@ +# 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 + +# Node +/ios +/dist +/android +/node_modules +npm-debug.log +package-lock.json +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 diff --git a/accounting-ng-nuttakit/.postcssrc.json b/accounting-ng-nuttakit/.postcssrc.json new file mode 100644 index 0000000..e092dc7 --- /dev/null +++ b/accounting-ng-nuttakit/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/accounting-ng-nuttakit/.vscode/extensions.json b/accounting-ng-nuttakit/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/accounting-ng-nuttakit/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/accounting-ng-nuttakit/.vscode/launch.json b/accounting-ng-nuttakit/.vscode/launch.json new file mode 100644 index 0000000..e8e08f9 --- /dev/null +++ b/accounting-ng-nuttakit/.vscode/launch.json @@ -0,0 +1,33 @@ +// { +// // 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" +// } +// ] +// } +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Chrome against localhost", + "type": "chrome", + "request": "launch", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}" + } + ] +} diff --git a/accounting-ng-nuttakit/.vscode/tasks.json b/accounting-ng-nuttakit/.vscode/tasks.json new file mode 100644 index 0000000..a298b5b --- /dev/null +++ b/accounting-ng-nuttakit/.vscode/tasks.json @@ -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" + } + } + } + } + ] +} diff --git a/accounting-ng-nuttakit/README.md b/accounting-ng-nuttakit/README.md new file mode 100644 index 0000000..e51cb70 --- /dev/null +++ b/accounting-ng-nuttakit/README.md @@ -0,0 +1,59 @@ +# AccountingNgNuttakit + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.18. + +## 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. diff --git a/accounting-ng-nuttakit/angular.json b/accounting-ng-nuttakit/angular.json new file mode 100644 index 0000000..bbd2c56 --- /dev/null +++ b/accounting-ng-nuttakit/angular.json @@ -0,0 +1,160 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "accounting-ng-nuttakit": { + "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/build:application", + "options": { + "outputPath": "dist/accounting-ng-nuttakit", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "node_modules/bootstrap/dist/css/bootstrap.min.css", + "node_modules/@fortawesome/fontawesome-free/css/all.min.css", + "src/styles.css" + ], + "scripts": [ + "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" + ] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.ts" + } + ], + "optimization": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "1MB", + "maximumError": "2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kB", + "maximumError": "10kB" + } + ], + "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/build:dev-server", + "options": { + "host": "0.0.0.0", + "allowedHosts": ["accounting.nuttakit.work", "localhost"] + }, + "configurations": { + "production": { + "buildTarget": "accounting-ng-nuttakit:build:production" + }, + "development": { + "buildTarget": "accounting-ng-nuttakit:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n" + }, + "test": { + "builder": "@angular/build:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "node_modules/bootstrap/dist/css/bootstrap.min.css", + "node_modules/@fortawesome/fontawesome-free/css/all.min.css", + "src/styles.css" + ], + "scripts": [ + "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" + ] + } + } + } + } + }, + "cli": { + "analytics": false + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } + } +} diff --git a/accounting-ng-nuttakit/capacitor.config.ts b/accounting-ng-nuttakit/capacitor.config.ts new file mode 100644 index 0000000..30bc824 --- /dev/null +++ b/accounting-ng-nuttakit/capacitor.config.ts @@ -0,0 +1,10 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'accounting.nuttakit.work', + appName: 'accounting-ng-nuttakit', + webDir: "dist/accounting-ng-nuttakit/browser", + // bundledWebRuntime: false +}; + +export default config; diff --git a/accounting-ng-nuttakit/package.json b/accounting-ng-nuttakit/package.json new file mode 100644 index 0000000..75bb719 --- /dev/null +++ b/accounting-ng-nuttakit/package.json @@ -0,0 +1,90 @@ +{ + "name": "accounting-ng-nuttakit", + "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", + "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/accounting-ng-nuttakit/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": "^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", + "ng2-charts": "^6.0.1", + "postcss": "^8.5.6", + "rxjs": "~7.8.0", + "tailwindcss": "^4.1.16", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular/build": "^20.3.9", + "@angular/cli": "^20.3.9", + "@angular/compiler-cli": "^20.3.10", + "@capacitor/cli": "latest", + "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", + "ngx-toastr": "^19.1.0", + "postcss": "^8.5.3", + "typescript": "~5.9.3", + "@types/jasmine": "~5.1.0" + } +} \ No newline at end of file diff --git a/accounting-ng-nuttakit/public/logo.png b/accounting-ng-nuttakit/public/logo.png new file mode 100644 index 0000000..b4b482a Binary files /dev/null and b/accounting-ng-nuttakit/public/logo.png differ diff --git a/accounting-ng-nuttakit/src/app/app-routing.module.ts b/accounting-ng-nuttakit/src/app/app-routing.module.ts new file mode 100644 index 0000000..eba6242 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/app-routing.module.ts @@ -0,0 +1,37 @@ +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 = [ + + { path: 'login', loadChildren: () => import('./controls/login-control/login-control.module').then(m => m.LoginControlModule) }, + + { path: 'c', component: LicensePrivacyTermsComponent}, + + { + path: 'main', + component: SidebarContentComponent, + canActivate: [authGuard], + children: [ + { + path: '', + loadChildren: () => + import('./controls/main-control/main-control.module').then( + (m) => m.MainControlModule + ), + }, + ], + }, + + { path: '', redirectTo: 'login', pathMatch: 'full' }, + + { path: '**', redirectTo: 'login' } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule {} diff --git a/accounting-ng-nuttakit/src/app/app.component.css b/accounting-ng-nuttakit/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/accounting-ng-nuttakit/src/app/app.component.html b/accounting-ng-nuttakit/src/app/app.component.html new file mode 100644 index 0000000..0680b43 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/app.component.html @@ -0,0 +1 @@ + diff --git a/accounting-ng-nuttakit/src/app/app.component.ts b/accounting-ng-nuttakit/src/app/app.component.ts new file mode 100644 index 0000000..fa6282a --- /dev/null +++ b/accounting-ng-nuttakit/src/app/app.component.ts @@ -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 = 'accounting-ng-nuttakit'; +} diff --git a/accounting-ng-nuttakit/src/app/app.module.ts b/accounting-ng-nuttakit/src/app/app.module.ts new file mode 100644 index 0000000..90181ec --- /dev/null +++ b/accounting-ng-nuttakit/src/app/app.module.ts @@ -0,0 +1,57 @@ +import { NgModule, Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { ToastrModule } from 'ngx-toastr'; +import { AppRoutingModule } from './app-routing.module'; +// import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +// import { LayoutComponent } from './content/content/layout/layout.component'; +import { SidebarContentComponent } from './content/sidebar-content/sidebar-content.component'; +import { SidebarComponent } from './component/sidebar/sidebar.component'; +// import { ReactiveFormsModule } from '@angular/forms'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { HttpClientModule } from '@angular/common/http'; +import { LicensePrivacyTermsComponent } from './component/license-privacy-terms/license-privacy-terms.component'; +// import { MainDashboardContentComponent } from './content/main-dashboard-content/main-dashboard-content.component'; +// import { MainDashboardComponent } from './component/main-dashboard/main-dashboard.component'; +// import { LoginForgotComponent } from './component/login-forgot/login-forgot.component'; +// import { LoginPageComponent } from './component/login-page/login-page.component'; +// import { LoginContentComponent } from './content/login-content/login-content.component'; + +import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; + +@NgModule({ + declarations: [ + AppComponent, + // LayoutComponent, + SidebarContentComponent, + SidebarComponent, + LicensePrivacyTermsComponent, + // MainDashboardContentComponent, + // MainDashboardComponent, + // LoginForgotComponent, + // LoginPageComponent, + // LoginPageComponentComponent, + ], + imports: [ + BrowserModule, + CommonModule, + ToastrModule.forRoot({ + positionClass:'toast-top-right', + preventDuplicates: true, + maxOpened: 3, + autoDismiss: true + }), + // ReactiveFormsModule, + BrowserAnimationsModule, + AppRoutingModule, + HttpClientModule, + FontAwesomeModule + ], + providers: [provideCharts(withDefaultRegisterables())], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.css b/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.css new file mode 100644 index 0000000..06045f8 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.css @@ -0,0 +1,68 @@ +.policy-container { + display: flex; + justify-content: center; + align-items: flex-start; + background: linear-gradient(135deg, #f3f6f9 0%, #e9eff5 100%); + min-height: 100vh; + padding: 40px 20px; + color: #1a1f36; + font-family: "Sarabun", "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif; +} + +.card { + background: white; + max-width: 800px; + width: 100%; + border-radius: 16px; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08); + padding: 40px; + line-height: 1.7; +} + +.page-title { + font-size: 26px; + font-weight: 600; + margin-bottom: 8px; +} + +.subtitle { + font-size: 13px; + color: #6b737a; + margin-bottom: 32px; +} + +section { + margin-bottom: 40px; +} + +h2 { + font-size: 20px; + margin-bottom: 10px; + color: #0b1a2b; +} + +p { + font-size: 15px; + color: #333; + margin-bottom: 12px; + text-align: justify; +} + +a { + color: #0078d4; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +footer { + text-align: center; + margin-top: 40px; +} + +.footer-text { + color: #6b737a; + font-size: 13px; +} diff --git a/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.html b/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.html new file mode 100644 index 0000000..08da7b8 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.html @@ -0,0 +1,82 @@ +
+
+

ข้อตกลงสิทธิ์การใช้งาน นโยบายความเป็นส่วนตัว และเงื่อนไขการให้บริการ

+

ปรับปรุงล่าสุด: 27 ตุลาคม 2025

+ +
+

1. ข้อตกลงสิทธิ์การใช้งาน (License Agreement)

+

+ ซอร์สโค้ด ส่วนประกอบ และทรัพย์สินการออกแบบทั้งหมดภายใต้โครงการ Nuttakit Software + อยู่ภายใต้สัญญาอนุญาตแบบ MIT License เว้นแต่จะมีการระบุเป็นอย่างอื่นโดยเฉพาะ +

+

+ ท่านได้รับสิทธิ์ในการใช้งาน คัดลอก แก้ไข รวม รวมเข้ากับซอฟต์แวร์อื่น เผยแพร่ หรือแจกจ่ายซอฟต์แวร์นี้ + เพื่อวัตถุประสงค์ส่วนตัวหรือเชิงพาณิชย์ได้ + โดยต้องคงไว้ซึ่งข้อความลิขสิทธิ์และข้อความอนุญาตนี้ในสำเนาทั้งหมดของซอฟต์แวร์ +

+

+ ซอฟต์แวร์นี้ถูกจัดให้ “ตามสภาพ” (AS IS) + โดยไม่มีการรับประกันใด ๆ ไม่ว่าจะโดยชัดแจ้งหรือโดยนัย + รวมถึงแต่ไม่จำกัดเฉพาะการรับประกันความเหมาะสมในการใช้งานหรือความถูกต้องของข้อมูล +

+
+ +
+

2. นโยบายความเป็นส่วนตัว (Privacy Policy)

+

+ NuttakitSoftwareให้ความสำคัญกับความเป็นส่วนตัวของผู้ใช้งาน + แอปพลิเคชันของเราจะเก็บข้อมูลเพียงบางส่วนเท่านั้น เช่น รหัสอุปกรณ์ + บันทึกการทำงาน หรือสถิติการใช้งาน + เพื่อใช้ในการวิเคราะห์ ปรับปรุง และพัฒนาประสิทธิภาพของระบบ +

+

+ ข้อมูลส่วนบุคคล (Personal Identifiable Information — PII) + จะไม่ถูกขาย แบ่งปัน หรือโอนไปยังบุคคลที่สามโดยไม่ได้รับความยินยอมจากท่านอย่างชัดเจน +

+

+ เราใช้มาตรฐานการเข้ารหัสระดับอุตสาหกรรม (AES-CBC) + เพื่อรักษาความปลอดภัยในการส่งข้อมูลระหว่างแอปพลิเคชัน เซิร์ฟเวอร์ และ API +

+

+ ผู้ใช้สามารถร้องขอให้ลบหรือขอรับสำเนาข้อมูลของตนเองได้ตลอดเวลา + โดยติดต่อทีมสนับสนุนของเรา +

+
+ +
+

3. เงื่อนไขการให้บริการ (Terms of Service)

+

+ เมื่อท่านใช้งานซอฟต์แวร์หรือบริการของเรา ถือว่าท่านยอมรับและปฏิบัติตามกฎหมายและข้อบังคับที่เกี่ยวข้องทั้งหมด +

+

+ ท่านจะต้องไม่ใช้ซอฟต์แวร์ของเราในทางที่ผิด + ไม่พยายามถอดรหัส แก้ไข ดัดแปลง หรือแสวงหาประโยชน์จากช่องโหว่ในระบบโดยไม่ได้รับอนุญาต +

+

+ Nuttakit ขอสงวนสิทธิ์ในการแก้ไขหรือยุติการให้บริการโดยไม่ต้องแจ้งให้ทราบล่วงหน้า + หากตรวจพบการละเมิดความปลอดภัยหรือการใช้งานที่ไม่เหมาะสม +

+

+ ทีมพัฒนา Nuttakit และผู้ร่วมพัฒนาไม่รับผิดชอบต่อความเสียหายใด ๆ + ที่อาจเกิดจากการใช้งานหรือไม่สามารถใช้งานซอฟต์แวร์นี้ได้ +

+
+ +
+

4. ช่องทางการติดต่อ (Contact)

+

+ หากท่านมีข้อสงสัยหรือข้อกังวลเกี่ยวกับข้อมูลส่วนบุคคลหรือเงื่อนไขการให้บริการ + สามารถติดต่อเราได้ที่: +
+ อีเมล: support@nuttakit.work
+ เว็บไซต์: + https://nuttakit.work +

+
+ +
+
+ +
+
+
diff --git a/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.ts b/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.ts new file mode 100644 index 0000000..9f460a8 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/license-privacy-terms/license-privacy-terms.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-license-privacy-terms', + standalone: false, + templateUrl: './license-privacy-terms.component.html', + styleUrl: './license-privacy-terms.component.css' +}) +export class LicensePrivacyTermsComponent { + +} diff --git a/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.css b/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.css new file mode 100644 index 0000000..cd46dbe --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.css @@ -0,0 +1,290 @@ +:root { + --bg-1: #f3f6f9; + --card-bg: #ffffff; + --muted: #6b737a; + --text: #0b1a2b; + --primary: #0078d4; + --primary-600: #0065b8; + --radius: 8px; + --shadow: 0 10px 30px rgba(11,26,43,0.08); + --glass: rgba(255,255,255,0.6); +} + +/* Page layout */ +.login-widget { + /* Fill the viewport and center the card. Do not let the page itself + scroll; the card gets an internal max-height instead. */ + min-height: 100vh; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 28px 18px; + background: linear-gradient(180deg, #f7f9fb 0%, var(--bg-1) 100%); + color: var(--text); +} + + +/* Card */ +.login-widget .card{ + width: 380px; + max-width: calc(100% - 40px); + background: var(--card-bg); + border-radius: calc(var(--radius) + 2px); + box-shadow: var(--shadow); + padding: 22px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 14px; + /* Constrain the card so it never forces the page to scroll. If content + grows, the card will scroll internally. */ + max-height: calc(100vh - 56px); + overflow: auto; +} + +/* Modal/backdrop styles */ +.login-backdrop{ + position: fixed; + inset: 0; /* top:0; right:0; bottom:0; left:0; */ + background: rgba(0,0,0,0.38); + display: flex; + align-items: center; + justify-content: center; + z-index: 1040; + padding: 24px; +} + +.login-modal{ width: 480px; max-width: 480px; } + +.modal-card{ + border-radius: 12px; + padding: 0; /* card children control internal padding */ + overflow: hidden; + box-shadow: 0 20px 50px rgba(2,6,23,0.4); +} + +/* Slightly larger brand area inside modal */ +.modal-card .brand{ padding: 18px; } + +/* Make the primary button pill-shaped and slightly larger */ +button.primary{ + color: #000; + border-radius: 999px; + padding: 10px 18px; + font-size: 15px; +} + +/* Make biometric and other action buttons visually lighter */ +.biometric{ + border-radius: 999px; + padding: 8px 12px; +} + +/* On small screens reduce modal padding and width to avoid overflow */ +@media (max-width: 420px){ + .login-backdrop{ padding: 12px; } + .login-modal{ max-width: 100%; } + .modal-card .brand{ padding: 12px; } +} + +/* Brand area */ +.brand{ + text-align: center; + padding-bottom: 4px; + border-bottom: 1px solid #eef2f5; +} +.brand .logo{ + height: 44px; + width: 44px; + object-fit: contain; + display: inline-block; + margin-bottom: 10px; +} +.brand h1{ + margin: 0; + font-size: 20px; + font-weight: 600; + letter-spacing: -0.2px; + color: var(--text); +} +.brand .subtitle{ + margin: 6px 0 12px; + color: var(--muted); + font-size: 13px; +} + +/* Form area */ +.form{ + /* keep compact spacing inside the card */ + /* width: 410px; */ + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 12px; + padding: 6px 0 2px; +} + +/* Field label wrapper */ +.field{ + display: flex; + flex-direction: column; + gap: 6px; +} +.field .label-text{ + font-size: 13px; + color: var(--muted); +} + +/* Inputs */ +input[type="email"], +input[type="password"], +input[type="text"]{ + width: 100%; + box-sizing: border-box; + padding: 10px 12px; + font-size: 15px; + color: var(--text); + background: #fff; + border: 1px solid #d8dee6; + border-radius: 6px; + outline: none; + transition: box-shadow .14s ease, border-color .14s ease, transform .06s ease; + -webkit-appearance: none; + appearance: none; +} + +input::placeholder{ + color: #9aa3ad; +} +input:focus{ + border-color: var(--primary); + box-shadow: 0 6px 20px rgba(0,120,212,0.10); + transform: translateZ(0); +} + +/* Checkbox / stay signed */ +.stay-signed{ + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 13px; + color: var(--muted); +} +.stay-signed input[type="checkbox"]{ + width: 16px; + height: 16px; + accent-color: var(--primary); +} + +/* Actions row */ +.actions{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 4px; +} +button.primary{ + background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%); + color: #000000; + border: none; + padding: 10px 14px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 14px; + box-shadow: 0 6px 18px rgba(0,120,212,0.12); + transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease; +} +button.primary:hover:not(:disabled){ + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(0,120,212,0.14); +} +button.primary:active{ + transform: translateY(0); +} +button.primary:disabled{ + opacity: 0.55; + cursor: not-allowed; + box-shadow: none; +} + +/* Alternative options */ +.alt-options{ + display: flex; + align-items: center; + gap: 12px; + margin-top: 6px; + flex-wrap: wrap; +} +.biometric{ + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + background: transparent; + color: var(--primary); + border: 1px solid rgba(0,120,212,0.14); + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 13px; +} +.biometric svg{ display: block; opacity: .95; } +.biometric:hover{ + background: rgba(0,120,212,0.04); +} + +/* Help link */ +.help-link{ + margin-left: auto; + font-size: 13px; + color: var(--primary); + text-decoration: none; +} +.help-link:hover{ text-decoration: underline; } + +/* Footer */ +.footer{ + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin-top: 6px; + padding-top: 10px; + border-top: 1px solid #eef2f5; + font-size: 13px; + color: var(--muted); +} +.footer a{ + color: var(--primary); + text-decoration: none; + font-weight: 600; +} +.footer a:hover{ text-decoration: underline; } +.divider{ color: #d0d6db; } + +/* Focus styles for keyboard users */ +:focus{ + outline: none; +} +:focus-visible{ + outline: 3px solid rgba(0,120,212,0.12); + outline-offset: 2px; + border-radius: 6px; +} + +/* Small screens */ +@media (max-width:420px){ + .login-widget .card{ + padding: 18px; + width: 100%; + } + .brand h1{ font-size: 18px; } + .brand .subtitle{ + font-family: "Kanit"; + font-weight: 1000; + font-style: normal; } + .biometric span, .primary{ font-size: 13px; } +} diff --git a/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.html b/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.html new file mode 100644 index 0000000..660b8f7 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.html @@ -0,0 +1,105 @@ + diff --git a/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.ts b/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.ts new file mode 100644 index 0000000..f9ff117 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/login-forgot/login-forgot.component.ts @@ -0,0 +1,91 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { GeneralService } from '../../services/generalservice'; + + +@Component({ + selector: 'app-login-forgot', + standalone: false, + templateUrl: './login-forgot.component.html', + styleUrl: './login-forgot.component.css' +}) +export class LoginForgotComponent implements OnInit { + // @Input() brandName = 'Contoso'; + // @Input() subtitle = 'to your account'; + // @Input() mode = ''; + @Output() otpEventSubmit = new EventEmitter(); + @Output() otpVerifyEventSubmit = new EventEmitter(); + + forgotFrm!: FormGroup; + isLoading: boolean = false; + isSendOtp: boolean = false; + isModalOpen: boolean = false; + // busy = false; + // message = ''; + + constructor( + // private generalService: GeneralService + // private fb: FormBuilder + ) {} + + ngOnInit(): void { + this.setupFormControl(); + } + + setupFormControl(){ + this.forgotFrm = new FormGroup({ + email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]), + otp: new FormControl('',[Validators.required, Validators.maxLength(6)]), + newPassword: new FormControl('',[Validators.required, Validators.maxLength(50)]), + confirmPassword: new FormControl('',[Validators.required, Validators.maxLength(50)]) + }); + + } + + EventSubmit(event: any){ + this.otpEventSubmit.emit(event); + } + + + VerifyEventSubmit(event: any){ + this.otpVerifyEventSubmit.emit(event); + } + + onSubmin(){ + let data = { + email: this.forgotFrm.get('email')?.value + } + + this.EventSubmit(data); + } + + onVerifySubmit(){ + let data = { + email: this.forgotFrm.get('email')?.value, + otp: this.forgotFrm.get('otp')?.value + } + this.VerifyEventSubmit(data); + } + + onSetNewPassword(){ + let newpassword = this.forgotFrm.get('newPassword')?.value; + let confirmPassword = this.forgotFrm.get('confirmPassword')?.value; + + let data = { + email: this.forgotFrm.get('email')?.value, + otp: this.forgotFrm.get('otp')?.value, + newPassword: newpassword + } + + if (newpassword.trim() === confirmPassword.trim()) { + // this.VerifyEventSubmit(data); + console.log("Password matched! (รหัสผ่านตรงกัน)"); + } else { + console.error("Password mismatched! (รหัสผ่านไม่ตรงกัน)"); + } + + // console.log(newpassword.value); + + } + // otp: } +} diff --git a/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.css b/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.css new file mode 100644 index 0000000..5a4bcfd --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.css @@ -0,0 +1,289 @@ +:root { + --bg-1: #f3f6f9; + --card-bg: #ffffff; + --muted: #6b737a; + --text: #0b1a2b; + --primary: #0078d4; + --primary-600: #0065b8; + --radius: 8px; + --shadow: 0 10px 30px rgba(11,26,43,0.08); + --glass: rgba(255,255,255,0.6); +} + +/* Page layout */ +.login-widget { + /* Fill the viewport and center the card. Do not let the page itself + scroll; the card gets an internal max-height instead. */ + min-height: 100vh; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 28px 18px; + background: linear-gradient(180deg, #f7f9fb 0%, var(--bg-1) 100%); + color: var(--text); +} + + +/* Card */ +.login-widget .card{ + width: 380px; + max-width: calc(100% - 40px); + background: var(--card-bg); + border-radius: calc(var(--radius) + 2px); + box-shadow: var(--shadow); + padding: 22px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 14px; + /* Constrain the card so it never forces the page to scroll. If content + grows, the card will scroll internally. */ + max-height: calc(100vh - 56px); + overflow: auto; +} + +/* Modal/backdrop styles */ +.login-backdrop{ + position: fixed; + inset: 0; /* top:0; right:0; bottom:0; left:0; */ + background: rgba(0,0,0,0.38); + display: flex; + align-items: center; + justify-content: center; + z-index: 1040; + padding: 24px; +} + +.login-modal{ width: 480px; max-width: 480px; } + +.modal-card{ + border-radius: 12px; + padding: 0; /* card children control internal padding */ + overflow: hidden; + box-shadow: 0 20px 50px rgba(2,6,23,0.4); +} + +/* Slightly larger brand area inside modal */ +.modal-card .brand{ padding: 18px; } + +/* Make the primary button pill-shaped and slightly larger */ +button.primary{ + color: #000; + border-radius: 999px; + padding: 10px 18px; + font-size: 15px; +} + +/* Make biometric and other action buttons visually lighter */ +.biometric{ + border-radius: 999px; + padding: 8px 12px; +} + +/* On small screens reduce modal padding and width to avoid overflow */ +@media (max-width: 420px){ + .login-backdrop{ padding: 12px; } + .login-modal{ max-width: 100%; } + .modal-card .brand{ padding: 12px; } +} + +/* Brand area */ +.brand{ + text-align: center; + padding-bottom: 4px; + border-bottom: 1px solid #eef2f5; +} +.brand .logo{ + height: 44px; + width: 44px; + object-fit: contain; + display: inline-block; + margin-bottom: 10px; +} +.brand h1{ + margin: 0; + font-size: 20px; + font-weight: 600; + letter-spacing: -0.2px; + color: var(--text); +} +.brand .subtitle{ + margin: 6px 0 12px; + color: var(--muted); + font-size: 13px; +} + +/* Form area */ +.form{ + /* keep compact spacing inside the card */ + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 12px; + padding: 6px 0 2px; +} + +/* Field label wrapper */ +.field{ + display: flex; + flex-direction: column; + gap: 6px; +} +.field .label-text{ + font-size: 13px; + color: var(--muted); +} + +/* Inputs */ +input[type="email"], +input[type="password"], +input[type="text"]{ + width: 100%; + box-sizing: border-box; + padding: 10px 12px; + font-size: 15px; + color: var(--text); + background: #fff; + border: 1px solid #d8dee6; + border-radius: 6px; + outline: none; + transition: box-shadow .14s ease, border-color .14s ease, transform .06s ease; + -webkit-appearance: none; + appearance: none; +} + +input::placeholder{ + color: #9aa3ad; +} +input:focus{ + border-color: var(--primary); + box-shadow: 0 6px 20px rgba(0,120,212,0.10); + transform: translateZ(0); +} + +/* Checkbox / stay signed */ +.stay-signed{ + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 13px; + color: var(--muted); +} +.stay-signed input[type="checkbox"]{ + width: 16px; + height: 16px; + accent-color: var(--primary); +} + +/* Actions row */ +.actions{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 4px; +} +button.primary{ + background: linear-gradient(180deg, var(--primary) 0%, var(--primary-600) 100%); + color: #000000; + border: none; + padding: 10px 14px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 14px; + box-shadow: 0 6px 18px rgba(0,120,212,0.12); + transition: transform .06s ease, box-shadow .12s ease, opacity .12s ease; +} +button.primary:hover:not(:disabled){ + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(0,120,212,0.14); +} +button.primary:active{ + transform: translateY(0); +} +button.primary:disabled{ + opacity: 0.55; + cursor: not-allowed; + box-shadow: none; +} + +/* Alternative options */ +.alt-options{ + display: flex; + align-items: center; + gap: 12px; + margin-top: 6px; + flex-wrap: wrap; +} +.biometric{ + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + background: transparent; + color: var(--primary); + border: 1px solid rgba(0,120,212,0.14); + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 13px; +} +.biometric svg{ display: block; opacity: .95; } +.biometric:hover{ + background: rgba(0,120,212,0.04); +} + +/* Help link */ +.help-link{ + margin-left: auto; + font-size: 13px; + color: var(--primary); + text-decoration: none; +} +.help-link:hover{ text-decoration: underline; } + +/* Footer */ +.footer{ + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin-top: 6px; + padding-top: 10px; + border-top: 1px solid #eef2f5; + font-size: 13px; + color: var(--muted); +} +.footer a{ + color: var(--primary); + text-decoration: none; + font-weight: 600; +} +.footer a:hover{ text-decoration: underline; } +.divider{ color: #d0d6db; } + +/* Focus styles for keyboard users */ +:focus{ + outline: none; +} +:focus-visible{ + outline: 3px solid rgba(0,120,212,0.12); + outline-offset: 2px; + border-radius: 6px; +} + +/* Small screens */ +@media (max-width:420px){ + .login-widget .card{ + padding: 18px; + width: 100%; + } + .brand h1{ font-size: 18px; } + .brand .subtitle{ + font-family: "Kanit"; + font-weight: 1000; + font-style: normal; } + .biometric span, .primary{ font-size: 13px; } +} diff --git a/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.html b/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.html new file mode 100644 index 0000000..72d7e2a --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.html @@ -0,0 +1,55 @@ + + diff --git a/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.ts b/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.ts new file mode 100644 index 0000000..a032f77 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/login-page/login-page.component.ts @@ -0,0 +1,90 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { faCoffee } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'app-login-page', + standalone: false, + templateUrl: './login-page.component.html', + styleUrls: ['./login-page.component.css'], +}) +export class LoginPageComponent implements OnInit { + @Input() brandName = 'Contoso'; + @Input() subtitle = 'to your account'; + @Input() mode = ''; + @Output() signedIn = new EventEmitter(); + + faCoffee = faCoffee; + loginForm!: FormGroup; + busy = false; + message = ''; + + constructor( + private router: Router + ) {} + + ngOnInit(): void { + this.setupFormControl(); + } + + setupFormControl(): void { + this.loginForm = new FormGroup({ + username: new FormControl('',[Validators.required, Validators.maxLength(100)]), + password: new FormControl( '', [Validators.required, Validators.maxLength(50)]), + remember: new FormControl(false) + }); + } + + signIn(): void { + if (this.loginForm.invalid) return; + this.signedIn.emit(this.loginForm.value); + } + + async useBiometric(): Promise { + this.message = ''; + if (!('credentials' in navigator) || !('get' in (navigator as any).credentials)) { + this.message = 'Biometric authentication is not available on this device/browser.'; + return; + } + + try { + this.busy = true; + // Example WebAuthn / PublicKeyCredential call. In a real application, + // you must obtain the challenge and allowedCredentials from your server. + const publicKeyCredentialRequestOptions = { + // challenge must be provided by server as ArrayBuffer + challenge: Uint8Array.from('server-provided-challenge', c => c.charCodeAt(0)).buffer, + timeout: 60000, + userVerification: 'preferred', + } as any; + + const credential: any = await (navigator as any).credentials.get({ + publicKey: publicKeyCredentialRequestOptions, + }); + + // Send credential to server for verification (not implemented here). + // Example: await this.authService.verifyWebAuthn(credential); + + // On success: + this.signedIn.emit({ email: '', remember: true }); + } catch (err: any) { + this.message = err?.message || 'Biometric sign-in cancelled or failed.'; + } finally { + this.busy = false; + } + } + + forgotPassword(): void { + // emit or navigate + this.router.navigate(['/login/forgot-password']); + } + + createAccount(): void { + this.message = 'Create account flow not implemented.'; + } + + privacy(): void { + this.router.navigate(['/license']); + } +} diff --git a/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.css b/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.css new file mode 100644 index 0000000..d7ecda0 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.css @@ -0,0 +1,629 @@ +:host { + display: block; + padding: 2rem clamp(1.25rem, 4vw, 3rem) 3rem; + background: radial-gradient(120% 120% at 0% 0%, #f6f8ff 0%, #eef5ff 55%, #ffffff 100%); + min-height: 100%; +} + +.dashboard { + display: flex; + flex-direction: column; + gap: 2rem; + max-width: 1280px; + margin: 0 auto; +} + +.dashboard__hero { + background: #0f172a; + color: #f8fafc; + padding: 2rem; + border-radius: 28px; + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + justify-content: space-between; + align-items: center; + box-shadow: 0 20px 50px rgba(15, 23, 42, 0.3); +} + +.hero__text h1 { + margin: 0 0 0.5rem; + font-size: clamp(1.8rem, 3vw, 2.5rem); +} + +.hero__subtitle { + margin: 0; + color: rgba(248, 250, 252, 0.7); +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.8rem; + color: rgba(248, 250, 252, 0.8); + margin: 0 0 0.5rem; +} + +.hero__actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.65rem 1.5rem; + 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.35); +} + +.btn--ghost { + background: rgba(248, 250, 252, 0.12); + color: #f8fafc; + border: 1px solid rgba(248, 250, 252, 0.2); +} + +.btn--compact { + padding: 0.45rem 1.15rem; + font-size: 0.9rem; +} + +.btn:focus-visible { + outline: 3px solid rgba(14, 165, 233, 0.4); + outline-offset: 3px; +} + +.btn:hover { + transform: translateY(-1px); +} + +.dashboard__stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 1rem; +} + +.dashboard__periods { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1rem; +} + +.period-card { + background: rgba(15, 23, 42, 0.85); + color: #f8fafc; + border-radius: 22px; + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + box-shadow: 0 18px 35px rgba(15, 23, 42, 0.4); +} + +.period-card__header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; + color: rgba(248, 250, 252, 0.75); +} + +.period-card__badge { + padding: 0.2rem 0.75rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; +} + +.period-card__badge--year { background: rgba(248, 250, 252, 0.14); } +.period-card__badge--month { background: rgba(125, 211, 252, 0.25); } +.period-card__badge--week { background: rgba(110, 231, 183, 0.2); } + +.period-card__values { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.75rem; +} + +.muted { + margin: 0; + font-size: 0.8rem; + color: rgba(248, 250, 252, 0.65); +} + +.income, +.expense, +.net { + margin: 0.1rem 0 0; + font-size: 1.2rem; + font-weight: 600; +} + +.income { color: #34d399; } +.expense { color: #fbbf24; } +.net { color: #38bdf8; } + +.trend-chip { + background: rgba(248, 250, 252, 0.12); + padding: 0.35rem 0.9rem; + border-radius: 999px; + font-size: 0.85rem; + font-weight: 600; + align-self: flex-start; +} + +.stat-card { + background: #ffffff; + border-radius: 20px; + padding: 1.25rem; + display: flex; + gap: 1rem; + align-items: center; + box-shadow: 0 8px 30px rgba(15, 23, 42, 0.08); +} + +.stat-card__icon { + width: 52px; + height: 52px; + border-radius: 16px; + flex-shrink: 0; + background: #e2e8f0; +} + +.accent-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); } +.accent-lavender { background: linear-gradient(135deg, #ddd6fe, #a78bfa); } +.accent-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); } +.accent-teal { background: linear-gradient(135deg, #99f6e4, #14b8a6); } + +.stat-card__label { + margin: 0; + color: #475569; + font-size: 0.9rem; +} + +.stat-card__value { + font-size: 1.5rem; + font-weight: 700; + color: #0f172a; +} + +.stat-card__trend { + margin: 0; + color: #64748b; + font-size: 0.85rem; +} + +.dashboard__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 1.5rem; +} + +.panel { + background: #ffffff; + border-radius: 24px; + padding: 1.5rem; + box-shadow: 0 12px 35px rgba(15, 23, 42, 0.08); + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.panel--main { + grid-column: span 2; + min-height: 280px; +} + +.panel--side { + grid-column: span 1; +} + +@media (max-width: 900px) { + .panel--main { + grid-column: span 1; + } +} + +.panel__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.panel__header h2 { + margin: 0; + font-size: 1.2rem; +} + +.panel__header p { + margin: 0; + color: #94a3b8; + font-size: 0.9rem; +} + +.ledger-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 1.5rem; +} + +.quick-log__form { + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.quick-log__form label { + display: flex; + flex-direction: column; + gap: 0.4rem; + font-size: 0.9rem; + color: #475569; +} + +.quick-log__form input, +.quick-log__form textarea { + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 0.75rem 1rem; + font-family: inherit; + font-size: 0.95rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.quick-log__form input:focus, +.quick-log__form textarea:focus { + border-color: #0ea5e9; + box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15); + outline: none; +} + +.quick-log__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; +} + +.quick-log__toggle { + display: inline-flex; + gap: 0.4rem; + background: #f1f5f9; + border-radius: 999px; + padding: 0.25rem; +} + +.toggle-btn { + border: none; + background: transparent; + border-radius: 999px; + padding: 0.4rem 1.1rem; + font-weight: 600; + color: #475569; + cursor: pointer; +} + +.toggle-btn.is-active { + background: #0ea5e9; + color: #f8fafc; +} + +.ledger-table { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.pie-panel__content { + display: grid; + grid-template-columns: minmax(220px, 1fr) 1fr; + gap: 2rem; + align-items: center; +} + +.pie-chart { + width: 220px; + height: 220px; + border-radius: 50%; + position: relative; + margin: 0 auto; + 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: 120px; + height: 120px; + border-radius: 50%; + background: #ffffff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + box-shadow: 0 10px 25px rgba(15, 23, 42, 0.1); +} + +.pie-chart__center p { + margin: 0; + font-size: 0.85rem; + color: #94a3b8; +} + +.pie-chart__center strong { + font-size: 1.2rem; + color: #0f172a; +} + +.pie-legend { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.pie-legend__item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.4rem 0; +} + +.swatch { + width: 14px; + height: 14px; + border-radius: 4px; +} + +.pie-legend__label { + margin: 0; + font-weight: 600; + color: #0f172a; +} + +.pie-legend__value { + margin: 0; + color: #94a3b8; + font-size: 0.85rem; +} + +.ledger-row { + display: grid; + grid-template-columns: 2fr 1fr 0.8fr 1.2fr; + gap: 1rem; + align-items: center; + padding: 0.75rem 0.4rem; + border-bottom: 1px solid #e2e8f0; +} + +.ledger-head { + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.08em; + color: #94a3b8; +} + +.ledger-main { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.pill { + padding: 0.2rem 0.7rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 600; +} + +.pill--income { + background: rgba(16, 185, 129, 0.12); + color: #059669; +} + +.pill--expense { + background: rgba(248, 113, 113, 0.15); + color: #dc2626; +} + +.ledger-title { + margin: 0; + font-weight: 600; +} + +.ledger-date { + margin: 0; + font-size: 0.85rem; + color: #94a3b8; +} + +.ledger-category { + font-weight: 500; + color: #475569; +} + +.ledger-amount { + font-weight: 700; +} + +.ledger-note { + color: #64748b; + font-size: 0.9rem; +} + +.trend-chart { + display: flex; + gap: 1rem; + align-items: flex-end; + min-height: 180px; +} + +.trend-chart__bar { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.trend-chart__value { + width: 100%; + border-radius: 16px 16px 6px 6px; + background: linear-gradient(180deg, rgba(14, 165, 233, 0.8) 0%, rgba(56, 189, 248, 0.4) 100%); +} + +.trend-chart__label { + font-size: 0.85rem; + color: #475569; +} + +.ratio-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.ratio { + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 18px; + padding: 0.85rem 1.2rem; + font-weight: 600; +} + +.ratio--positive { + background: rgba(16, 185, 129, 0.12); + color: #047857; +} + +.ratio--neutral { + background: rgba(59, 130, 246, 0.12); + color: #1d4ed8; +} + +.ratio--warning { + background: rgba(251, 191, 36, 0.15); + color: #b45309; +} + +.alerts-panel .alert { + display: flex; + justify-content: space-between; + align-items: center; + background: #f8fafc; + border-radius: 18px; + padding: 1rem 1.2rem; + gap: 1rem; +} + +.alert__title { + margin: 0 0 0.3rem; + font-weight: 600; + color: #0f172a; +} + +.alert__detail { + margin: 0; + color: #64748b; + font-size: 0.9rem; +} + +.alert__tag { + padding: 0.4rem 0.9rem; + border-radius: 999px; + background: #e0f2fe; + color: #0369a1; + font-size: 0.85rem; + font-weight: 600; +} + +.task-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.task { + background: #f8fafc; + border-radius: 18px; + padding: 1rem 1.2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.task__title { + margin: 0 0 0.25rem; + font-weight: 600; +} + +.task__due { + margin: 0; + color: #94a3b8; + font-size: 0.9rem; +} + +.task__badge { + padding: 0.35rem 0.8rem; + border-radius: 12px; + background: #e2e8f0; + font-weight: 600; +} + +.is-credit { + color: #10b981; + font-weight: 600; +} + +.is-debit { + color: #ef4444; + font-weight: 600; +} + +@media (max-width: 640px) { + .dashboard__hero, + .panel { + padding: 1.25rem; + } + + .quick-log__grid { + grid-template-columns: 1fr; + } + + .pie-panel__content { + grid-template-columns: 1fr; + } + + .pie-chart { + width: 180px; + height: 180px; + } + + .ledger-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .ledger-row span:nth-child(3), + .ledger-row span:nth-child(4) { + text-align: left; + } +} diff --git a/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.html b/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.html new file mode 100644 index 0000000..dd50ebc --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.html @@ -0,0 +1,332 @@ +
+
+
+

ภาพรวมบัญชี

+

ยินดีต้อนรับกลับ, {{ ownerName }}

+

+ จดบันทึกรายรับรายจ่าย และดูสรุปต่อปี เดือน สัปดาห์ ได้ในหน้าเดียว +

+
+
+ + +
+
+ +
+
+
+ + {{ summary.label }} + +

{{ summary.note }}

+
+
+
+

รายรับ

+

{{ summary.income }}

+
+
+

รายจ่าย

+

{{ summary.expense }}

+
+
+

คงเหลือสุทธิ

+

{{ summary.net }}

+
+
+
+ แนวโน้ม {{ summary.trend }} +
+
+
+ +
+
+
+
+

{{ card.label }}

+
{{ card.value }}
+

{{ card.trend }} · {{ card.context }}

+
+
+
+ +
+
+
+
+

บันทึกรายการแบบรวดเร็ว

+

จดรายรับรายจ่ายภายในไม่กี่คลิก

+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+
+

สมุดบันทึกล่าสุด

+

แยกสีระหว่างรายรับและรายจ่าย

+
+ +
+
+
+ รายการ + หมวดหมู่ + ยอดเงิน + บันทึก +
+
+
+ + {{ idx.type == 'i' ? 'รับ' : 'จ่าย' }} + +
+

{{ idx.title }}

+

{{ idx.date }}

+
+
+ {{ idx.category }} + + {{ idx.amount }} + + {{ idx.note }} +
+
+
+
+ +
+ + +
+
+
+

สัดส่วนค่าใช้จ่าย

+

ดูหมวดไหนใช้เงินมากที่สุด

+
+ +
+
+
+
+

รวมเดือนนี้

+ ฿732K +
+
+
    +
  • + +
    +

    {{ part.label }}

    +

    {{ part.value }}%

    +
    +
  • +
+
+
+ +
+
+
+

สรุปสภาพคล่อง

+

อัปเดตล่าสุด 5 นาทีที่แล้ว

+
+
+
+
+
+

+ {{ ratio.label }} +

+ {{ ratio.value }} +
+
+
+
+ +
+
+
+

การแจ้งเตือนสำคัญ

+

จัดลำดับงานค้างก่อนครบกำหนด

+
+
+
+
+

{{ alert.title }}

+

{{ alert.detail }}

+
+ {{ alert.tag }} +
+
+ +
+
+
+

รายการยอดค้างจ่าย

+

ช่วยเตือนความจำให้

+
+ +
+
    +
  • +
    +

    {{ task.title }}

    +

    {{ task.due }}

    +
    + {{ task.priority }} +
  • +
+
+ +
+
+ + +@if(isModalOpen == true){ + +} diff --git a/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.ts b/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.ts new file mode 100644 index 0000000..3d6ba96 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/main-dashboard/main-dashboard.component.ts @@ -0,0 +1,220 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { GeneralService } from '../../services/generalservice'; + +@Component({ + selector: 'app-main-dashboard', + standalone: false, + templateUrl: './main-dashboard.component.html', + styleUrl: './main-dashboard.component.css' +}) +export class MainDashboardComponent implements OnInit { + + mode: string = 'i'; + isModalOpen: boolean = false; + isSubmitting: boolean = false; + arrearsForm!: FormGroup; + saveFrm!: FormGroup; + + + readonly ownerName = 'Nuttakit'; + + readonly kpiCards = [ + { + label: 'รายรับรวม', + value: '฿1.28M', + trend: '+12.4%', + context: 'เทียบกับเดือนก่อน', + accent: 'mint' + }, + { + label: 'รายจ่ายรวม', + value: '฿732K', + trend: '-4.1%', + context: 'จัดการได้ดีขึ้น', + accent: 'lavender' + }, + { + label: 'ยอดค้างชำระ', + value: '฿184K', + trend: '-2 ใบแจ้งหนี้', + context: 'รอติดตาม', + accent: 'amber' + }, + { + label: 'อัตรากำไร', + value: '37.8%', + trend: '+1.9 จุด', + context: 'ระยะ 30 วัน', + accent: 'teal' + } + ]; + + readonly revenueTrend = [ + { label: 'ม.ค.', value: 52 }, + { label: 'ก.พ.', value: 61 }, + { label: 'มี.ค.', value: 73 }, + { label: 'เม.ย.', value: 68 }, + { label: 'พ.ค.', value: 82 }, + { label: 'มิ.ย.', value: 77 } + ]; + + readonly quickRatios = [ + { label: 'กระแสเงินสด', value: '+฿312K', status: 'positive' }, + { label: 'วงเงินคงเหลือ', value: '฿890K', status: 'neutral' }, + { label: 'ค่าใช้จ่ายเดือนนี้', value: '฿412K', status: 'warning' } + ]; + + readonly periodSummaries = [ + { + label: 'รายปี', + note: 'ปี 2567', + income: '฿9.6M', + expense: '฿5.1M', + net: '+฿4.5M', + trend: '+18%', + badge: 'year' + }, + { + label: 'รายเดือน', + note: 'มิถุนายน 2567', + income: '฿1.28M', + expense: '฿732K', + net: '+฿548K', + trend: '+6%', + badge: 'month' + }, + { + label: 'รายสัปดาห์', + note: 'สัปดาห์ที่ 24', + income: '฿312K', + expense: '฿188K', + net: '+฿124K', + trend: '+2%', + badge: 'week' + } + ]; + + readonly alerts = [ + { + title: 'ใบแจ้งหนี้ #INV-083 จะครบกำหนด', + detail: 'ลูกค้า Metro Engineering', + tag: 'ภายใน 3 วัน' + }, + { + title: 'มีเอกสารที่ต้องอนุมัติ 2 รายการ', + detail: 'เบิกค่าใช้จ่ายฝ่ายการตลาด', + tag: 'รออนุมัติ' + }, + { + title: 'พบรายการใช้จ่ายผิดปกติ', + detail: 'ค่าใช้จ่ายเดินทางสูงกว่าค่าเฉลี่ย 28%', + tag: 'ตรวจสอบ' + } + ]; + + readonly tasks = [ + { + title: 'กระทบยอดธนาคาร เดือน มิ.ย.', + due: 'วันนี้ 16:00', + priority: 'สูง' + }, + { + title: 'เตรียมรายงาน VAT', + due: 'พรุ่งนี้ 10:30', + priority: 'กลาง' + }, + { + title: 'ออกใบเสนอราคา โครงการใหม่', + due: 'ศุกร์ 14:00', + priority: 'ต่ำ' + } + ]; + + readonly ledgerEntries = [ + { + type: 'i', + title: 'ค่าบริการที่ปรึกษา', + category: 'บริการ', + amount: '+฿85,000', + date: 'วันนี้ · 10:15', + note: 'โครงการ Warehouse Automation' + }, + { + type: 'e', + title: 'ค่าเช่าออฟฟิศ', + category: 'ค่าใช้จ่ายคงที่', + amount: '-฿48,000', + date: 'วันนี้ · 09:00', + note: 'สำนักงานพระราม 9' + }, + { + type: 'i', + title: 'รับเงินมัดจำ', + category: 'สัญญาใหม่', + amount: '+฿120,000', + date: 'เมื่อวาน', + note: 'ลูกค้า Urbane CoWorking' + }, + { + type: 'e', + title: 'ค่าวัตถุดิบ', + category: 'ต้นทุนโครงการ', + amount: '-฿27,500', + date: '12 มิ.ย.', + note: 'สั่งผ่าน Blue Supply' + } + ]; + + readonly expenseBreakdown = [ + { label: 'ฝ่ายบริหาร', value: 32, color: '#0ea5e9' }, + { label: 'การตลาด', value: 18, color: '#f97316' }, + { label: 'ต้นทุนโครงการ', value: 27, color: '#10b981' }, + { label: 'บุคลากร', value: 15, color: '#a855f7' }, + { label: 'อื่นๆ', value: 8, color: '#e11d48' } + ]; + + readonly expenseGradient = this.buildExpenseGradient(); + + ngOnInit(): void { + this.setupFormControl(); + } + + setupFormControl(){ + this.arrearsForm = new FormGroup({ + // email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]), + amount: new FormControl('',[Validators.required, Validators.maxLength(20)]), + expdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]), + note: new FormControl('',[Validators.maxLength(200)]), + reason: new FormControl('',[Validators.required, Validators.maxLength(200)]) + }); + + this.saveFrm = new FormGroup({ + actacpdtm: new FormControl('',[Validators.required, Validators.maxLength(12)]), + actqty: new FormControl('',[Validators.required]), + actcat: new FormControl('',[Validators.required, Validators.maxLength(1)]), + actcmt: new FormControl('',[Validators.maxLength(200)]) + }); + } + + onSaveSubmit(){ + + } + + onArrearsSubmit(){ + + } + + private buildExpenseGradient(): string { + let current = 0; + const segments = this.expenseBreakdown + .map(part => { + const start = current; + const end = current + part.value; + current = end; + return `${part.color} ${start}% ${end}%`; + }) + .join(', '); + return `conic-gradient(${segments})`; + } +} diff --git a/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.css b/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.css new file mode 100644 index 0000000..4d8834e --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.css @@ -0,0 +1,39 @@ +/* .sidebar { + width: 220px; + background: #222; + color: white; + height: 100vh; + padding: 20px; +} */ +/* .sidebar ul { + list-style: none; + padding: 0; +} +.sidebar li { + margin: 10px 0; + cursor: pointer; + transition: 0.2s; +} +.sidebar li:hover { + color: #00bcd4; +} */ + +@keyframes spin-slow { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} +.animate-spin-slow { + animation: spin-slow 8s linear infinite; +} + +@media (max-width: 768px) { + .sidebar { + position: absolute; + z-index: 50; + transform: translateX(-100%); + } + + .sidebar.expanded { + transform: translateX(0); + } +} diff --git a/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.html b/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.html new file mode 100644 index 0000000..cd2492e --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.html @@ -0,0 +1,65 @@ +
+ + + +
+

+ Global Sidebar +

+
+ +
+ +
    + +
  • + + Dashboard +
  • + +
  • + + Profile +
  • + +
  • + + Report +
  • + + + +
  • + + Logout +
  • +
+
+ +
+
+ + + + diff --git a/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.ts b/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.ts new file mode 100644 index 0000000..893babd --- /dev/null +++ b/accounting-ng-nuttakit/src/app/component/sidebar/sidebar.component.ts @@ -0,0 +1,66 @@ +import { Component, HostListener, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { trigger, state, style, transition, animate } from '@angular/animations'; + +@Component({ + selector: 'app-sidebar', + standalone: false, + templateUrl: './sidebar.component.html', + styleUrls: ['./sidebar.component.css'], + animations: [ + trigger('sidebarState', [ + state('expanded', style({ + width: '220px', + opacity: 1 + })), + state('collapsed', style({ + width: '70px', + opacity: 0.95 + })), + transition('expanded <=> collapsed', [ + animate('300ms ease-in-out') + ]) + ]) + ] +}) +export class SidebarComponent implements OnInit { + isOpen = true; // ขยายไหม + isMobile = false; // ตรวจอุปกรณ์ + showOverlay = false; // สำหรับ mobile overlay + + constructor(private router: Router) {} + + ngOnInit() { + this.checkDevice(); + window.addEventListener('resize', () => this.checkDevice()); + } + + @HostListener('window:resize') + checkDevice() { + this.isMobile = window.innerWidth <= 768; + if (this.isMobile) { + this.isOpen = false; // ซ่อน sidebar ตอนเข้า mobile + } else { + this.isOpen = true; // เปิดไว้ตลอดใน desktop + this.showOverlay = false; + } + } + + toggleSidebar() { + if (this.isMobile) { + this.showOverlay = !this.showOverlay; + this.isOpen = this.showOverlay; + } else { + this.isOpen = !this.isOpen; + } + } + + navigate(path: string) { + this.router.navigate([path]); + } + + logout() { + localStorage.removeItem('access_token'); + this.router.navigate(['/login']); + } +} diff --git a/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.css b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.css new file mode 100644 index 0000000..fbe7f82 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.css @@ -0,0 +1,19 @@ +:host { + display: block; + margin: 0; + padding: 0; + width: 100%; + height: 100vh; /* ครอบเต็มหน้าจอ */ + overflow: hidden; /* ปิด scroll bar */ + box-sizing: border-box; +} + +.login-content { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; /* ✅ แก้ตรงนี้ จาก row → column */ + align-items: center; /* ✅ จัดให้อยู่กลางแนวนอน */ + justify-content: center; /* ✅ จัดให้อยู่กลางแนวตั้ง */ + text-align: center; /* ✅ ให้ข้อความตรงกลาง */ +} diff --git a/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.html b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.html new file mode 100644 index 0000000..473a2e8 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.html @@ -0,0 +1,11 @@ +
+ + @if (mode == "default") { + + } @else if(mode == "forgot-password"){ + + } + +
diff --git a/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts new file mode 100644 index 0000000..5918822 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/login-content/login-content.component.ts @@ -0,0 +1,140 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { GeneralService } from '../../services/generalservice'; +import { LoginForgotComponent } from '../../../app/component/login-forgot/login-forgot.component'; +import { LoginPageComponent } from '../../../app/component/login-page/login-page.component'; +import { finalize } from 'rxjs/operators'; + +@Component({ + selector: 'app-login-content', + standalone: false, + templateUrl: './login-content.component.html', + styleUrl: './login-content.component.css' +}) +export class LoginContentComponent implements OnInit { + @ViewChild(LoginForgotComponent) loginForgotComponent!: LoginForgotComponent; + @ViewChild(LoginPageComponent) loginPageComponent!: LoginPageComponent; + mode: 'forgot-password' | 'default' = 'default'; + + constructor( + private generalService: GeneralService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit(): void { + let param = this.route.snapshot.paramMap.get('mode'); + + if (param === 'forgot-password') { + this.mode = 'forgot-password'; + } else { + // this.router.navigate(['/login']); // This can cause navigation loops + this.mode = 'default'; + } + } + + onSignInSubmit(value: any){ + const uri = '/api/login/login'; + const request = { + username: value.username, + password: value.password + }; + + if (this.loginPageComponent) { + this.loginPageComponent.busy = true; + this.loginPageComponent.message = ''; + } + + this.generalService.postRequest(uri, request) + .pipe( + finalize(() => { + if (this.loginPageComponent) { + this.loginPageComponent.busy = false; + } + }) + ).subscribe({ + next: (result: any) => { + if (result.code === '200' && result.data?.token) { + this.generalService.trowApi(result); + localStorage.setItem('access_token', result.data.token); + this.router.navigate(['main/dashboard']); + } else { + const errorMessage = result.message_th || result.message || 'Sign-in failed.'; + if (this.loginPageComponent) { + this.loginPageComponent.message = errorMessage; + } + this.generalService.trowApi(result); + } + }, + error: (error: any) => { + const errorMessage = error?.error?.message_th || error?.error?.message || 'An error occurred.'; + if (this.loginPageComponent) { + this.loginPageComponent.message = errorMessage; + } + this.generalService.trowApi(error.error || { message_th: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' }); + } + }); + } + + + onOtpSendSubmit(value: any){ + let uri = '/api/login/otp/send'; + let request = { + email: value.email + // otp: value.otp + } + + this.loginForgotComponent.isLoading = true; + this.generalService.postRequest(uri, request) + .pipe( + // ✅ finalize จะทำงานทุกกรณี (ทั้ง success/error) + finalize(() => { + this.loginForgotComponent.isLoading = false; + }) + ).subscribe({ + next: (result: any) => { + if (result.code === '200') { + this.generalService.trowApi(result); + console.log(`✅ OTP ส่งไปที่ ${value.email}`); + } else { + console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); + } + }, + error: (error: any) => { + this.loginForgotComponent.isSendOtp = false; + this.loginForgotComponent.isLoading = false; + this.generalService.trowApi(error); + console.error('❌ Error sending OTP:', error); + }, + complete: () => { + this.loginForgotComponent.isLoading = false; + this.loginForgotComponent.isSendOtp = true; + console.log('📨 OTP send request completed'); + } + }); + } + + onVerifySubmit(value: any){ + let uri = '/api/login/otp/verify'; + let request = { + email: value.email, + otp: value.otp + } + this.generalService.postRequest(uri, request).subscribe({ + next: (result: any) => { + if (result.code === '200') { + console.log(`OTP ส่งไปยืนยันสำเร็จ`); + } else { + console.warn('⚠️ ไม่สามารถส่ง OTP ได้:', result.message_th); + } + }, + error: (error: any) => { + console.error('❌ Error sending OTP:', error); + }, + complete: () => { + this.router.navigate(['/login']); + console.log('📨 OTP send request completed'); + } + }); + } +} diff --git a/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.css b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.css new file mode 100644 index 0000000..e69de29 diff --git a/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.html b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.html new file mode 100644 index 0000000..d680121 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.html @@ -0,0 +1 @@ + diff --git a/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts new file mode 100644 index 0000000..016c70a --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/main-dashboard-content/main-dashboard-content.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { ChartConfiguration, ChartOptions } from 'chart.js'; +import { BaseChartDirective } from 'ng2-charts'; +import { GeneralService } from '../../services/generalservice'; + +@Component({ + selector: 'app-main-dashboard-content', + standalone: false, + templateUrl: './main-dashboard-content.component.html', + styleUrls: ['./main-dashboard-content.component.css'] +}) +export class MainDashboardContentComponent implements OnInit { + @ViewChild(BaseChartDirective) chart?: BaseChartDirective; + + public lineChartData: ChartConfiguration<'line'>['data'] = { + labels: [], + datasets: [ + { + data: [], + label: 'Revenue', + fill: true, + tension: 0.5, + borderColor: 'rgba(75,192,192,1)', + backgroundColor: 'rgba(75,192,192,0.2)' + } + ] + }; + + public lineChartOptions: ChartOptions<'line'> = { + responsive: true, + scales: { + y: { + beginAtZero: true + } + }, + plugins: { + legend: { + display: true, + }, + title: { + display: true, + text: 'Revenue Summary - Last 6 Months' + } + } + }; + + constructor(private generalService: GeneralService) {} + + ngOnInit(): void { + this.fetchChartData(); + } + + 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(); + } +} diff --git a/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.css b/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.css new file mode 100644 index 0000000..3d08045 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.css @@ -0,0 +1,11 @@ +.layout { + display: flex; + height: 100vh; + width: 100%; +} +.main-container { + flex: 1; + background: #f5f5f5; + overflow-y: auto; + padding: 20px; +} diff --git a/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.html b/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.html new file mode 100644 index 0000000..caeb126 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.html @@ -0,0 +1,9 @@ +
+ + + + +
+ +
+
diff --git a/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.ts b/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.ts new file mode 100644 index 0000000..c0ee53f --- /dev/null +++ b/accounting-ng-nuttakit/src/app/content/sidebar-content/sidebar-content.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-sidebar-content', + standalone: false, + templateUrl: './sidebar-content.component.html', + styleUrl: './sidebar-content.component.css' +}) +export class SidebarContentComponent { + +} diff --git a/accounting-ng-nuttakit/src/app/controls/login-control/login-control-routing.module.ts b/accounting-ng-nuttakit/src/app/controls/login-control/login-control-routing.module.ts new file mode 100644 index 0000000..7b041c9 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/controls/login-control/login-control-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { LoginContentComponent } from '../../content/login-content/login-content.component'; + +const routes: Routes = [ + { path: '', component: LoginContentComponent }, + { path: ':mode', component: LoginContentComponent } // ตัวอย่าง param /login/reset + // { path: 'forgot-password', component: LoginContentComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class LoginControlRoutingModule { } diff --git a/accounting-ng-nuttakit/src/app/controls/login-control/login-control.module.ts b/accounting-ng-nuttakit/src/app/controls/login-control/login-control.module.ts new file mode 100644 index 0000000..e4298fd --- /dev/null +++ b/accounting-ng-nuttakit/src/app/controls/login-control/login-control.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LoginContentComponent } from '../../content/login-content/login-content.component'; +import { LoginControlRoutingModule } from './login-control-routing.module'; +import { LoginPageComponent } from '../../component/login-page/login-page.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { LoginForgotComponent } from '../../component/login-forgot/login-forgot.component'; +// import { AppModule } from '../../app.module'; + +@NgModule({ + declarations: [ + LoginContentComponent, + LoginPageComponent, + LoginForgotComponent + ], + imports: [ + CommonModule, + ReactiveFormsModule, + FontAwesomeModule, + // AppModule, + LoginControlRoutingModule + ] +}) +export class LoginControlModule { } diff --git a/accounting-ng-nuttakit/src/app/controls/main-control/main-control-routing.module.ts b/accounting-ng-nuttakit/src/app/controls/main-control/main-control-routing.module.ts new file mode 100644 index 0000000..632626a --- /dev/null +++ b/accounting-ng-nuttakit/src/app/controls/main-control/main-control-routing.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component'; +// import { MainReportComponent } from '../../component/main-report/main-report.component'; + + + +const routes: Routes = [ + { path: 'dashboard', component: MainDashboardContentComponent }, + // children: [ + // { + // path: 'dashboard', + // // loadChildren: () => import('./controls/profile-control/profile-control.module').then(m => m.ProfileControlModule) + // }, + // { path: 'report', component: MainReportComponent }, + // { path: '', redirectTo: 'profile', pathMatch: 'full' } + // ] + + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { path: '**', redirectTo: 'dashboard' } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MainControlRoutingModule { } diff --git a/accounting-ng-nuttakit/src/app/controls/main-control/main-control.module.ts b/accounting-ng-nuttakit/src/app/controls/main-control/main-control.module.ts new file mode 100644 index 0000000..b2cf561 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/controls/main-control/main-control.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +// import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { MainControlRoutingModule } from './main-control-routing.module'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { MainDashboardComponent } from '../../component/main-dashboard/main-dashboard.component'; +import { MainDashboardContentComponent } from '../../content/main-dashboard-content/main-dashboard-content.component'; + +// import { MainReportComponent } from '../../component/main-report/main-report.component'; + + + +@NgModule({ + declarations: [ + MainDashboardComponent, + MainDashboardContentComponent + // MainReportComponent + ], + imports: [ + CommonModule, + MainControlRoutingModule, + ReactiveFormsModule + // BrowserAnimationsModule + ] +}) +export class MainControlModule { } diff --git a/accounting-ng-nuttakit/src/app/services/auth.guard.ts b/accounting-ng-nuttakit/src/app/services/auth.guard.ts new file mode 100644 index 0000000..ec66571 --- /dev/null +++ b/accounting-ng-nuttakit/src/app/services/auth.guard.ts @@ -0,0 +1,14 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; + +export const authGuard: CanActivateFn = (route, state) => { + const router = inject(Router); + const accessToken = localStorage.getItem('access_token'); + + if (accessToken) { + return true; + } else { + router.navigate(['/login']); + return false; + } +}; diff --git a/accounting-ng-nuttakit/src/app/services/generalservice.ts b/accounting-ng-nuttakit/src/app/services/generalservice.ts new file mode 100644 index 0000000..0d5376c --- /dev/null +++ b/accounting-ng-nuttakit/src/app/services/generalservice.ts @@ -0,0 +1,138 @@ +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 { + 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) => { + console.error('❌ [POST Request Error]:', error); + return throwError(() => error); + }) + ); + } + + // GET Request + getRequest(uri: string): Observable { + const fullUrl = `${this.baseUrl}${uri}`; + return this.http.get(fullUrl, this.getHttpOptions()).pipe( + map((res: any) => res), + catchError((error: any) => { + console.error('❌ [GET Request Error]:', error); + return throwError(() => error); + }) + ); + } + + // PUT Request + putRequest(uri: string, body: any): Observable { + 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 { + 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' + }); + } + } +} diff --git a/accounting-ng-nuttakit/src/environments/environment.development.ts b/accounting-ng-nuttakit/src/environments/environment.development.ts new file mode 100644 index 0000000..d0503a7 --- /dev/null +++ b/accounting-ng-nuttakit/src/environments/environment.development.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiBaseUrl: 'http://localhost:8000' +}; diff --git a/accounting-ng-nuttakit/src/environments/environment.ts b/accounting-ng-nuttakit/src/environments/environment.ts new file mode 100644 index 0000000..f3e8a81 --- /dev/null +++ b/accounting-ng-nuttakit/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiBaseUrl: 'https://api.nuttakit.work' +}; diff --git a/accounting-ng-nuttakit/src/index.html b/accounting-ng-nuttakit/src/index.html new file mode 100644 index 0000000..c6f97e2 --- /dev/null +++ b/accounting-ng-nuttakit/src/index.html @@ -0,0 +1,20 @@ + + + + + AccountingNgNuttakit + + + + + + + + + + + + + + + diff --git a/accounting-ng-nuttakit/src/main.ts b/accounting-ng-nuttakit/src/main.ts new file mode 100644 index 0000000..6d1f71d --- /dev/null +++ b/accounting-ng-nuttakit/src/main.ts @@ -0,0 +1,7 @@ +import { platformBrowser } from '@angular/platform-browser'; +import { AppModule } from './app/app.module'; + +platformBrowser().bootstrapModule(AppModule, { + ngZoneEventCoalescing: true, +}) + .catch(err => console.error(err)); diff --git a/accounting-ng-nuttakit/src/styles.css b/accounting-ng-nuttakit/src/styles.css new file mode 100644 index 0000000..ddfe0d5 --- /dev/null +++ b/accounting-ng-nuttakit/src/styles.css @@ -0,0 +1,221 @@ +@import "tailwindcss"; + +/* Global base styles for the app. Keep lightweight and self-contained so + the login component can reliably fill the viewport without producing + an outer page scrollbar. */ + +/* Force ngx-toastr container to be fixed to the viewport and positioned correctly */ +.toast-container.toast-top-right { + position: fixed !important; + top: 12px !important; + right: 12px !important; + z-index: 999999 !important; +} + +/* Make sure the page and app root occupy full height so 100vh aligns */ +html, body, app-root { + height: 100%; + min-height: 100%; +} +/* เริ่มต้น: สำหรับ Desktop */ +.login-mobile { + width: 415px; +} + +/* ถ้าเป็น Mobile (<=768px) ให้ลบ width ออก */ +@media (max-width: 768px) { + .login-mobile { + width: auto !important; + } +} + +@media (max-width: 768px) { + .sidebar { + position: fixed; + top: 0; + left: 0; + height: 100%; + z-index: 50; + transition: transform 0.3s ease-in-out; + } +} +/* ✅ Toast Custom Style */ +.ngx-toastr { + border-radius: 8px !important; + backdrop-filter: blur(6px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); + font-family: "Segoe UI", Roboto, sans-serif; + padding: 12px 16px !important; + min-width: 260px; + transition: all 0.3s ease-in-out; +} + +.success-toast { + background: rgba(255, 255, 255, 0.8) !important; + color: #15803d !important; + border-left: 5px solid #16a34a; +} + +.error-toast { + background: rgba(239, 68, 68, 0.8) !important; + color: #fff !important; + border-left: 5px solid #dc2626; +} + +.toast-title { + font-weight: 600 !important; + margin-bottom: 2px; +} + +.toast-message { + font-size: 14px; +} + + +/* sensible default box model */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: "Kanit", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Prevent the browser from showing a scroll bar for the page itself; + the login card will scroll internally if needed. */ +} + +/* Simple utilities used by nested components in this workspace */ +.content-box { + border: 2px solid black; + padding: 10px; + margin: 20px; +} + +.comp-box { + border: 1px solid #555; + border-radius: 8px; + padding: 10px; + margin: 10px; + /* Use flex centering so nested components (like the login widget) + are centered without forcing the document to scroll. */ + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +/* If the project uses Bootstrap, the Bootstrap utilities will still apply. + These local utility rules only ensure a consistent appearance if Bootstrap + isn't available. */ + + + + + +.kanit-thin { + font-family: "Kanit", sans-serif; + font-weight: 100; + font-style: normal; +} + +.kanit-extralight { + font-family: "Kanit", sans-serif; + font-weight: 200; + font-style: normal; +} + +.kanit-light { + font-family: "Kanit", sans-serif; + font-weight: 300; + font-style: normal; +} + +.kanit-regular { + font-family: "Kanit", sans-serif; + font-weight: 400; + font-style: normal; +} + +.kanit-medium { + font-family: "Kanit", sans-serif; + font-weight: 500; + font-style: normal; +} + +.kanit-semibold { + font-family: "Kanit", sans-serif; + font-weight: 600; + font-style: normal; +} + +.kanit-bold { + font-family: "Kanit", sans-serif; + font-weight: 700; + font-style: normal; +} + +.kanit-extrabold { + font-family: "Kanit", sans-serif; + font-weight: 800; + font-style: normal; +} + +.kanit-black { + font-family: "Kanit", sans-serif; + font-weight: 900; + font-style: normal; +} + +.kanit-thin-italic { + font-family: "Kanit", sans-serif; + font-weight: 100; + font-style: italic; +} + +.kanit-extralight-italic { + font-family: "Kanit", sans-serif; + font-weight: 200; + font-style: italic; +} + +.kanit-light-italic { + font-family: "Kanit", sans-serif; + font-weight: 300; + font-style: italic; +} + +.kanit-regular-italic { + font-family: "Kanit", sans-serif; + font-weight: 400; + font-style: italic; +} + +.kanit-medium-italic { + font-family: "Kanit", sans-serif; + font-weight: 500; + font-style: italic; +} + +.kanit-semibold-italic { + font-family: "Kanit", sans-serif; + font-weight: 600; + font-style: italic; +} + +.kanit-bold-italic { + font-family: "Kanit", sans-serif; + font-weight: 700; + font-style: italic; +} + +.kanit-extrabold-italic { + font-family: "Kanit", sans-serif; + font-weight: 800; + font-style: italic; +} + +.kanit-black-italic { + font-family: "Kanit", sans-serif; + font-weight: 900; + font-style: italic; +} diff --git a/accounting-ng-nuttakit/tsconfig.app.json b/accounting-ng-nuttakit/tsconfig.app.json new file mode 100644 index 0000000..3775b37 --- /dev/null +++ b/accounting-ng-nuttakit/tsconfig.app.json @@ -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" + ] +} diff --git a/accounting-ng-nuttakit/tsconfig.json b/accounting-ng-nuttakit/tsconfig.json new file mode 100644 index 0000000..5525117 --- /dev/null +++ b/accounting-ng-nuttakit/tsconfig.json @@ -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 + } +} diff --git a/accounting-ng-nuttakit/tsconfig.spec.json b/accounting-ng-nuttakit/tsconfig.spec.json new file mode 100644 index 0000000..5fb748d --- /dev/null +++ b/accounting-ng-nuttakit/tsconfig.spec.json @@ -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" + ] +} diff --git a/accounting-ng-nuttakit/vite.config.ts b/accounting-ng-nuttakit/vite.config.ts new file mode 100644 index 0000000..77e4691 --- /dev/null +++ b/accounting-ng-nuttakit/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import { angular } from '@analogjs/vite-plugin-angular'; + +export default defineConfig({ + plugins: [angular()], + server: { + host: '0.0.0.0', + allowedHosts: ['accounting.nuttakit.work'], // 👈 เพิ่มโดเมนของคุณ + }, +});