This commit is contained in:
5
ng-ttc-frontend/.postcssrc.json
Normal file
5
ng-ttc-frontend/.postcssrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
||||
633
ng-ttc-frontend/package-lock.json
generated
633
ng-ttc-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<div class="policy-container">
|
||||
<div class="card">
|
||||
<h1 class="page-title">ข้อตกลงสิทธิ์การใช้งาน นโยบายความเป็นส่วนตัว และเงื่อนไขการให้บริการ</h1>
|
||||
<p class="subtitle">ปรับปรุงล่าสุด: 27 ตุลาคม 2025</p>
|
||||
|
||||
<section>
|
||||
<h2>1. ข้อตกลงสิทธิ์การใช้งาน (License Agreement)</h2>
|
||||
<p>
|
||||
ซอร์สโค้ด ส่วนประกอบ และทรัพย์สินการออกแบบทั้งหมดภายใต้โครงการ Nuttakit Software
|
||||
อยู่ภายใต้สัญญาอนุญาตแบบ <strong>MIT License</strong> เว้นแต่จะมีการระบุเป็นอย่างอื่นโดยเฉพาะ
|
||||
</p>
|
||||
<p>
|
||||
ท่านได้รับสิทธิ์ในการใช้งาน คัดลอก แก้ไข รวม รวมเข้ากับซอฟต์แวร์อื่น เผยแพร่ หรือแจกจ่ายซอฟต์แวร์นี้
|
||||
เพื่อวัตถุประสงค์ส่วนตัวหรือเชิงพาณิชย์ได้
|
||||
โดยต้องคงไว้ซึ่งข้อความลิขสิทธิ์และข้อความอนุญาตนี้ในสำเนาทั้งหมดของซอฟต์แวร์
|
||||
</p>
|
||||
<p>
|
||||
ซอฟต์แวร์นี้ถูกจัดให้ “ตามสภาพ” (AS IS)
|
||||
โดยไม่มีการรับประกันใด ๆ ไม่ว่าจะโดยชัดแจ้งหรือโดยนัย
|
||||
รวมถึงแต่ไม่จำกัดเฉพาะการรับประกันความเหมาะสมในการใช้งานหรือความถูกต้องของข้อมูล
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. นโยบายความเป็นส่วนตัว (Privacy Policy)</h2>
|
||||
<p>
|
||||
NuttakitSoftwareให้ความสำคัญกับความเป็นส่วนตัวของผู้ใช้งาน
|
||||
แอปพลิเคชันของเราจะเก็บข้อมูลเพียงบางส่วนเท่านั้น เช่น รหัสอุปกรณ์
|
||||
บันทึกการทำงาน หรือสถิติการใช้งาน
|
||||
เพื่อใช้ในการวิเคราะห์ ปรับปรุง และพัฒนาประสิทธิภาพของระบบ
|
||||
</p>
|
||||
<p>
|
||||
ข้อมูลส่วนบุคคล (Personal Identifiable Information — PII)
|
||||
จะไม่ถูกขาย แบ่งปัน หรือโอนไปยังบุคคลที่สามโดยไม่ได้รับความยินยอมจากท่านอย่างชัดเจน
|
||||
</p>
|
||||
<p>
|
||||
เราใช้มาตรฐานการเข้ารหัสระดับอุตสาหกรรม (AES-CBC)
|
||||
เพื่อรักษาความปลอดภัยในการส่งข้อมูลระหว่างแอปพลิเคชัน เซิร์ฟเวอร์ และ API
|
||||
</p>
|
||||
<p>
|
||||
ผู้ใช้สามารถร้องขอให้ลบหรือขอรับสำเนาข้อมูลของตนเองได้ตลอดเวลา
|
||||
โดยติดต่อทีมสนับสนุนของเรา
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. เงื่อนไขการให้บริการ (Terms of Service)</h2>
|
||||
<p>
|
||||
เมื่อท่านใช้งานซอฟต์แวร์หรือบริการของเรา ถือว่าท่านยอมรับและปฏิบัติตามกฎหมายและข้อบังคับที่เกี่ยวข้องทั้งหมด
|
||||
</p>
|
||||
<p>
|
||||
ท่านจะต้องไม่ใช้ซอฟต์แวร์ของเราในทางที่ผิด
|
||||
ไม่พยายามถอดรหัส แก้ไข ดัดแปลง หรือแสวงหาประโยชน์จากช่องโหว่ในระบบโดยไม่ได้รับอนุญาต
|
||||
</p>
|
||||
<p>
|
||||
Nuttakit ขอสงวนสิทธิ์ในการแก้ไขหรือยุติการให้บริการโดยไม่ต้องแจ้งให้ทราบล่วงหน้า
|
||||
หากตรวจพบการละเมิดความปลอดภัยหรือการใช้งานที่ไม่เหมาะสม
|
||||
</p>
|
||||
<p>
|
||||
ทีมพัฒนา Nuttakit และผู้ร่วมพัฒนาไม่รับผิดชอบต่อความเสียหายใด ๆ
|
||||
ที่อาจเกิดจากการใช้งานหรือไม่สามารถใช้งานซอฟต์แวร์นี้ได้
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. ช่องทางการติดต่อ (Contact)</h2>
|
||||
<p>
|
||||
หากท่านมีข้อสงสัยหรือข้อกังวลเกี่ยวกับข้อมูลส่วนบุคคลหรือเงื่อนไขการให้บริการ
|
||||
สามารถติดต่อเราได้ที่:
|
||||
<br />
|
||||
<strong>อีเมล:</strong> support@nuttakit.work<br />
|
||||
<strong>เว็บไซต์:</strong>
|
||||
<a href="https://nuttakit.work" target="_blank">https://nuttakit.work</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<hr />
|
||||
<p class="footer-text">© 2025 Nuttakit Software. สงวนลิขสิทธิ์ทั้งหมด</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<div class="login-backdrop">
|
||||
<div class="login-modal d-flex align-items-center justify-content-center ">
|
||||
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||
<div class="brand">
|
||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
||||
<h1 id="signin-title" class='kanit-bold'>ลืมรหัสผ่าน</h1>
|
||||
<p class="subtitle">โปรดกรอก Email ของท่าน</p>
|
||||
</div>
|
||||
<form [formGroup]="forgotFrm" class="form px-3 pb-3 login-mobile">
|
||||
<label class="field">
|
||||
<span class="label-text ">อีเมล์</span>
|
||||
<input type="email" formControlName="email" class="px-2 " id="englishInput" autocomplete="username" placeholder="nuttakit@gmail.com" aria-label="Email address" required />
|
||||
</label>
|
||||
|
||||
@if (isSendOtp === true) {
|
||||
<label class="field">
|
||||
<span class="label-text">รหัสยืนยัน OTP</span>
|
||||
<input type="email" formControlName="otp" autocomplete="otp" placeholder="123456" alt required/>
|
||||
</label>
|
||||
}
|
||||
<!-- <div class="justify-end flex"> -->
|
||||
<!-- <label class="stay-signed">
|
||||
<input type="checkbox" formControlName="remember" />
|
||||
<span>จดจำรหัสผ่าน</span>
|
||||
</label> -->
|
||||
<div class="flex flex-row gap-2 mt-4 justify-end">
|
||||
<div class="flex-row hover:-translate-y-2.5 transform transition-all">
|
||||
<button class="bg-[linear-gradient(180deg,var(--primary)_0%,var(--primary-600)_100%)]
|
||||
text-black
|
||||
border-0
|
||||
px-3.5 py-2.5
|
||||
rounded-md
|
||||
font-semibold
|
||||
cursor-pointer
|
||||
text-[14px]
|
||||
shadow-[0_6px_18px_var(--color-blue-500)]
|
||||
transition
|
||||
ease-in-out
|
||||
duration-100
|
||||
hover:scale-[1.02]
|
||||
active:opacity-90" (click)="isModalOpen = true">
|
||||
เปิด Modal
|
||||
</button>
|
||||
</div>
|
||||
@if (isSendOtp === false) {
|
||||
<div class="flex justify-end">
|
||||
@if (isLoading === true) {
|
||||
<button type="submit" class="primary cursor-progress!" disabled>
|
||||
กำลังส่ง...
|
||||
</button>
|
||||
} @else {
|
||||
<button type="submit" class="primary" (click)="onSubmin()">
|
||||
{{ 'ยืนยันส่ง OTP รีเซ็ตรหัสผ่าน' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else if(isSendOtp === true) {
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" class="primary" (click)="onSubmin()">
|
||||
{{ 'ส่งอีกครั้ง' }}
|
||||
</button>
|
||||
<button type="submit" class="primary" (click)="onVerifySubmit()">
|
||||
{{ 'รีเซ็ตรหัสผ่าน' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- <button mat-raised-button color="primary" [disabled]="isLoading">
|
||||
{{ isLoading ? 'กำลังส่ง...' : 'ส่งรหัส OTP' }}
|
||||
</button> -->
|
||||
<!-- } -->
|
||||
<!-- </div> -->
|
||||
</form>
|
||||
@if(isModalOpen){
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50" [formGroup]="forgotFrm">
|
||||
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-fit">
|
||||
<h2 class="text-xl font-bold mb-4">เปลี่ยนรหัสผ่าน</h2>
|
||||
<hr class="w-full h-1 bg-gray-300 rounded-sm shadow-neutral-400 md:my-1">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">รหัสผ่านใหม่</label>
|
||||
<input type="password" id="newPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" formControlName="newPassword" placeholder="กรอกรหัสผ่านใหม่">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-1">ยืนยันรหัสผ่านใหม่</label>
|
||||
<input type="password" id="confirmPassword" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" formControlName="confirmPassword" placeholder="กรอกยืนยันรหัสผ่านใหม่">
|
||||
@if ( this.forgotFrm.get('confirmPassword')!.touched && this.forgotFrm.get('newPassword')?.value !== this.forgotFrm.get('confirmPassword')?.value ){
|
||||
<span class="text-red-600 md">รหัสผ่านไม่ตรงกัน</span>
|
||||
}
|
||||
</div>
|
||||
<!-- <hr class="w-full h-[] bg-gray-100 border-0 rounded-sm md:my-1 dark:bg-gray-700"> -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="bg-red-500 text-white px-4 py-2 rounded" (click)="isModalOpen = false">
|
||||
ปิด
|
||||
</button>
|
||||
<button class="bg-green-500 text-white px-4 py-2 rounded shadow-[0_1px_18px_var(--color-green-300)] hover:-translate-y-1.5 hover:shadow-[0_6px_18px_var(--color-green-500)] transition-all duration-500 ease-in-out" (click)="onSetNewPassword()">
|
||||
ยืนยัน
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<any>();
|
||||
@Output() otpVerifyEventSubmit = new EventEmitter<any>();
|
||||
|
||||
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: }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Modal-like backdrop that covers the viewport -->
|
||||
<div class="login-backdrop">
|
||||
<div class="login-modal d-flex align-items-center justify-content-center">
|
||||
<div class="card modal-card" role="dialog" aria-labelledby="signin-title" aria-modal="true">
|
||||
<div class="brand">
|
||||
<!-- <img src="assets/logo.png" alt="Company logo" class="logo" /> -->
|
||||
<img src="logo.png" alt="Company logo" class="logo mb-2"/>
|
||||
<h1 id="signin-title" class='kanit-bold'>เข้าสู่ระบบ</h1>
|
||||
<p class="subtitle">บัญชีโปรแกรมจัดการบัญชีของคุณ</p>
|
||||
</div>
|
||||
<form [formGroup]="loginForm" (ngSubmit)="signIn()" class="form px-3 pb-3">
|
||||
<label class="field">
|
||||
<span class="label-text">อีเมล์</span>
|
||||
<input type="email" formControlName="username" autocomplete="username" placeholder="nuttakit@gmail.com" required />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label-text">รหัสผ่าน</span>
|
||||
<input type="password" formControlName="password" autocomplete="current-password" required />
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<label class="stay-signed">
|
||||
<input type="checkbox" formControlName="remember" />
|
||||
<span>จดจำรหัสผ่าน</span>
|
||||
</label>
|
||||
<!-- <fa-icon [icon]="faCoffee" /> -->
|
||||
<button type="submit" class="primary" [disabled]="!(loginForm.get('username')?.valid && loginForm.get('password')?.value)">
|
||||
เข้าสู่ระบบ
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="alt-options">
|
||||
<button type="button" class="biometric" (click)="useBiometric()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" fill="currentColor" opacity=".9"/>
|
||||
<path d="M6.2 10.9A6 6 0 0 1 12 6a6 6 0 0 1 5.8 4.9M12 22v-2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 12a8 8 0 0 1 16 0v1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>เข้าสู่ระบบด้วย Windows Hello / Touch ID</span>
|
||||
</button>
|
||||
|
||||
<a class="help-link" href="#" (click)="$event.preventDefault(); forgotPassword()">ลืมรหัส ใช่ หรือ ไม่?</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a href="#" (click)="$event.preventDefault(); createAccount()">สร้างบัญชี</a>
|
||||
<span class="divider">•</span>
|
||||
<a href="#" (click)="$event.preventDefault(); privacy()">Privacy & terms</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<any>();
|
||||
|
||||
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<void> {
|
||||
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']);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
<section class="dashboard">
|
||||
<header class="dashboard__hero">
|
||||
<div class="hero__text">
|
||||
<p class="eyebrow">ภาพรวมบัญชี</p>
|
||||
<h1>ยินดีต้อนรับกลับ, {{ ownerName }}</h1>
|
||||
<p class="hero__subtitle">
|
||||
จดบันทึกรายรับรายจ่าย และดูสรุปต่อปี เดือน สัปดาห์ ได้ในหน้าเดียว
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero__actions">
|
||||
<button class="btn btn--primary">สร้างรายงานด่วน</button>
|
||||
<!-- <button class="btn btn--ghost">อัปโหลดใบเสร็จ</button> -->
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dashboard__periods">
|
||||
<article class="period-card" *ngFor="let summary of periodSummaries">
|
||||
<header class="period-card__header">
|
||||
<span class="period-card__badge" [ngClass]="'period-card__badge--' + summary.badge">
|
||||
{{ summary.label }}
|
||||
</span>
|
||||
<p>{{ summary.note }}</p>
|
||||
</header>
|
||||
<div class="period-card__values">
|
||||
<div>
|
||||
<p class="muted">รายรับ</p>
|
||||
<p class="income">{{ summary.income }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted">รายจ่าย</p>
|
||||
<p class="expense">{{ summary.expense }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted">คงเหลือสุทธิ</p>
|
||||
<p class="net">{{ summary.net }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="trend-chip">แนวโน้ม {{ summary.trend }}</span>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard__stats">
|
||||
<article class="stat-card" *ngFor="let card of kpiCards">
|
||||
<div class="stat-card__icon" [ngClass]="'accent-' + card.accent"></div>
|
||||
<div class="stat-card__body">
|
||||
<p class="stat-card__label">{{ card.label }}</p>
|
||||
<div class="stat-card__value">{{ card.value }}</div>
|
||||
<p class="stat-card__trend">{{ card.trend }} · {{ card.context }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="ledger-grid">
|
||||
<article class="panel quick-log">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>บันทึกรายการแบบรวดเร็ว</h2>
|
||||
<p>จดรายรับรายจ่ายภายในไม่กี่คลิก</p>
|
||||
</div>
|
||||
</div>
|
||||
<form class="quick-log__form">
|
||||
<label>
|
||||
<span>ประเภท</span>
|
||||
<div class="quick-log__toggle">
|
||||
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'i' }" (click)="mode = 'i'">รายรับ</button>
|
||||
<button type="button" class="toggle-btn" [ngClass]="{ 'is-active': mode == 'e' }" (click)="mode = 'e'">รายจ่าย</button>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>วันที่</span>
|
||||
<!-- <input type="text" disabled placeholder="10/04/2025 เวลา 12:00"/> -->
|
||||
|
||||
<input type="datetime-local"/>
|
||||
</label>
|
||||
<div class="quick-log__grid">
|
||||
<label>
|
||||
<span>หมวดหมู่</span>
|
||||
@if(mode == 'i'){
|
||||
<select class="w-full h-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white">
|
||||
<option value="">ไม่เลือก</option>
|
||||
@for (item of myDropAct.income; track item.dtlcod) {
|
||||
<option [value]="item.dtlcod">
|
||||
{{ item.dtlnam }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}@else if(mode == 'e'){
|
||||
<select class="w-full h-full px-4 py-2 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-50 transition-all bg-white">
|
||||
<option value="">ไม่เลือก</option>
|
||||
@for (item of myDropAct.expense; track item.dtlcod) {
|
||||
<option [value]="item.dtlcod">
|
||||
{{ item.dtlnam }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</label>
|
||||
<label>
|
||||
<span>ยอดเงิน (฿)</span>
|
||||
<input type="number" placeholder="0.00" />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>บันทึกเพิ่มเติม</span>
|
||||
<textarea rows="3" placeholder="รายละเอียดการรับ/จ่าย"></textarea>
|
||||
</label>
|
||||
<button type="button" class="btn btn--primary">บันทึกรายการ</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="panel ledger-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สมุดบันทึกล่าสุด</h2>
|
||||
<p>แยกสีระหว่างรายรับและรายจ่าย</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--compact">ดูทั้งหมด</button>
|
||||
</div>
|
||||
<div class="ledger-table" [class.is-scrollable]="myActData.length > 5">
|
||||
<div class="ledger-row ledger-head">
|
||||
<span>รายการ</span>
|
||||
<span>หมวดหมู่</span>
|
||||
<span>ยอดเงิน</span>
|
||||
<span>บันทึก</span>
|
||||
</div>
|
||||
<!-- @for (idx of myActData; track i; let i = $index) {
|
||||
<div class="ledger-row">
|
||||
<div class="ledger-main">
|
||||
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
|
||||
{{ idx.type === 'i' ? 'รับ' : 'จ่าย' }}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<p class="ledger-title">{{ idx.title }}</p>
|
||||
<p class="ledger-date">{{ idx.date }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="ledger-category">{{ idx.category }}</span>
|
||||
|
||||
<span class="ledger-amount" [ngClass]="idx.type === 'i' ? 'is-credit' : 'is-debit'">
|
||||
{{ idx.amount }}
|
||||
</span>
|
||||
|
||||
<span class="ledger-note">{{ idx.note }}</span>
|
||||
</div>
|
||||
} -->
|
||||
@for (idx of myActData; track idx.actseq; let i = $index) {
|
||||
<div class="ledger-row">
|
||||
|
||||
<div class="ledger-main">
|
||||
<span class="pill" [ngClass]="idx.acttyp === 'i' ? 'pill--income' : 'pill--expense'">
|
||||
{{ idx.acttyp === 'i' ? 'รับ' : 'จ่าย' }}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<p class="ledger-title">{{ idx.acttypnam }}</p>
|
||||
<p class="ledger-date">{{ idx.actacpdtm ?? '' | dtmtodatetime}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="ledger-category">{{ idx.actcatnam }}</span>
|
||||
|
||||
<span class="ledger-amount" [ngClass]="idx.acttyp === 'i' ? 'is-credit' : 'is-debit'">
|
||||
{{ idx.actqty }}
|
||||
</span>
|
||||
|
||||
<span class="ledger-note">{{ idx.actcmt }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard__grid">
|
||||
<!-- <article class="panel panel--main">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>แนวโน้มรายรับ</h2>
|
||||
<p>สรุป 6 เดือนล่าสุด</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--compact">ดาวน์โหลดข้อมูล</button>
|
||||
</div>
|
||||
<div class="trend-chart">
|
||||
<div class="trend-chart__bar" *ngFor="let point of revenueTrend">
|
||||
<span class="trend-chart__value" [style.height.%]="point.value"></span>
|
||||
<span class="trend-chart__label">{{ point.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article> -->
|
||||
|
||||
<article class="panel panel--main pie-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สัดส่วนค่าใช้จ่าย</h2>
|
||||
<p>ดูหมวดไหนใช้เงินมากที่สุด</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--compact">จัดการหมวดหมู่</button>
|
||||
</div>
|
||||
<div class="pie-panel__content">
|
||||
<div class="pie-chart" [style.background]="ActSumDataGradient">
|
||||
<div class="pie-chart__center">
|
||||
<p>รวมเดือนนี้</p>
|
||||
<strong>{{myActSumData.summary.totalExpense}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="pie-legend">
|
||||
<li class="pie-legend__item" *ngFor="let idx of myActSumData.pie.expense">
|
||||
<span class="swatch" [style.background]="idx.color"></span>
|
||||
<div>
|
||||
<p class="pie-legend__label">{{ idx.label }}</p>
|
||||
<p class="pie-legend__value">{{ idx.percent }}%</p>
|
||||
<p class="pie-legend__value">{{ idx.value }} บาท</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<!-- ตัวเลขซ้อนทับกัน -->
|
||||
<article class="panel panel--side">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สรุปสภาพคล่อง</h2>
|
||||
<p>อัปเดตล่าสุด 5 นาทีที่แล้ว</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ratio-list">
|
||||
<div class="ratio" *ngFor="let ratio of quickRatios" [ngClass]="'ratio--' + ratio.status">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.5rem;">
|
||||
<p style="margin:0;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ ratio.label }}
|
||||
</p>
|
||||
<span style="margin-left:0.5rem;flex:0 0 auto">{{ ratio.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel alerts-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>การแจ้งเตือนสำคัญ</h2>
|
||||
<p>จัดลำดับงานค้างก่อนครบกำหนด</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert" *ngFor="let alert of alerts">
|
||||
<div>
|
||||
<p class="alert__title">{{ alert.title }}</p>
|
||||
<p class="alert__detail">{{ alert.detail }}</p>
|
||||
</div>
|
||||
<span class="alert__tag">{{ alert.tag }}</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel tasks-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>รายการยอดค้างจ่าย</h2>
|
||||
<p>ช่วยเตือนความจำให้</p>
|
||||
</div>
|
||||
<button class="btn btn--primary btn--compact" (click)="isModalOpen = true">เพิ่มงาน</button>
|
||||
</div>
|
||||
<ul class="task-list">
|
||||
<li class="task" *ngFor="let task of tasks">
|
||||
<div>
|
||||
<p class="task__title">{{ task.title }}</p>
|
||||
<p class="task__due">{{ task.due }}</p>
|
||||
</div>
|
||||
<span class="task__badge">{{ task.priority }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@if(isModalOpen == true){
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 backdrop-blur-sm transition-all duration-300 ease-in-out" role="dialog" aria-modal="true" [formGroup]="arrearsForm">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-auto overflow-hidden transform scale-100 transition-all duration-300 ease-out">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="flex items-center justify-between gap-4 px-6 py-5 border-b bg-linear-to-r from-rose-50 to-white">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-rose-600" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path d="M12 2v6M6 12h12M4 20h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 m-0">เพิ่มยอดค้างชำระ</h2>
|
||||
<p class="text-sm text-gray-500 m-0">บันทึกยอดที่ยังค้างชำระเพื่อการติดตาม</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" (click)="isModalOpen = false" class="text-gray-400 hover:text-rose-600 p-2 rounded-md transition-colors duration-200" aria-label="ปิด">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Form -->
|
||||
<form class="px-6 py-6 bg-white" (ngSubmit)="onArrearsSubmit()" autocomplete="off" novalidate>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
|
||||
<!-- จำนวนเงิน -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">จำนวนเงิน (฿)</span>
|
||||
<div class="mt-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
id="amount"
|
||||
formControlName="amount"
|
||||
placeholder="0.00"
|
||||
class="w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"
|
||||
/>
|
||||
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">THB</span>
|
||||
</div>
|
||||
@if(arrearsForm.get('amount')?.touched && arrearsForm.get('amount')?.invalid) {
|
||||
<p class="mt-1 text-xs text-red-600">
|
||||
กรุณากรอกจำนวนเงินที่ถูกต้อง
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">วันครบกำหนกจ่าย</span>
|
||||
<div class="mt-1 relative">
|
||||
<input type="datetime-local" formControlName="expdtm" class=" w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"/>
|
||||
</div>
|
||||
@if(arrearsForm.get('expdtm')?.touched && arrearsForm.get('expdtm')?.invalid) {
|
||||
<p class="mt-1 text-xs text-red-600">
|
||||
กรุณาระบุวันครบกำหนดชำระ
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
|
||||
<!-- เหตุผล -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">เหตุผล</span>
|
||||
<input
|
||||
type="text"
|
||||
id="reason"
|
||||
formControlName="reason"
|
||||
placeholder="เช่น บิลค้างชำระจากผู้ขาย"
|
||||
class="mt-1 w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 transition-all"
|
||||
/>
|
||||
@if(arrearsForm.get('reason')?.touched && arrearsForm.get('reason')?.invalid) {
|
||||
<p class="mt-1 text-xs text-red-600">
|
||||
กรุณากรอกเหตุผล
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
|
||||
<!-- บันทึกเพิ่มเติม -->
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-gray-700">บันทึกเพิ่มเติม (ไม่บังคับ)</span>
|
||||
<textarea
|
||||
rows="3"
|
||||
formControlName="note"
|
||||
placeholder="รายละเอียดเพิ่มเติม (เช่น เลขใบแจ้งหนี้ หรือ ผู้ติดต่อ)"
|
||||
class="mt-1 w-full px-4 py-2 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-rose-400 focus:border-rose-500 resize-none transition-all"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="flex items-center justify-end gap-3 pt-4 border-t mt-4">
|
||||
<button type="button" (click)="isModalOpen = false" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300 transition-colors duration-200">
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button type="submit" class="rounded-2xl">
|
||||
บันทึก
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -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})`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<section class="report">
|
||||
<header class="report__header">
|
||||
<div>
|
||||
<p class="eyebrow">สรุปรายงาน</p>
|
||||
<h1>รายงานรายรับรายจ่าย</h1>
|
||||
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
|
||||
</div>
|
||||
<div class="report__actions">
|
||||
<button class="btn btn--ghost">ส่งออกเป็น Excel</button>
|
||||
<button class="btn btn--primary" (click)="openPreview()">ปริ้นรายงาน</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="summary-grid">
|
||||
<article class="summary-card" *ngFor="let card of summaryCards">
|
||||
<p class="summary-card__label">{{ card.label }}</p>
|
||||
<h2>{{ card.value }}</h2>
|
||||
<p class="summary-card__detail">{{ card.detail }}</p>
|
||||
<span class="summary-card__tone" [ngClass]="'tone-' + card.tone"></span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="report__content">
|
||||
<article class="panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สมุดรายวัน</h2>
|
||||
<p>บันทึกรายรับรายจ่ายทั้งหมดในช่วงเวลา</p>
|
||||
</div>
|
||||
<button class="btn btn--compact btn--ghost">กรองข้อมูล</button>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="table__head">
|
||||
<span>วันที่</span>
|
||||
<span>เลขที่เอกสาร</span>
|
||||
<span>หัวข้อ</span>
|
||||
<span>หมวดหมู่</span>
|
||||
<span class="amount-col">ยอดเงิน</span>
|
||||
</div>
|
||||
<div class="table__row" *ngFor="let record of formattedRecords">
|
||||
<span>{{ record.date }}</span>
|
||||
<span class="mono">{{ record.doc }}</span>
|
||||
<span>
|
||||
<strong>{{ record.topic }}</strong>
|
||||
<small class="muted">{{ record.type === 'income' ? 'รายรับ' : 'รายจ่าย' }}</small>
|
||||
</span>
|
||||
<span>{{ record.category }}</span>
|
||||
<span class="amount-col" [ngClass]="record.tone">{{ record.displayAmount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel pie-panel">
|
||||
<div class="panel__header">
|
||||
<div>
|
||||
<h2>สัดส่วนค่าใช้จ่าย</h2>
|
||||
<p>เปรียบเทียบหมวดหลักของรายจ่ายเดือนนี้</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pie-panel__content">
|
||||
<div class="pie-chart" [style.background]="expenseGradient">
|
||||
<div class="pie-chart__center">
|
||||
<p>รวมรายจ่าย</p>
|
||||
<strong>฿732K</strong>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="pie-legend">
|
||||
<li *ngFor="let part of expenseBreakdown">
|
||||
<span class="swatch" [style.background]="part.color"></span>
|
||||
<div>
|
||||
<p class="legend-label">{{ part.label }}</p>
|
||||
<p class="legend-value">{{ part.value }}%</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="preview-modal" *ngIf="printPreviewOpen">
|
||||
<div class="preview-modal__backdrop" (click)="closePreview()"></div>
|
||||
<div class="preview-modal__content">
|
||||
<header class="preview-modal__header">
|
||||
<div>
|
||||
<p class="eyebrow">Print Preview</p>
|
||||
<h2>รายงานรายรับรายจ่าย</h2>
|
||||
<p class="muted">ช่วงวันที่ {{ reportRange.start }} - {{ reportRange.end }}</p>
|
||||
</div>
|
||||
<div class="preview-modal__actions">
|
||||
<button class="btn btn--ghost" (click)="closePreview()">ปิด</button>
|
||||
<button class="btn btn--primary">พิมพ์</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="preview-sheet">
|
||||
<div class="preview-sheet__header">
|
||||
<div>
|
||||
<h3>Accounting Summary</h3>
|
||||
<p>Prepared on {{ reportRange.end }}</p>
|
||||
</div>
|
||||
<div class="preview-totals">
|
||||
<div *ngFor="let total of previewTotals">
|
||||
<p>{{ total.label }}</p>
|
||||
<strong>{{ total.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-pie">
|
||||
<div class="mini-pie" [style.background]="expenseGradient"></div>
|
||||
<ul>
|
||||
<li *ngFor="let part of expenseBreakdown">
|
||||
<span class="swatch" [style.background]="part.color"></span>
|
||||
<span>{{ part.label }} · {{ part.value }}%</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>วันที่</th>
|
||||
<th>เลขที่</th>
|
||||
<th>หัวข้อ</th>
|
||||
<th>หมวดหมู่</th>
|
||||
<th>ยอดเงิน</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let record of formattedRecords">
|
||||
<td>{{ record.date }}</td>
|
||||
<td>{{ record.doc }}</td>
|
||||
<td>{{ record.topic }}</td>
|
||||
<td>{{ record.category }}</td>
|
||||
<td [ngClass]="record.tone">{{ record.displayAmount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<div
|
||||
class="h-screen bg-linear-to-b from-amber-950 to-amber-900 text-gray-100 shadow-2xl flex flex-col relative transition-all duration-300 ease-in-out"
|
||||
[@sidebarState]="isOpen ? 'expanded' : 'collapsed'">
|
||||
|
||||
<button
|
||||
(click)="toggleSidebar()"
|
||||
class="absolute -right-3 top-6 bg-amber-700 hover:bg-amber-600 text-white rounded-full p-2 shadow-md transition-all duration-300">
|
||||
<i class="fas" [ngClass]="isOpen ? 'fa-angle-left' : 'fa-angle-right'"></i>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3 p-5">
|
||||
<h3 *ngIf="isOpen" class="text-2xl font-bold transition-all duration-300">
|
||||
Global Sidebar
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<hr class="border-t border-amber-700 mx-4 my-4 opacity-70">
|
||||
|
||||
<ul class="flex flex-col gap-2 px-2 grow">
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/main/dashboard')">
|
||||
<i class="fas fa-tachometer-alt text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium">Dashboard</span>
|
||||
</li>
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/main/profile')">
|
||||
<i class="fas fa-user-circle text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium">Profile</span>
|
||||
</li>
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer
|
||||
hover:bg-amber-800 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="navigate('/main/report')">
|
||||
<i class="fas fa-chart-bar text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium">Report</span>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer mt-auto
|
||||
hover:bg-red-700 hover:shadow-lg transition-all duration-300 ease-in-out"
|
||||
(click)="logout()">
|
||||
<i class="fas fa-sign-out-alt text-xl group-hover:scale-110 transition-transform"></i>
|
||||
<span *ngIf="isOpen" class="text-lg font-medium text-red-200">Logout</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="isMobile && showOverlay"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300"
|
||||
(click)="toggleSidebar()">
|
||||
</div>
|
||||
<!--
|
||||
|
||||
<div class="flex-1 bg-gray-100 text-gray-900 overflow-y-auto">
|
||||
<router-outlet></router-outlet>
|
||||
</div> -->
|
||||
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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; /* ✅ ให้ข้อความตรงกลาง */
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="justify-content-center flex-column">
|
||||
<!-- <h2>Login | เข้าสู่ระบบ ({{ mode }})</h2> -->
|
||||
@if (mode == "default") {
|
||||
<app-login-page [mode]="mode" (signedIn)="onSignInSubmit($event)"></app-login-page>
|
||||
} @else if(mode == "forgot-password"){
|
||||
<app-login-forgot (otpEventSubmit)="onOtpSendSubmit($event)" (otpVerifyEventSubmit)="onVerifySubmit($event)"></app-login-forgot>
|
||||
}
|
||||
<!-- @else {
|
||||
|
||||
} -->
|
||||
</div>
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<app-main-dashboard></app-main-dashboard>
|
||||
@@ -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();
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
.main-container {
|
||||
flex: 1;
|
||||
background: #f5f5f5;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar (เฉพาะ main) -->
|
||||
<app-sidebar></app-sidebar>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50 text-gray-900">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
@@ -2,10 +2,17 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>NgTtcFrontend</title>
|
||||
<title>AccountingNgNuttakit</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Kanit:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"> -->
|
||||
<!-- <link rel="stylesheet" href="node_modules/font-awesome/css/all.min.css"> -->
|
||||
<!-- <link rel="stylesheet" href="styles.css"> -->
|
||||
<!-- <link rel="stylesheet" href="styles.css"> -->
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user