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 @@ +
+
+ +
+
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 @@ +
+
+
+

ภาพรวมบัญชี

+

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

+

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

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

{{ summary.note }}

+
+
+
+

รายรับ

+

{{ summary.income }}

+
+
+

รายจ่าย

+

{{ summary.expense }}

+
+
+

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

+

{{ summary.net }}

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

{{ card.label }}

+
{{ card.value }}
+

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

+
+
+
+ +
+
+
+
+

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

+

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

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

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

+

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

+
+ +
+
+
+ รายการ + หมวดหมู่ + ยอดเงิน + บันทึก +
+ + @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 }} บาท

    +
    +
  • +
+
+
+ +
+
+
+

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

+

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

+
+
+
+
+
+

+ {{ 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 @@ +
+
+
+

สรุปรายงาน

+

รายงานรายรับรายจ่าย

+

ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}

+
+
+ + +
+
+ +
+
+

{{ card.label }}

+

{{ card.value }}

+

{{ card.detail }}

+ +
+
+ +
+
+
+
+

สมุดรายวัน

+

บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา

+
+ +
+
+
+ วันที่ + เลขที่เอกสาร + หัวข้อ + หมวดหมู่ + ยอดเงิน +
+
+ {{ record.date }} + {{ record.doc }} + + {{ record.topic }} + {{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }} + + {{ record.category }} + {{ record.displayAmount }} +
+
+
+ +
+
+
+

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

+

เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้

+
+
+
+
+
+

รวมรายจ่าย

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

    {{ part.label }}

    +

    {{ part.value }}%

    +
    +
  • +
+
+
+
+
+ +
+
+
+
+
+

Print Preview

+

รายงานรายรับรายจ่าย

+

ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}

+
+
+ + +
+
+ +
+
+
+

Accounting Summary

+

Prepared on {{ reportRange.end }}

+
+
+
+

{{ total.label }}

+ {{ total.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; +}