diff --git a/ng-ttc-frontend/.postcssrc.json b/ng-ttc-frontend/.postcssrc.json
new file mode 100644
index 0000000..e092dc7
--- /dev/null
+++ b/ng-ttc-frontend/.postcssrc.json
@@ -0,0 +1,5 @@
+{
+ "plugins": {
+ "@tailwindcss/postcss": {}
+ }
+}
diff --git a/ng-ttc-frontend/package-lock.json b/ng-ttc-frontend/package-lock.json
index f9f2928..301c568 100644
--- a/ng-ttc-frontend/package-lock.json
+++ b/ng-ttc-frontend/package-lock.json
@@ -16,7 +16,10 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
+ "@tailwindcss/postcss": "^4.1.17",
+ "postcss": "^8.5.6",
"rxjs": "~7.8.0",
+ "tailwindcss": "^4.1.17",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
@@ -34,6 +37,18 @@
"typescript": "~5.6.2"
}
},
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -200,6 +215,35 @@
}
}
},
+ "node_modules/@angular-devkit/build-angular/node_modules/postcss": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
+ "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
@@ -3236,18 +3280,26 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -3268,14 +3320,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -5066,6 +5116,280 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
+ "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.17"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
+ "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-x64": "4.1.17",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.17",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.17",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.17",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
+ "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
+ "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
+ "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
+ "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
+ "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
+ "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
+ "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
+ "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
+ "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
+ "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.6.0",
+ "@emnapi/runtime": "^1.6.0",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.0.7",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
+ "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
+ "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
+ "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.1.17",
+ "@tailwindcss/oxide": "4.1.17",
+ "postcss": "^8.4.41",
+ "tailwindcss": "4.1.17"
+ }
+ },
"node_modules/@tufjs/canonical-json": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
@@ -7155,9 +7479,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
"license": "Apache-2.0",
- "optional": true,
"engines": {
"node": ">=8"
}
@@ -7402,7 +7724,6 @@
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
- "dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -8326,7 +8647,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/handle-thing": {
@@ -9775,6 +10095,255 @@
}
}
},
+ "node_modules/lightningcss": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -10554,7 +11123,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -11410,7 +11978,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -11464,10 +12031,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.2",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
- "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
- "dev": true,
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -11485,7 +12051,7 @@
"license": "MIT",
"peer": true,
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -12874,7 +13440,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -13201,11 +13766,16 @@
"node": ">=0.10"
}
},
+ "node_modules/tailwindcss": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
+ "license": "MIT"
+ },
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -14089,35 +14659,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/vite/node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
"node_modules/vite/node_modules/rollup": {
"version": "4.53.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
diff --git a/ng-ttc-frontend/package.json b/ng-ttc-frontend/package.json
index 116f59a..efbcf5f 100644
--- a/ng-ttc-frontend/package.json
+++ b/ng-ttc-frontend/package.json
@@ -18,7 +18,10 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
+ "@tailwindcss/postcss": "^4.1.17",
+ "postcss": "^8.5.6",
"rxjs": "~7.8.0",
+ "tailwindcss": "^4.1.17",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
diff --git a/ng-ttc-frontend/src/app/app.component.css b/ng-ttc-frontend/src/app/app.component.css
index e69de29..ddfe0d5 100644
--- a/ng-ttc-frontend/src/app/app.component.css
+++ b/ng-ttc-frontend/src/app/app.component.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/ng-ttc-frontend/src/app/component/license-privacy-terms/license-privacy-terms.component.css b/ng-ttc-frontend/src/app/component/license-privacy-terms/license-privacy-terms.component.css
new file mode 100644
index 0000000..06045f8
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/license-privacy-terms/license-privacy-terms.component.html b/ng-ttc-frontend/src/app/component/license-privacy-terms/license-privacy-terms.component.html
new file mode 100644
index 0000000..08da7b8
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/license-privacy-terms/license-privacy-terms.component.ts b/ng-ttc-frontend/src/app/component/license-privacy-terms/license-privacy-terms.component.ts
new file mode 100644
index 0000000..9f460a8
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.css b/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.css
new file mode 100644
index 0000000..cd46dbe
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.html b/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.html
new file mode 100644
index 0000000..660b8f7
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+

+
ลืมรหัสผ่าน
+
โปรดกรอก Email ของท่าน
+
+
+ @if(isModalOpen){
+
+
+
เปลี่ยนรหัสผ่าน
+
+
+
+
+
+
+
+
+ @if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
+ รหัสผ่านไม่ตรงกัน
+ }
+
+
+
+
+
+
+
+
+ }
+
+
+
diff --git a/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.ts b/ng-ttc-frontend/src/app/component/login-forgot/login-forgot.component.ts
new file mode 100644
index 0000000..f9ff117
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/login-page/login-page.component.css b/ng-ttc-frontend/src/app/component/login-page/login-page.component.css
new file mode 100644
index 0000000..5a4bcfd
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/login-page/login-page.component.html b/ng-ttc-frontend/src/app/component/login-page/login-page.component.html
new file mode 100644
index 0000000..72d7e2a
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/login-page/login-page.component.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+

+
เข้าสู่ระบบ
+
บัญชีโปรแกรมจัดการบัญชีของคุณ
+
+
+
+
+
diff --git a/ng-ttc-frontend/src/app/component/login-page/login-page.component.ts b/ng-ttc-frontend/src/app/component/login-page/login-page.component.ts
new file mode 100644
index 0000000..a032f77
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.css b/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.css
new file mode 100644
index 0000000..70db525
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.css
@@ -0,0 +1,646 @@
+: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;
+ }
+}
+
+.quick-log__form select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+}
+
+.ledger-table.is-scrollable {
+ max-height: 25rem;
+ overflow-y: auto;
+ padding-right: 0.5rem;
+}
\ No newline at end of file
diff --git a/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.html b/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.html
new file mode 100644
index 0000000..0044269
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.html
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
รายรับ
+
{{ summary.income }}
+
+
+
รายจ่าย
+
{{ summary.expense }}
+
+
+
คงเหลือสุทธิ
+
{{ summary.net }}
+
+
+
+
+
+
+
+
+
+
+
{{ card.label }}
+
{{ card.value }}
+
{{ card.trend }} · {{ card.context }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 5">
+
+ รายการ
+ หมวดหมู่
+ ยอดเงิน
+ บันทึก
+
+
+ @for (idx of myActData; track idx.actseq; let i = $index) {
+
+
+
+
+ {{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}
+
+
+
+
{{ idx.acttypnam }}
+
{{ idx.actacpdtm ?? '' | dtmtodatetime}}
+
+
+
+
{{ idx.actcatnam }}
+
+
+ {{ idx.actqty }}
+
+
+
{{ idx.actcmt }}
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
รวมเดือนนี้
+
{{myActSumData.summary.totalExpense}}
+
+
+
+ -
+
+
+
{{ idx.label }}
+
{{ idx.percent }}%
+
{{ idx.value }} บาท
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ ratio.label }}
+
+
{{ ratio.value }}
+
+
+
+
+
+
+
+
+
+
{{ alert.title }}
+
{{ alert.detail }}
+
+
{{ alert.tag }}
+
+
+
+
+
+
+ -
+
+
{{ task.title }}
+
{{ task.due }}
+
+ {{ task.priority }}
+
+
+
+
+
+
+
+
+@if(isModalOpen == true){
+
+}
diff --git a/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.ts b/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.ts
new file mode 100644
index 0000000..4c49e00
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/main-dashboard/main-dashboard.component.ts
@@ -0,0 +1,264 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
+import { GeneralService } from '../../services/generalservice';
+import { IDropAct, IStateDrop, IStateResultResponse, IActData, IActSumData } from '../../interfaces/dashboard.interface'
+import { DashboardStateService } from '../../services/state/dashboard-state.service';
+
+@Component({
+ selector: 'app-main-dashboard',
+ 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;
+ myActData: IActData[] = [];
+ // myDropAct: IStateDrop[] = [];
+ myDropAct: IStateDrop = { income: [], expense: [] };
+ myActSumData: IActSumData = {
+ summary: {
+ totalIncome: '',
+ totalExpense: '',
+ netProfit: '',
+ profitRate: '',
+ adjustedProfitRate: '',
+ period: ''
+ },
+ pie: {
+ income: [],
+ expense: []
+ }
+ };
+ ActSumDataGradient: any
+
+
+ readonly ownerName = 'Nuttakit';
+
+ constructor(
+ private dashboardStateService: DashboardStateService
+ ){}
+
+ readonly kpiCards = [
+ {
+ label: 'รายรับรวม',
+ 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' }
+ ];
+
+
+ ngOnInit(): void {
+ this.setupFormControl();
+ this.dashboardStateService.getStateResult().subscribe(data => {
+ if (data) {
+ this.myDropAct = data;
+ }
+ });
+ // ผลลับท์ ของ รายการ
+ this.dashboardStateService.getStateAccountResult().subscribe(data => {
+ if (data) {
+ this.myActData = data;
+ }
+ });
+ // ผลลัพการ คำนวณ ของ ปัญชี ต่างๆ
+ this.dashboardStateService.getStateSumResult().subscribe(data => {
+ if (data) {
+ this.myActSumData = data;
+ this.ActSumDataGradient = this.buildExpenseGradient()
+ }
+ });
+ }
+ setupFormControl(){
+ this.arrearsForm = new FormGroup({
+ // email: new FormControl('',[Validators.required, Validators.email, Validators.maxLength(100)]),
+ 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 {
+ if (!this.myActSumData?.pie?.expense?.length) return '';
+
+ let current = 0;
+ const segments = this.myActSumData.pie.expense
+ .map(part => {
+ const start = current;
+ const percent = parseFloat(part.percent); // แปลงจาก string → number
+ const end = current + percent;
+ current = end;
+ return `${part.color} ${start}% ${end}%`;
+ })
+ .join(', ');
+
+ return `conic-gradient(${segments})`;
+ }
+}
diff --git a/ng-ttc-frontend/src/app/component/main-report/main-report.component.css b/ng-ttc-frontend/src/app/component/main-report/main-report.component.css
new file mode 100644
index 0000000..9a96ffa
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/main-report/main-report.component.css
@@ -0,0 +1,441 @@
+:host {
+ display: block;
+ padding: 2rem clamp(1rem, 4vw, 3rem);
+ background: #f8fafc;
+ min-height: 100%;
+}
+
+.report {
+ max-width: 1280px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.report__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+ align-items: flex-end;
+}
+
+.eyebrow {
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ font-size: 0.8rem;
+ color: #94a3b8;
+ margin: 0 0 0.25rem;
+}
+
+.report__header h1 {
+ margin: 0 0 0.25rem;
+ font-size: clamp(1.8rem, 4vw, 2.4rem);
+ color: #0f172a;
+}
+
+.muted {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.95rem;
+}
+
+.report__actions {
+ display: inline-flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.btn {
+ border: none;
+ border-radius: 999px;
+ padding: 0.65rem 1.4rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.btn--primary {
+ background: linear-gradient(135deg, #22d3ee, #0ea5e9);
+ color: #0f172a;
+ box-shadow: 0 15px 30px rgba(14, 165, 233, 0.25);
+}
+
+.btn--ghost {
+ background: #fff;
+ color: #0f172a;
+ border: 1px solid #cbd5f5;
+}
+
+.btn--compact {
+ padding: 0.45rem 1.1rem;
+ font-size: 0.9rem;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+}
+
+.summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 1rem;
+}
+
+.summary-card {
+ position: relative;
+ background: #fff;
+ border-radius: 20px;
+ padding: 1.25rem;
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
+}
+
+.summary-card__label {
+ margin: 0;
+ color: #64748b;
+ font-size: 0.9rem;
+}
+
+.summary-card h2 {
+ margin: 0.4rem 0;
+ font-size: 1.6rem;
+ color: #0f172a;
+}
+
+.summary-card__detail {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.9rem;
+}
+
+.summary-card__tone {
+ position: absolute;
+ inset: 0;
+ border-radius: 20px;
+ pointer-events: none;
+ opacity: 0.15;
+}
+
+.tone-mint { background: linear-gradient(135deg, #a7f3d0, #34d399); }
+.tone-amber { background: linear-gradient(135deg, #fde68a, #fbbf24); }
+.tone-indigo { background: linear-gradient(135deg, #c4b5fd, #818cf8); }
+.tone-slate { background: linear-gradient(135deg, #cbd5f5, #94a3b8); }
+
+.report__content {
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
+ gap: 1.5rem;
+}
+
+.panel {
+ background: #fff;
+ border-radius: 24px;
+ padding: 1.5rem;
+ box-shadow: 0 15px 45px rgba(15, 23, 42, 0.08);
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.panel__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ align-items: center;
+}
+
+.panel__header h2 {
+ margin: 0;
+}
+
+.panel__header p {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.9rem;
+}
+
+.table {
+ border: 1px solid #e2e8f0;
+ border-radius: 18px;
+ overflow: hidden;
+}
+
+.table__head,
+.table__row {
+ display: grid;
+ grid-template-columns: 1.2fr 1fr 1.6fr 1fr 0.8fr;
+ padding: 0.85rem 1rem;
+ gap: 1rem;
+ align-items: center;
+}
+
+.table__head {
+ background: #f1f5f9;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #64748b;
+}
+
+.table__row:nth-child(even) {
+ background: rgba(15, 23, 42, 0.015);
+}
+
+.table__row strong {
+ display: block;
+}
+
+.table__row small {
+ display: block;
+ font-size: 0.85rem;
+}
+
+.mono {
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 0.9rem;
+}
+
+.amount-col {
+ text-align: right;
+ font-weight: 600;
+}
+
+.income {
+ color: #16a34a;
+}
+
+.expense {
+ color: #dc2626;
+}
+
+.pie-panel__content {
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+ justify-content: center;
+}
+
+.pie-chart {
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ position: relative;
+ box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
+}
+
+.pie-chart__center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 110px;
+ height: 110px;
+ border-radius: 50%;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
+}
+
+.pie-chart__center p {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.85rem;
+}
+
+.pie-chart__center strong {
+ color: #0f172a;
+}
+
+.pie-legend {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+}
+
+.pie-legend li {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+}
+
+.swatch {
+ width: 14px;
+ height: 14px;
+ border-radius: 4px;
+}
+
+.legend-label {
+ margin: 0;
+ font-weight: 600;
+}
+
+.legend-value {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.85rem;
+}
+
+.preview-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 120;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.preview-modal__backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(15, 23, 42, 0.55);
+ backdrop-filter: blur(4px);
+}
+
+.preview-modal__content {
+ position: relative;
+ background: #fff;
+ border-radius: 24px;
+ padding: 1.5rem;
+ width: min(1100px, 100%);
+ max-height: 90vh;
+ overflow: auto;
+ box-shadow: 0 25px 60px rgba(15, 23, 42, 0.35);
+}
+
+.preview-modal__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.preview-modal__actions {
+ display: inline-flex;
+ gap: 0.5rem;
+}
+
+.preview-sheet {
+ margin-top: 1.5rem;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 16px;
+ padding: 1.5rem;
+ font-size: 0.95rem;
+}
+
+.preview-sheet__header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.preview-totals {
+ display: flex;
+ gap: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.preview-totals p {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.85rem;
+}
+
+.preview-totals strong {
+ display: block;
+ color: #0f172a;
+}
+
+.preview-pie {
+ margin: 1.5rem 0;
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+}
+
+.mini-pie {
+ width: 140px;
+ height: 140px;
+ border-radius: 50%;
+ box-shadow: inset 0 0 20px rgba(15, 23, 42, 0.08);
+}
+
+.preview-pie ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+}
+
+.preview-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.9rem;
+}
+
+.preview-table th,
+.preview-table td {
+ padding: 0.65rem 0.75rem;
+ border: 1px solid #e2e8f0;
+ text-align: left;
+}
+
+.preview-table th {
+ background: #f1f5f9;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.08em;
+ color: #64748b;
+}
+
+.preview-table td:last-child {
+ text-align: right;
+ font-weight: 600;
+}
+
+@media (max-width: 900px) {
+ .report__content {
+ grid-template-columns: 1fr;
+ }
+
+ .table__head,
+ .table__row {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ text-align: left;
+ }
+
+ .amount-col {
+ text-align: left;
+ }
+
+ .pie-panel__content {
+ flex-direction: column;
+ }
+}
+
+@media (max-width: 640px) {
+ :host {
+ padding: 1.5rem 1rem 2rem;
+ }
+
+ .report__actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+
+ .preview-modal {
+ padding: 1rem;
+ }
+}
diff --git a/ng-ttc-frontend/src/app/component/main-report/main-report.component.html b/ng-ttc-frontend/src/app/component/main-report/main-report.component.html
new file mode 100644
index 0000000..99bb809
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/main-report/main-report.component.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+ {{ card.label }}
+ {{ card.value }}
+ {{ card.detail }}
+
+
+
+
+
+
+
+
+
+ วันที่
+ เลขที่เอกสาร
+ หัวข้อ
+ หมวดหมู่
+ ยอดเงิน
+
+
+ {{ record.date }}
+ {{ record.doc }}
+
+ {{ record.topic }}
+ {{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }}
+
+ {{ record.category }}
+ {{ record.displayAmount }}
+
+
+
+
+
+
+
+
+
+ -
+
+
+
{{ part.label }}
+
{{ part.value }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ {{ part.label }} · {{ part.value }}%
+
+
+
+
+
+
+
+ | วันที่ |
+ เลขที่ |
+ หัวข้อ |
+ หมวดหมู่ |
+ ยอดเงิน |
+
+
+
+
+ | {{ record.date }} |
+ {{ record.doc }} |
+ {{ record.topic }} |
+ {{ record.category }} |
+ {{ record.displayAmount }} |
+
+
+
+
+
+
diff --git a/ng-ttc-frontend/src/app/component/main-report/main-report.component.ts b/ng-ttc-frontend/src/app/component/main-report/main-report.component.ts
new file mode 100644
index 0000000..1ca16fc
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/main-report/main-report.component.ts
@@ -0,0 +1,138 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-main-report',
+ templateUrl: './main-report.component.html',
+ standalone: false,
+ styleUrls: ['./main-report.component.css']
+})
+export class MainReportComponent {
+ readonly reportRange = {
+ start: '1 มิถุนายน 2567',
+ end: '30 มิถุนายน 2567'
+ };
+
+ readonly summaryCards = [
+ { label: 'รายรับรวม', value: '฿1,284,500', detail: '+12.4% MoM', tone: 'mint' },
+ { label: 'รายจ่ายรวม', value: '฿732,800', detail: '-4.1% MoM', tone: 'amber' },
+ { label: 'กำไรสุทธิ', value: '฿551,700', detail: 'Margin 42.9%', tone: 'indigo' },
+ { label: 'บันทึกรายการ', value: '86 รายการ', detail: '32 รายรับ · 54 รายจ่าย', tone: 'slate' }
+ ];
+
+ readonly ledgerRecords = [
+ {
+ date: '01 มิ.ย. 2567',
+ doc: 'RCPT-9101',
+ type: 'income',
+ topic: 'ค่าบริการที่ปรึกษา',
+ category: 'บริการ',
+ amount: 145000
+ },
+ {
+ date: '02 มิ.ย. 2567',
+ doc: 'EXP-4407',
+ type: 'expense',
+ topic: 'ค่าวัสดุโครงการ A',
+ category: 'ต้นทุนโครงการ',
+ amount: -38900
+ },
+ {
+ date: '06 มิ.ย. 2567',
+ doc: 'RCPT-9110',
+ type: 'income',
+ topic: 'รับเงินมัดจำโครงการ',
+ category: 'สัญญาใหม่',
+ amount: 220000
+ },
+ {
+ date: '09 มิ.ย. 2567',
+ doc: 'EXP-4412',
+ type: 'expense',
+ topic: 'เงินเดือนพนักงาน',
+ category: 'บุคลากร',
+ amount: -180000
+ },
+ {
+ date: '12 มิ.ย. 2567',
+ doc: 'EXP-4416',
+ type: 'expense',
+ topic: 'ค่าเช่าออฟฟิศ',
+ category: 'ค่าใช้จ่ายคงที่',
+ amount: -48000
+ },
+ {
+ date: '19 มิ.ย. 2567',
+ doc: 'RCPT-9122',
+ type: 'income',
+ topic: 'ค่าสัญญาบริการรายปี',
+ category: 'บริการ',
+ amount: 325000
+ },
+ {
+ date: '23 มิ.ย. 2567',
+ doc: 'EXP-4425',
+ type: 'expense',
+ topic: 'ค่าโฆษณาออนไลน์',
+ category: 'การตลาด',
+ amount: -72000
+ },
+ {
+ date: '28 มิ.ย. 2567',
+ doc: 'RCPT-9133',
+ type: 'income',
+ topic: 'รายรับจากคู่ค้าใหม่',
+ category: 'พันธมิตร',
+ amount: 210500
+ }
+ ];
+
+ readonly expenseBreakdown = [
+ { label: 'ต้นทุนโครงการ', value: 34, color: '#10b981' },
+ { label: 'บุคลากร', value: 26, color: '#6366f1' },
+ { label: 'การตลาด', value: 18, color: '#f97316' },
+ { label: 'ค่าใช้จ่ายคงที่', value: 14, color: '#0ea5e9' },
+ { label: 'อื่นๆ', value: 8, color: '#e11d48' }
+ ];
+
+ readonly previewTotals = [
+ { label: 'รายรับรวม', value: '฿1,284,500' },
+ { label: 'รายจ่ายรวม', value: '฿732,800' },
+ { label: 'กำไรสุทธิ', value: '฿551,700' }
+ ];
+
+ printPreviewOpen = false;
+
+ get expenseGradient(): string {
+ let current = 0;
+ const segments = this.expenseBreakdown
+ .map(slice => {
+ const start = current;
+ const end = current + slice.value;
+ current = end;
+ return `${slice.color} ${start}% ${end}%`;
+ })
+ .join(', ');
+ return `conic-gradient(${segments})`;
+ }
+
+ get formattedRecords() {
+ return this.ledgerRecords.map(record => ({
+ ...record,
+ displayAmount: this.formatCurrency(record.amount),
+ tone: record.type === 'income' ? 'income' : 'expense'
+ }));
+ }
+
+ openPreview(): void {
+ this.printPreviewOpen = true;
+ }
+
+ closePreview(): void {
+ this.printPreviewOpen = false;
+ }
+
+ private formatCurrency(amount: number): string {
+ const formatter = new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB', maximumFractionDigits: 0 });
+ return formatter.format(amount);
+ }
+}
diff --git a/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.css b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.css
new file mode 100644
index 0000000..4d8834e
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.html b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.html
new file mode 100644
index 0000000..cd2492e
--- /dev/null
+++ b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ Global Sidebar
+
+
+
+
+
+
+
+ -
+
+ Dashboard
+
+
+ -
+
+ Profile
+
+
+ -
+
+ Report
+
+
+
+
+ -
+
+ Logout
+
+
+
+
+
+
+
+
+
+
diff --git a/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts b/ng-ttc-frontend/src/app/component/sidebar/sidebar.component.ts
new file mode 100644
index 0000000..893babd
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/content/login-content/login-content.component.css b/ng-ttc-frontend/src/app/content/login-content/login-content.component.css
new file mode 100644
index 0000000..fbe7f82
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/content/login-content/login-content.component.html b/ng-ttc-frontend/src/app/content/login-content/login-content.component.html
new file mode 100644
index 0000000..473a2e8
--- /dev/null
+++ b/ng-ttc-frontend/src/app/content/login-content/login-content.component.html
@@ -0,0 +1,11 @@
+
+
+ @if (mode == "default") {
+
+ } @else if(mode == "forgot-password"){
+
+ }
+
+
diff --git a/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts b/ng-ttc-frontend/src/app/content/login-content/login-content.component.ts
new file mode 100644
index 0000000..9bc734e
--- /dev/null
+++ b/ng-ttc-frontend/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);
+ }
+ });
+ }
+
+
+ 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/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.css b/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.html b/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.html
new file mode 100644
index 0000000..d680121
--- /dev/null
+++ b/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.html
@@ -0,0 +1 @@
+
diff --git a/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.ts b/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.ts
new file mode 100644
index 0000000..7ac22b0
--- /dev/null
+++ b/ng-ttc-frontend/src/app/content/main-dashboard-content/main-dashboard-content.component.ts
@@ -0,0 +1,162 @@
+import { DashboardStateService } from './../../services/state/dashboard-state.service';
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { ChartConfiguration, ChartOptions } from 'chart.js';
+import { BaseChartDirective } from 'ng2-charts';
+import { GeneralService } from '../../services/generalservice';
+import { IDropAct, IStateDrop, IActData, IActSumData } from '../../interfaces/dashboard.interface';
+
+
+@Component({
+ selector: 'app-main-dashboard-content',
+ standalone: false,
+ templateUrl: './main-dashboard-content.component.html',
+ styleUrls: ['./main-dashboard-content.component.css']
+})
+export class MainDashboardContentComponent implements OnInit {
+ @ViewChild(BaseChartDirective) chart?: BaseChartDirective;
+ myDropAct!: IStateDrop;
+ myActData: IActData[] = [];
+ myActSumData: IActSumData = {
+ summary: {
+ totalIncome: '',
+ totalExpense: '',
+ netProfit: '',
+ profitRate: '',
+ adjustedProfitRate: '',
+ period: ''
+ },
+ pie: {
+ income: [],
+ expense: []
+ }
+ };
+
+ constructor(
+ private generalService: GeneralService,
+ private dashboardStateService: DashboardStateService
+ ) {}
+
+ ngOnInit(): void {
+ let token = localStorage.getItem('access_token')
+ this.OnSearchAct(token, true);
+ this.OnSetupDashboard(token, true);
+ this.OnSearchSum(token, true);
+ }
+
+ OnSearchAct(value: any, setupFirst: boolean): void {
+ const uri = '/api/web/accountingSearch';
+ let request = {
+ token: value
+ }
+ this.generalService.postRequest(uri, request).subscribe({
+ next: (result: any) => {
+ if (result.code === '200') {
+ this.generalService.trowApi(result);
+ this.myActData = result.data;
+ this.dashboardStateService.setStateAccountResult(this.myActData);
+ }else{
+ this.generalService.trowApi(result);
+ }
+ },
+ error: (error: any) => {
+ this.generalService.trowApi(error);
+ },
+ complete: () => {
+
+ }
+ });
+ }
+
+
+
+ OnSetupDashboard(value: any, setupFirst: boolean): void {
+ const uri = '/api/web/accountingSetup';
+ let request = {
+ token: value
+ }
+ this.generalService.postRequest(uri, request).subscribe({
+ next: (result: any) => {
+ if (result.code === '200') {
+ this.generalService.trowApi(result);
+ this.myDropAct = result.data
+ this.dashboardStateService.setStateResult(this.myDropAct)
+ }else{
+ this.generalService.trowApi(result);
+ }
+ },
+ error: (error: any) => {
+ this.generalService.trowApi(error);
+ },
+ complete: () => {
+
+ }
+ });
+ }
+
+ OnSearchSum(value: any, setupFirst: boolean): void {
+ const uri = '/api/web/accountingSum';
+ let request = {
+ token: value
+ }
+ this.generalService.postRequest(uri, request).subscribe({
+ next: (result: any) => {
+ if (result.code === '200') {
+ this.generalService.trowApi(result);
+ this.myActSumData = result.data
+ this.dashboardStateService.setStateSumResult(this.myActSumData);
+ }else{
+ this.generalService.trowApi(result);
+ }
+ },
+ error: (error: any) => {
+ this.generalService.trowApi(error);
+ },
+ complete: () => {
+
+ }
+ });
+ }
+
+ // 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/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.css b/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.css
new file mode 100644
index 0000000..3d08045
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html b/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html
new file mode 100644
index 0000000..caeb126
--- /dev/null
+++ b/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.html
@@ -0,0 +1,9 @@
+
diff --git a/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.ts b/ng-ttc-frontend/src/app/content/sidebar-content/sidebar-content.component.ts
new file mode 100644
index 0000000..c0ee53f
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/controls/login-control/login-control-routing.module.ts b/ng-ttc-frontend/src/app/controls/login-control/login-control-routing.module.ts
new file mode 100644
index 0000000..7b041c9
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/controls/login-control/login-control.module.ts b/ng-ttc-frontend/src/app/controls/login-control/login-control.module.ts
new file mode 100644
index 0000000..e4298fd
--- /dev/null
+++ b/ng-ttc-frontend/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/ng-ttc-frontend/src/app/controls/main-control/main-control-routing.module.ts b/ng-ttc-frontend/src/app/controls/main-control/main-control-routing.module.ts
new file mode 100644
index 0000000..285e146
--- /dev/null
+++ b/ng-ttc-frontend/src/app/controls/main-control/main-control-routing.module.ts
@@ -0,0 +1,29 @@
+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';
+// import { MainReportComponent } from '../../component/main-report/main-report.component';
+
+
+
+const routes: Routes = [
+ { path: 'dashboard', component: MainDashboardContentComponent },
+ { path: 'report', component: MainReportComponent },
+ // 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/ng-ttc-frontend/src/app/controls/main-control/main-control.module.ts b/ng-ttc-frontend/src/app/controls/main-control/main-control.module.ts
new file mode 100644
index 0000000..ce71e45
--- /dev/null
+++ b/ng-ttc-frontend/src/app/controls/main-control/main-control.module.ts
@@ -0,0 +1,36 @@
+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 { AccDateFormatPipe } from '../../pipe/dtmtodatetime.pipe';
+import { MainReportComponent } from '../../component/main-report/main-report.component';
+
+// import { MainReportComponent } from '../../component/main-report/main-report.component';
+
+
+
+@NgModule({
+ declarations: [
+ MainDashboardComponent,
+ MainDashboardContentComponent,
+ MainReportComponent,
+ AccDateFormatPipe
+ // MainReportComponent
+ ],
+ imports: [
+ CommonModule,
+ MainControlRoutingModule,
+ ReactiveFormsModule
+ // BrowserAnimationsModule
+ ],
+ exports: [
+ AccDateFormatPipe
+ ]
+})
+export class MainControlModule { }
diff --git a/ng-ttc-frontend/src/index.html b/ng-ttc-frontend/src/index.html
index fa99a79..c6f97e2 100644
--- a/ng-ttc-frontend/src/index.html
+++ b/ng-ttc-frontend/src/index.html
@@ -2,10 +2,17 @@
- NgTtcFrontend
+ AccountingNgNuttakit
+
+
+
+
+
+
+
diff --git a/ng-ttc-frontend/src/styles.css b/ng-ttc-frontend/src/styles.css
index 90d4ee0..ddfe0d5 100644
--- a/ng-ttc-frontend/src/styles.css
+++ b/ng-ttc-frontend/src/styles.css
@@ -1 +1,221 @@
-/* You can add global styles to this file, and also import other style files */
+@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;
+}