ne radi vite server
This commit is contained in:
@@ -10,13 +10,19 @@ export default defineConfig({
|
||||
}),
|
||||
|
||||
server: {
|
||||
host: true,
|
||||
host: '0.0.0.0',
|
||||
port: 4321,
|
||||
},
|
||||
|
||||
image: {
|
||||
// Dodaj domene s kojih Astro smije povlačiti i optimizirati slike
|
||||
domains: ['localhost', '127.0.0.1','v003-backend.captain.mitteworkspace.cloud'],
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'frontend-operativa.local.mitteworkspace.cloud',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
build: {
|
||||
@@ -24,11 +30,22 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
vite: {
|
||||
build: {
|
||||
// Smanji intenzitet optimizacije tijekom dev-a
|
||||
minify: false,
|
||||
cssMinify: false,
|
||||
},
|
||||
plugins: [tailwindcss()],
|
||||
// KLJUČNO ZA TAILWIND v4 + NODE standalone SSR:
|
||||
ssr: {
|
||||
// Prisiljava Vite da uključi Tailwind v4 stilove u serverski bundle
|
||||
noExternal: ['tailwindcss', '@tailwindcss/vite']
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/node_modules/**', '**/dist/**'],
|
||||
},
|
||||
allowedHosts: ['.mitteworkspace.cloud']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
469
001.FRONTEND/package-lock.json
generated
469
001.FRONTEND/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^10.0.6",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"astro": "^6.1.9",
|
||||
"astro": "^6.3.7",
|
||||
"flowbite": "^4.0.1",
|
||||
"photoswipe": "^5.4.4",
|
||||
"tailwindcss": "^4.2.4"
|
||||
@@ -203,17 +203,6 @@
|
||||
"node": ">= 20.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
@@ -224,16 +213,6 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
@@ -1161,40 +1140,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@oslojs/encoding": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
|
||||
"integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.132.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
|
||||
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@@ -1205,270 +1156,6 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
|
||||
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
|
||||
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
|
||||
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
|
||||
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
|
||||
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
|
||||
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||
@@ -2210,16 +1897,6 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
@@ -2414,80 +2091,6 @@
|
||||
"sharp": "^0.34.0"
|
||||
}
|
||||
},
|
||||
"node_modules/astro/node_modules/vite": {
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
|
||||
"integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -5304,40 +4907,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
|
||||
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.132.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.2",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.2",
|
||||
"@rolldown/binding-darwin-x64": "1.0.2",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.2",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.2",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.2",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.2",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.2",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
|
||||
@@ -5643,9 +5212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz",
|
||||
"integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -6013,17 +5582,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
|
||||
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
|
||||
"integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.2",
|
||||
"tinyglobby": "^0.2.16"
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -6039,10 +5608,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
@@ -6055,18 +5623,15 @@
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"name": "poslovanje",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"overrides": {
|
||||
"vite": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
@@ -10,12 +13,12 @@
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"start": "node ./dist/server/entry.mjs"
|
||||
"start": "NODE_OPTIONS=--max-old-space-size=4096 npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^10.0.6",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"astro": "^6.1.9",
|
||||
"astro": "^6.3.7",
|
||||
"flowbite": "^4.0.1",
|
||||
"photoswipe": "^5.4.4",
|
||||
"tailwindcss": "^4.2.4"
|
||||
|
||||
9
001.FRONTEND/public/css/all.min.css
vendored
Normal file
9
001.FRONTEND/public/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
001.FRONTEND/public/js/flowbite.min.js
vendored
Normal file
2
001.FRONTEND/public/js/flowbite.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
001.FRONTEND/public/webfonts/fa-brands-400.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/fa-regular-400.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/fa-solid-900.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/inter/inter-400.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/inter/inter-400.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/inter/inter-500.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/inter/inter-500.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/inter/inter-600.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/inter/inter-600.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/inter/inter-700.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/inter/inter-700.woff2
Normal file
Binary file not shown.
BIN
001.FRONTEND/public/webfonts/inter/inter-800.woff2
Normal file
BIN
001.FRONTEND/public/webfonts/inter/inter-800.woff2
Normal file
Binary file not shown.
@@ -54,7 +54,9 @@ import Button from "./Button.astro";
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { login } from "../lib/api";
|
||||
import { login, fetchCurrentUser } from "../lib/api";
|
||||
// 🚀 Ponovno uvozimo centraliziranu funkciju za rute
|
||||
import { dohvatiRutuPoUlozi } from "../utils/auth";
|
||||
|
||||
const loginForm = document.querySelector('#login-form') as HTMLFormElement;
|
||||
const btn = document.querySelector('#login-btn') as HTMLButtonElement;
|
||||
@@ -64,10 +66,8 @@ import Button from "./Button.astro";
|
||||
|
||||
if (!btn) return;
|
||||
|
||||
// Pohranjujemo originalni sadržaj (ikonu + tekst)
|
||||
const originalContent = btn.innerHTML;
|
||||
|
||||
// Vizualni feedback (Loading stanje)
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-3"></i> AUTENTIFIKACIJA...';
|
||||
|
||||
@@ -76,25 +76,32 @@ import Button from "./Button.astro";
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
try {
|
||||
// 1. Prijava i pohrana JWT tokena
|
||||
const result = await login(email, password);
|
||||
|
||||
if (result.success) {
|
||||
// 2. Dohvat profila ulogiranog korisnika
|
||||
const user = await fetchCurrentUser();
|
||||
|
||||
sessionStorage.setItem('pending_toast', JSON.stringify({
|
||||
type: 'success',
|
||||
message: 'Pristup odobren. Dobrodošli u sustav.'
|
||||
}));
|
||||
window.location.href = '/';
|
||||
|
||||
// 3. 🚀 PREUSMJERAVANJE PREKO POMOĆNE FUNKCIJE:
|
||||
// Namjerno NE prosljeđujemo user.id jer želimo da serviser sleti na opću /operativa/serviseri stazu
|
||||
const ciljanaRuta = dohvatiRutuPoUlozi(user?.uloga);
|
||||
|
||||
window.location.href = ciljanaRuta;
|
||||
} else {
|
||||
// Poziv tvog global-toast sustava
|
||||
// @ts-ignore
|
||||
window.showToast?.(result.error || "Pristup odbijen: Neispravni podaci.", "error");
|
||||
|
||||
// Vraćanje gumba u normalu
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
}
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
console.error("Login krah na klijentu:", err);
|
||||
// @ts-ignore
|
||||
window.showToast?.("Sustav nedostupan. Provjerite vezu.", "error");
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalContent;
|
||||
|
||||
@@ -1,59 +1,193 @@
|
||||
---
|
||||
// src/components/NaslovList.astro
|
||||
// src/components/RadniNalogLista.astro
|
||||
import NaslovList from "./NaslovList.astro";
|
||||
|
||||
interface Props {
|
||||
naslov: string;
|
||||
ukupno: number;
|
||||
label1: string;
|
||||
count1: number;
|
||||
filter1: string;
|
||||
label2: string;
|
||||
count2: number;
|
||||
filter2: string;
|
||||
label3?: string;
|
||||
count3?: number;
|
||||
filter3?: string;
|
||||
limit?: number;
|
||||
naslov?: string;
|
||||
prikaziNaslov?: boolean;
|
||||
}
|
||||
|
||||
const { naslov, ukupno, label1, count1, filter1, label2, count2, filter2, label3, count3, filter3 } = Astro.props;
|
||||
const {
|
||||
limit = 0,
|
||||
naslov = "Zadnje aktivnosti",
|
||||
prikaziNaslov = true,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex flex-row justify-between items-center w-full min-h-[40px]">
|
||||
|
||||
<h2 id="filter-title" class="p-0 m-0 text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight italic leading-none">
|
||||
{naslov}
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-wrap justify-end items-center gap-2 lg:gap-3">
|
||||
<div
|
||||
class="stat-card flex items-center gap-2 px-2.5 py-1.5 bg-emerald-50 dark:bg-emerald-900/20 rounded-xl border border-emerald-100 dark:border-emerald-800/50 cursor-pointer transition-all active:scale-95 hover:border-emerald-400"
|
||||
data-filter={filter1}
|
||||
>
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-500"></div>
|
||||
<span class="text-[9px] lg:text-[10px] font-black uppercase text-emerald-700 dark:text-emerald-400 whitespace-nowrap">
|
||||
{label1}: {count1}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-6 group" id="radni-nalozi-sekcija" data-state="loading">
|
||||
{prikaziNaslov && (
|
||||
<NaslovList
|
||||
naslov={naslov}
|
||||
ukupno={0}
|
||||
label1="Planirano"
|
||||
count1={0}
|
||||
filter1="planirano"
|
||||
label2="U radu"
|
||||
count2={0}
|
||||
filter2="u_radu"
|
||||
label3="Završeno"
|
||||
count3={0}
|
||||
filter3="zavrseno"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
||||
|
||||
<div
|
||||
class="stat-card flex items-center gap-2 px-2.5 py-1.5 bg-red-50 dark:bg-red-900/20 rounded-xl border border-red-100 dark:border-red-800/50 cursor-pointer transition-all active:scale-95 hover:border-red-400"
|
||||
data-filter={filter2}
|
||||
id="nalozi-loader"
|
||||
class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse group-data-[state=loaded]:hidden group-data-[state=error]:hidden"
|
||||
>
|
||||
<div class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
|
||||
<span class="text-[9px] lg:text-[10px] font-black uppercase text-red-700 dark:text-red-400 whitespace-nowrap">
|
||||
{label2}: {count2}
|
||||
</span>
|
||||
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija radnih naloga...
|
||||
</div>
|
||||
|
||||
{label3 && count3 !== undefined && filter3 && (
|
||||
<div
|
||||
class="stat-card flex items-center gap-2 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-900/20 rounded-xl border border-gray-100 dark:border-gray-800/50 cursor-pointer transition-all active:scale-95 hover:border-gray-400"
|
||||
data-filter={filter3}
|
||||
>
|
||||
<div class="w-2 h-2 rounded-full bg-gray-400"></div>
|
||||
<span class="text-[9px] lg:text-[10px] font-black uppercase text-gray-700 dark:text-gray-400 whitespace-nowrap">
|
||||
{label3}: {count3}
|
||||
</span>
|
||||
|
||||
<div id="nalozi-list" class="space-y-0">
|
||||
<div id="nalozi-placeholder" class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest group-data-[state=loaded]:hidden">
|
||||
Nema zapisa za odabrani status
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="rn-bridge" data-limit={limit} data-naslov={naslov}></span>
|
||||
|
||||
<script>
|
||||
import { fetchCurrentUser, fetchDashboardData, fetchServiserTerminalData } from "../lib/api";
|
||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
||||
import { initFilters } from "../scripts/filters.js";
|
||||
|
||||
async function renderirajRadneNalogeKlijentski() {
|
||||
const container = document.getElementById('nalozi-list');
|
||||
const sekcija = document.getElementById('radni-nalozi-sekcija');
|
||||
const bridge = document.getElementById('rn-bridge');
|
||||
|
||||
if (!container || !sekcija || !bridge) return;
|
||||
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||||
|
||||
if (!token) {
|
||||
sekcija.setAttribute('data-state', 'error');
|
||||
container.innerHTML = `
|
||||
<div class="p-20 text-center flex flex-col items-center justify-center gap-3">
|
||||
<div class="w-12 h-12 bg-red-600/10 text-red-600 rounded-xl flex items-center justify-center text-lg mb-2">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<h4 class="text-xs font-black uppercase tracking-widest text-red-600">Pristup odbijen</h4>
|
||||
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-wider italic">Niste autorizirani za pregled operativnih naloga flote.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = parseInt(bridge.getAttribute('data-limit') || '0');
|
||||
const originalNaslov = bridge.getAttribute('data-naslov') || 'Zadnje aktivnosti';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const statusFilter = urlParams.get('status')?.toLowerCase();
|
||||
|
||||
try {
|
||||
sekcija.setAttribute('data-state', 'loading');
|
||||
|
||||
const user = await fetchCurrentUser();
|
||||
const ulogaGore = user?.uloga ? String(user.uloga).toUpperCase().trim() : '';
|
||||
|
||||
let nalozi = [];
|
||||
|
||||
if (ulogaGore === 'SERVISER' && user?.id) {
|
||||
const terminalData = await fetchServiserTerminalData(user.id);
|
||||
nalozi = terminalData?.radni_nalozi || [];
|
||||
} else {
|
||||
const data = await fetchDashboardData();
|
||||
nalozi = data?.nalozi || [];
|
||||
}
|
||||
|
||||
// 3. Proračun statistike na klijentskoj strani
|
||||
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
||||
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
||||
const zavrsenoCount = nalozi.filter(n => ['zavrsen', 'završeno', 'gotovo', 'zavrseno'].includes(n.status?.toLowerCase())).length;
|
||||
|
||||
// 4. 🚀 USKLAĐENO HVAĆANJE I FILTRIRANJE PREMA TVOM data-filter PROPSU:
|
||||
const card1 = sekcija.querySelector('[data-filter="planirano"]');
|
||||
if (card1) {
|
||||
const span = card1.querySelector('span');
|
||||
if (span) span.textContent = `Planirano: ${planiranoCount}`;
|
||||
if (planiranoCount > 0) card1.classList.remove('hidden'); else card1.classList.add('hidden');
|
||||
}
|
||||
|
||||
const card2 = sekcija.querySelector('[data-filter="u_radu"]');
|
||||
if (card2) {
|
||||
const span = card2.querySelector('span');
|
||||
if (span) span.textContent = `U radu: ${uRaduCount}`;
|
||||
if (uRaduCount > 0) card2.classList.remove('hidden'); else card2.classList.add('hidden');
|
||||
}
|
||||
|
||||
const card3 = sekcija.querySelector('[data-filter="zavrseno"]');
|
||||
if (card3) {
|
||||
const span = card3.querySelector('span');
|
||||
if (span) span.textContent = `Završeno: ${zavrsenoCount}`;
|
||||
if (zavrsenoCount > 0) card3.classList.remove('hidden'); else card3.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 5. Filtriranje i limitiranje niza prema klijentskom stanju URL-a
|
||||
let filtriraniNalozi = statusFilter ? nalozi.filter(n => n.status?.toLowerCase() === statusFilter) : nalozi;
|
||||
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
|
||||
|
||||
const glavniNaslov = sekcija.querySelector('h2');
|
||||
if (glavniNaslov) {
|
||||
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
|
||||
}
|
||||
|
||||
sekcija.setAttribute('data-state', 'loaded');
|
||||
|
||||
// 7. Generiranje HTML kartica radnih naloga unutar tijela
|
||||
if (prikazaniNalozi.length === 0) {
|
||||
container.innerHTML = `<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">Nema zapisa za odabrani status</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = prikazaniNalozi.map((n) => {
|
||||
const statusMalo = n.status?.toLowerCase();
|
||||
const statusKlasa = getStatusColorClass(statusMalo);
|
||||
const ikona = statusMalo === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice';
|
||||
|
||||
const klijentPrikaz = n.kupac_naziv || n.klijent_naziv || 'Nepoznat klijent';
|
||||
const strojPrikaz = n.stroj_naziv || 'Bez stroja';
|
||||
const voziloPrikaz = n.vozilo_naziv || 'Bez vozila';
|
||||
|
||||
return `
|
||||
<a href="/operativa/radni-nalozi/${n.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
|
||||
<i class="fa-solid ${ikona}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${n.broj_naloga}</h4>
|
||||
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${formatStatus(statusMalo)}</p>
|
||||
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
|
||||
${klijentPrikaz} | ${voziloPrikaz} | ${strojPrikaz}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
|
||||
${formatStatus(statusMalo)}
|
||||
</span>
|
||||
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
|
||||
<i class="fa-solid fa-chevron-right text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
initFilters();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Kritična greška u klijentskom renderu radnih naloga:", err);
|
||||
sekcija.setAttribute('data-state', 'error');
|
||||
container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji operativnih podataka.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
renderirajRadneNalogeKlijentski();
|
||||
document.addEventListener('astro:after-swap', renderirajRadneNalogeKlijentski);
|
||||
</script>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
// src/components/Navbar.astro
|
||||
import site from "../data/site.json";
|
||||
import Button from "./Button.astro";
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
@@ -9,7 +10,6 @@ const currentPath = Astro.url.pathname;
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-20 items-center">
|
||||
|
||||
<!-- LOGO SEKCIJA -->
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center gap-3 group">
|
||||
<div class="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:rotate-6 transition-transform duration-300">
|
||||
@@ -21,87 +21,159 @@ const currentPath = Astro.url.pathname;
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="nav-auth-section" class="hidden flex items-center gap-4 group/auth" data-state="loading">
|
||||
|
||||
<div class="hidden group-data-[state=anonymous]/auth:block">
|
||||
<Button
|
||||
id="nav-login-btn"
|
||||
href="/login"
|
||||
variant="none"
|
||||
class="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-2xl text-xs font-black uppercase tracking-wider shadow-lg shadow-blue-500/20 active:scale-95 transition-all border border-blue-500 cursor-pointer italic"
|
||||
>
|
||||
<i class="fa-solid fa-right-to-bracket"></i> Prijava
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="hidden group-data-[state=authenticated]/auth:block">
|
||||
<Button
|
||||
id="mobile-menu-button"
|
||||
variant="none"
|
||||
class="w-12 h-12 flex items-center justify-center rounded-2xl bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300 active:scale-90 transition-all border border-gray-100 dark:border-gray-700 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700/50"
|
||||
>
|
||||
<i class="fa-solid fa-bars-staggered text-xl"></i>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE MENU GUMB -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<button
|
||||
id="mobile-menu-button"
|
||||
type="button"
|
||||
class="w-12 h-12 flex items-center justify-center rounded-2xl bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300 active:scale-90 transition-all border border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<i class="fa-solid fa-bars-staggered text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEBDEĆI MOBILNI IZBORNIK (Transparentan) -->
|
||||
<div
|
||||
id="mobile-menu"
|
||||
class="hidden absolute top-[calc(100%+0.5rem)] left-4 right-4
|
||||
class="hidden absolute top-[calc(100%+0.5rem)] md:left-auto left-4 right-4 md:right-8 md:w-80
|
||||
bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl
|
||||
border border-white/20 dark:border-gray-800/50
|
||||
p-4 rounded-[2.5rem] shadow-2xl z-[100]
|
||||
p-4 rounded-[2.5rem] shadow-2xl shadow-blue-500/5 z-[100]
|
||||
transition-all animate-in fade-in slide-in-from-top-4"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
{site.navigation.map((item) => (
|
||||
<a
|
||||
href={item.url}
|
||||
class="flex items-center justify-between px-6 py-4 rounded-[1.8rem] font-black text-[11px] uppercase tracking-widest text-gray-600 dark:text-gray-300 hover:bg-blue-600 hover:text-white transition-all duration-300 group"
|
||||
{site.navigation.map((item) => {
|
||||
// Provjera je li trenutna putanja aktivna (uzima u obzir i potencijalne dinamičke ID pod-rute)
|
||||
const isCurrent = currentPath === item.url || (item.url === '/operativa/serviseri' && currentPath.includes('/operativa/serviseri'));
|
||||
|
||||
return (
|
||||
<a
|
||||
href={item.url}
|
||||
data-nav-link={item.url}
|
||||
class={`flex items-center justify-between px-6 py-4 rounded-[1.8rem] font-black text-[11px] uppercase tracking-widest transition-all duration-300 group italic ${
|
||||
isCurrent
|
||||
? "bg-blue-600 text-white shadow-lg shadow-blue-500/20"
|
||||
: "text-gray-600 dark:text-gray-300 hover:bg-blue-600 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
<i class="fa-solid fa-chevron-right text-[10px] opacity-30 group-hover:translate-x-1 group-hover:opacity-100 transition-all"></i>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
<div class="w-full mt-1 mb-2">
|
||||
<Button
|
||||
href="/operativa/radni-nalozi/novi"
|
||||
variant="none"
|
||||
class="w-full bg-blue-600 text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center shadow-lg shadow-blue-500/40 active:scale-95 transition-all block italic"
|
||||
>
|
||||
{item.name}
|
||||
<i class="fa-solid fa-chevron-right text-[10px] opacity-30 group-hover:translate-x-1 group-hover:opacity-100 transition-all"></i>
|
||||
</a>
|
||||
))}
|
||||
<i class="fa-solid fa-plus mr-2"></i> Novi nalog
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr class="my-2 border-gray-100/50 dark:border-gray-800/50" />
|
||||
<a href="/operativa/radni-nalozi/novi" class="w-full bg-blue-600 text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center shadow-lg shadow-blue-500/40 active:scale-95 transition-all">
|
||||
<i class="fa-solid fa-plus mr-2"></i> Novi nalog
|
||||
</a>
|
||||
|
||||
<div class="w-full">
|
||||
<Button
|
||||
id="logout-mobile"
|
||||
variant="none"
|
||||
class="w-full bg-red-600/10 text-red-600 hover:bg-red-600 hover:text-white p-5 rounded-[1.8rem] font-black uppercase text-[11px] tracking-[0.2em] text-center active:scale-95 transition-all border border-red-100/30 dark:border-red-900/30 cursor-pointer block italic"
|
||||
>
|
||||
<i class="fa-solid fa-right-from-bracket mr-2"></i> Odjava iz sustava
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setupNavbar() {
|
||||
import { logout, fetchCurrentUser } from "../lib/api";
|
||||
|
||||
async function setupNavbar() {
|
||||
const btn = document.getElementById('mobile-menu-button');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const authSection = document.getElementById('nav-auth-section');
|
||||
const logoutMobile = document.getElementById('logout-mobile');
|
||||
|
||||
if (btn && menu) {
|
||||
// Re-inicijalizacija gumba za Astro View Transitions
|
||||
const newBtn = btn.cloneNode(true) as HTMLButtonElement;
|
||||
btn.parentNode?.replaceChild(newBtn, btn);
|
||||
if (!authSection) return;
|
||||
|
||||
newBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('hidden');
|
||||
});
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||||
const stanjeAutentifikacije = token ? 'authenticated' : 'anonymous';
|
||||
|
||||
// Zatvori menu na klik izvan njega
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menu.contains(e.target as Node) && !newBtn.contains(e.target as Node)) {
|
||||
menu.classList.add('hidden');
|
||||
// 1. Postavljamo stanje u DOM (odmah prebacuje vidljivost gumbova Prijava / Menu)
|
||||
authSection.setAttribute('data-state', stanjeAutentifikacije);
|
||||
authSection.classList.remove('hidden');
|
||||
authSection.classList.add('flex');
|
||||
|
||||
// 2. Dinamičko popravljanje URL putanje za servisera ako imamo token i menu elemente
|
||||
if (token && menu) {
|
||||
let userProfil = null;
|
||||
|
||||
try {
|
||||
// 🚀 OBRANA OD KRAHA: Omotavamo hvat u zaseban try-catch kako greška s API-ja ne bi srušila cijeli navbar
|
||||
userProfil = await fetchCurrentUser();
|
||||
} catch (apiErr) {
|
||||
console.warn("Navbar nije uspio dohvatiti trenutnog korisnika s API-ja:", apiErr);
|
||||
}
|
||||
|
||||
if (userProfil && userProfil.id) {
|
||||
const serviserLink = menu.querySelector('[data-nav-link="/operativa/serviseri"]') as HTMLAnchorElement;
|
||||
if (serviserLink) {
|
||||
serviserLink.href = `/operativa/serviseri/${userProfil.id}`;
|
||||
|
||||
// Popravak aktivne klase ukoliko smo točno na toj dinamičkoj ruti
|
||||
if (window.location.pathname === `/operativa/serviseri/${userProfil.id}`) {
|
||||
serviserLink.classList.add('bg-blue-600', 'text-white', 'shadow-lg', 'shadow-blue-500/20');
|
||||
serviserLink.classList.remove('text-gray-600', 'dark:text-gray-300');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Zatvori nakon odabira linka
|
||||
menu.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => menu.classList.add('hidden'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!btn || !menu) return;
|
||||
|
||||
// Resetiranje event listenera kloniranjem (sprječava duplanje na View Transitions okidanju)
|
||||
const newBtn = btn.cloneNode(true) as HTMLButtonElement;
|
||||
btn.parentNode?.replaceChild(newBtn, btn);
|
||||
|
||||
newBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menu.contains(e.target as Node) && !newBtn.contains(e.target as Node)) {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
menu.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => menu.classList.add('hidden'));
|
||||
});
|
||||
|
||||
logoutMobile?.addEventListener('click', () => {
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
// Logout funkcija
|
||||
const logout = () => {
|
||||
sessionStorage.removeItem('user_session');
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
document.getElementById('logout-btn')?.addEventListener('click', logout);
|
||||
document.getElementById('logout-mobile')?.addEventListener('click', logout);
|
||||
|
||||
// Okidanje na inicijalno učitavanje i Astro after-swap
|
||||
setupNavbar();
|
||||
document.addEventListener('astro:after-swap', setupNavbar);
|
||||
|
||||
</script>
|
||||
161
001.FRONTEND/src/components/PutniNalogLista.astro
Normal file
161
001.FRONTEND/src/components/PutniNalogLista.astro
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
// src/components/PutniNalogLista.astro
|
||||
import NaslovList from "./NaslovList.astro";
|
||||
|
||||
interface Props {
|
||||
naslov?: string;
|
||||
prikaziNaslov?: boolean;
|
||||
voziloId?: string | number;
|
||||
}
|
||||
|
||||
const {
|
||||
naslov = "Putni nalozi i rute",
|
||||
prikaziNaslov = true,
|
||||
voziloId = null,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<div class="space-y-6" id="putni-nalozi-sekcija">
|
||||
{prikaziNaslov && (
|
||||
<NaslovList
|
||||
naslov={naslov}
|
||||
ukupno={0}
|
||||
label1="Aktivne rute" count1={0} filter1="aktivan"
|
||||
label2="Završeni" count2={0} filter2="zavrsen"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
||||
<div id="pn-loader" class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse">
|
||||
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija putnih naloga...
|
||||
</div>
|
||||
|
||||
<div id="putni-nalozi-list" class="space-y-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span id="pn-bridge" data-vozilo-id={voziloId} data-naslov={naslov}></span>
|
||||
|
||||
<script>
|
||||
import { fetchPutniNaloziData } from "../lib/api";
|
||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
||||
import { initFilters } from "../scripts/filters.js";
|
||||
|
||||
async function renderirajPutneNalogeKlijentski() {
|
||||
const bridge = document.getElementById('pn-bridge');
|
||||
const container = document.getElementById('putni-nalozi-list');
|
||||
const loader = document.getElementById('pn-loader');
|
||||
if (!bridge || !container) return;
|
||||
|
||||
const voziloId = bridge.getAttribute('data-vozilo-id');
|
||||
const originalNaslov = bridge.getAttribute('data-naslov') || 'Putni nalozi i rute';
|
||||
|
||||
// Dohvat aktivnog filtera iz URL-a na klijentu (?status=aktivan)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const statusFilter = urlParams.get('status')?.toLowerCase();
|
||||
|
||||
try {
|
||||
// 1. Dohvat s API-ja (Sada sigurno čita localStorage token u pregledniku)
|
||||
const data = await fetchPutniNaloziData();
|
||||
|
||||
// Prilagodba ako API vraća omotan objekt ili čisti niz
|
||||
const sviPutniNalozi = Array.isArray(data) ? data : (data?.results || []);
|
||||
|
||||
// 2. STROGO FILTRIRANJE PREMA ID-JU VOZILA (Na razini podataka)
|
||||
let filtriraniNalozi = sviPutniNalozi;
|
||||
if (voziloId) {
|
||||
const ciljaniId = parseInt(voziloId);
|
||||
filtriraniNalozi = sviPutniNalozi.filter(pn => {
|
||||
const vId = pn.vozilo?.id || pn.vozilo || pn.vozilo_id;
|
||||
return parseInt(vId) === ciljaniId;
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Izračun statistike na klijentskoj strani (isključivo za ovo vozilo ako je ID prisutan)
|
||||
const aktivniCount = filtriraniNalozi.filter(pn => pn.status?.toLowerCase() === 'aktivan').length;
|
||||
const zavrseniCount = filtriraniNalozi.filter(pn => ['zavrsen', 'završeno'].includes(pn.status?.toLowerCase())).length;
|
||||
|
||||
// Opcionalno dodatno filtriranje po statusu s gornjih gumbića
|
||||
if (statusFilter) {
|
||||
filtriraniNalozi = filtriraniNalozi.filter(pn => pn.status?.toLowerCase() === statusFilter);
|
||||
}
|
||||
|
||||
// 4. Dinamičko ažuriranje brojača i naslova u DOM-u (NaslovList)
|
||||
const sekcija = document.getElementById('putni-nalozi-sekcija');
|
||||
if (sekcija) {
|
||||
const glavniNaslov = sekcija.querySelector('h2');
|
||||
if (glavniNaslov) {
|
||||
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
|
||||
}
|
||||
|
||||
const ukupnoBadge = sekcija.querySelector('h2 + span, .ukupno-badge');
|
||||
if (ukupnoBadge) {
|
||||
ukupnoBadge.textContent = filtriraniNalozi.length.toString();
|
||||
}
|
||||
|
||||
const aktivniBadge = sekcija.querySelector('[data-filter="aktivan"] span, [filter-type="aktivan"]');
|
||||
if (aktivniBadge) aktivniBadge.textContent = aktivniCount.toString();
|
||||
|
||||
const zavrseniBadge = sekcija.querySelector('[data-filter="zavrsen"] span, [filter-type="zavrsen"]');
|
||||
if (zavrseniBadge) zavrseniBadge.textContent = zavrseniCount.toString();
|
||||
}
|
||||
|
||||
// Sakrij loader
|
||||
if (loader) loader.classList.add('hidden');
|
||||
|
||||
// 5. Generiranje HTML-a (Preslikana točna struktura tvog GenericKarticaItem)
|
||||
if (filtriraniNalozi.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">
|
||||
Nema evidentiranih putnih naloga
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtriraniNalozi.map((pn) => {
|
||||
const statusMalo = pn.status?.toLowerCase();
|
||||
const statusKlasa = getStatusColorClass(statusMalo);
|
||||
const relacija = pn.relacija || pn.mjesto_odredista || 'Terenska lokacija';
|
||||
const vozilo = pn.vozilo_naziv || 'Dodijeljeno vozilo';
|
||||
const izvrsitelj = pn.korisnik_ime || 'Vozač tehničar';
|
||||
const pocetniKm = pn.pocetna_km?.toLocaleString('hr-HR') || 0;
|
||||
|
||||
return `
|
||||
<a href="/operativa/putni-nalozi/${pn.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
|
||||
<i class="fa-solid fa-route"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${pn.broj_naloga}</h4>
|
||||
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${relacija}</p>
|
||||
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
|
||||
Vozilo: ${vozilo} | Izvršitelj: ${izvrsitelj} | Početno: ${pocetniKm} KM
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
|
||||
${pn.status_prikaz || formatStatus(statusMalo)}
|
||||
</span>
|
||||
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
|
||||
<i class="fa-solid fa-chevron-right text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Pokretanje klijentskih filtera nakon što se DOM uspješno izgradi
|
||||
initFilters();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Kritična greška u klijentskom renderu putnih naloga:", err);
|
||||
if (container) container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji putnih naloga.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Izvršavanje pri učitavanju i re-inicijalizacija kod Astro View Transitions prebacivanja rute
|
||||
renderirajPutneNalogeKlijentski();
|
||||
document.addEventListener('astro:after-swap', renderirajPutneNalogeKlijentski);
|
||||
</script>
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
// src/components/RadniNalogLista.astro
|
||||
import { fetchDashboardData } from "../lib/api";
|
||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
||||
import NaslovList from "./NaslovList.astro";
|
||||
import GenericKarticaItem from "./GenericKarticaItem.astro";
|
||||
|
||||
interface Props {
|
||||
limit?: number;
|
||||
@@ -16,80 +13,190 @@ const {
|
||||
naslov = "Zadnje aktivnosti",
|
||||
prikaziNaslov = true,
|
||||
} = Astro.props;
|
||||
|
||||
// 1. Dohvat filtera iz URL-a (pretvaramo u mala slova radi sigurnosti)
|
||||
const statusFilter = Astro.url.searchParams.get('status')?.toLowerCase();
|
||||
|
||||
// 2. Dohvat podataka s API-ja
|
||||
const { nalozi = [] } = await fetchDashboardData();
|
||||
|
||||
// 3. Logika filtriranja (Serverska strana - tolerantna na velika/mala slova)
|
||||
let filtriraniNalozi = nalozi;
|
||||
|
||||
if (statusFilter) {
|
||||
filtriraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === statusFilter);
|
||||
}
|
||||
|
||||
// 4. POPRAVLJENO: Izračun brojeva uz obvezno pretvaranje u mala slova (.toLowerCase())
|
||||
// Na ovaj način 'PLANIRANO' s backenda sigurno postaje 'planirano' u JS-u
|
||||
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
||||
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
||||
|
||||
// 5. Primjena limita na prikaz (npr. top 5 nalozi)
|
||||
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
|
||||
|
||||
// Dinamički naslov ovisno o filteru
|
||||
const prikazaniNaslov = statusFilter
|
||||
? `Filtrirano: ${formatStatus(statusFilter)}`
|
||||
: naslov;
|
||||
---
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-6 group" id="radni-nalozi-sekcija" data-state="loading">
|
||||
{prikaziNaslov && (
|
||||
<NaslovList
|
||||
naslov={prikazaniNaslov}
|
||||
ukupno={filtriraniNalozi.length}
|
||||
label1="Planirano"
|
||||
count1={planiranoCount} // Sada će ispravno prikazati stvarni broj
|
||||
filter1="planirano"
|
||||
label2="U radu"
|
||||
count2={uRaduCount} // Sada će ispravno prikazati stvarni broj
|
||||
filter2="u_radu"
|
||||
naslov={naslov}
|
||||
sinkronizirano={false} label1="Planirano" count1={0} filter1="planirano"
|
||||
label2="U radu" count2={0} filter2="u_radu"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 shadow-2xl shadow-blue-500/5 overflow-hidden">
|
||||
<div id="nalozi-list">
|
||||
{prikazaniNalozi.length > 0 ? (
|
||||
prikazaniNalozi.map((n) => (
|
||||
<GenericKarticaItem
|
||||
href={`/operativa/radni-nalozi/${n.id}`}
|
||||
status={n.status?.toLowerCase()} // Šaljemo ujednačeni lowercase u podkomponente
|
||||
statusBojaClass={getStatusColorClass(n.status?.toLowerCase())}
|
||||
ikona={n.status?.toLowerCase() === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice'}
|
||||
naslov={`#${n.broj_naloga}`}
|
||||
subNaslov={formatStatus(n.status?.toLowerCase())}
|
||||
metaTekst={`${n.klijent_naziv || 'Nepoznat klijent'} | ${n.vozilo_naziv || 'Bez vozila'} | ${n.stroj_naziv || 'Bez stroja'}`}
|
||||
statusPrikaz={formatStatus(n.status?.toLowerCase())}
|
||||
bojaTeme={n.boja_teme || 'blue'}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div class="p-20 text-center uppercase font-black opacity-20 italic">
|
||||
Nema zapisa za odabrani status
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
id="nalozi-loader"
|
||||
class="p-20 text-center text-xs font-black uppercase tracking-widest text-gray-400 animate-pulse group-data-[state=loaded]:hidden group-data-[state=error]:hidden"
|
||||
>
|
||||
<i class="fa-solid fa-circle-notch animate-spin mr-3 text-blue-600 text-base"></i> Sinkronizacija radnih naloga...
|
||||
</div>
|
||||
|
||||
<div id="nalozi-list" class="space-y-0">
|
||||
<div id="nalozi-placeholder" class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest group-data-[state=loaded]:hidden">
|
||||
Nema zapisa za odabrani status
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Ponovno inicijaliziramo klijentsku skriptu za klikove na kartice filtera
|
||||
import { initFilters } from "../scripts/filters.js";
|
||||
|
||||
// Prvo pokretanje
|
||||
initFilters();
|
||||
<span id="rn-bridge" data-limit={limit} data-naslov={naslov}></span>
|
||||
|
||||
// Podrška za Astro View Transitions
|
||||
document.addEventListener('astro:after-swap', initFilters);
|
||||
<script>
|
||||
import { fetchDashboardData } from "../lib/api";
|
||||
import { getStatusColorClass, formatStatus } from "../utils/ui";
|
||||
import { initFilters } from "../scripts/filters.js";
|
||||
|
||||
async function renderirajRadneNalogeKlijentski() {
|
||||
const container = document.getElementById('nalozi-list');
|
||||
const sekcija = document.getElementById('radni-nalozi-sekcija');
|
||||
const bridge = document.getElementById('rn-bridge');
|
||||
|
||||
if (!container || !sekcija || !bridge) return;
|
||||
|
||||
// 1. KLIJENTSKI OSIGURAČ: Provjera autorizacije prije API poziva preko ključa iz tvojeg api.js
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||||
|
||||
if (!token) {
|
||||
// Ako nema tokena, odmah zaključaj listu i baci sigurnosni Error izgled
|
||||
sekcija.setAttribute('data-state', 'error');
|
||||
container.innerHTML = `
|
||||
<div class="p-20 text-center flex flex-col items-center justify-center gap-3">
|
||||
<div class="w-12 h-12 bg-red-600/10 text-red-600 rounded-xl flex items-center justify-center text-lg mb-2">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<h4 class="text-xs font-black uppercase tracking-widest text-red-600">Pristup odbijen</h4>
|
||||
<p class="text-[10px] text-gray-400 font-bold uppercase tracking-wider italic">Niste autorizirani za pregled operativnih naloga flote.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = parseInt(bridge.getAttribute('data-limit') || '0');
|
||||
const originalNaslov = bridge.getAttribute('data-naslov') || 'Zadnje aktivnosti';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const statusFilter = urlParams.get('status')?.toLowerCase();
|
||||
|
||||
try {
|
||||
// Svaki put kad klijent pokrene funkciju, osiguravamo loading stanje
|
||||
sekcija.setAttribute('data-state', 'loading');
|
||||
|
||||
// 2. Dohvat podataka (Šalje fetch zahtjev s ispravnim Authorization zaglavljem)
|
||||
const data = await fetchDashboardData();
|
||||
const nalozi = data?.nalozi || [];
|
||||
|
||||
// 3. Proračun statistike na klijentskoj strani
|
||||
const planiranoCount = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
||||
const uRaduCount = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
||||
const zavrsenoCount = nalozi.filter(n => ['zavrsen', 'završeno', 'gotovo', 'zavrseno'].includes(n.status?.toLowerCase())).length;
|
||||
|
||||
// 4. 🛠️ POPRAVLJENO: Upisivanje vrijednosti i inteligentno sakrivanje samo ako je brojač točno 0
|
||||
const card1 = sekcija.querySelector('#card-filter1');
|
||||
if (card1) {
|
||||
const val = card1.querySelector('.count-value');
|
||||
if (val) val.textContent = planiranoCount.toString();
|
||||
|
||||
if (planiranoCount > 0) {
|
||||
card1.classList.remove('hidden');
|
||||
} else {
|
||||
card1.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
const card2 = sekcija.querySelector('#card-filter2');
|
||||
if (card2) {
|
||||
const val = card2.querySelector('.count-value');
|
||||
if (val) val.textContent = uRaduCount.toString();
|
||||
|
||||
if (uRaduCount > 0) {
|
||||
card2.classList.remove('hidden');
|
||||
} else {
|
||||
card2.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
const card3 = sekcija.querySelector('#card-filter3');
|
||||
if (card3) {
|
||||
const val = card3.querySelector('.count-value');
|
||||
if (val) val.textContent = zavrsenoCount.toString();
|
||||
|
||||
if (zavrsenoCount > 0) {
|
||||
card3.classList.remove('hidden');
|
||||
} else {
|
||||
card3.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Filtriranje i limitiranje niza prema klijentskom stanju URL-a
|
||||
let filtriraniNalozi = statusFilter ? nalozi.filter(n => n.status?.toLowerCase() === statusFilter) : nalozi;
|
||||
const prikazaniNalozi = limit > 0 ? filtriraniNalozi.slice(0, limit) : filtriraniNalozi;
|
||||
|
||||
// Ažuriranje teksta glavnog naslova ovisno o aktivnom filteru
|
||||
const glavniNaslov = sekcija.querySelector('h2');
|
||||
if (glavniNaslov) {
|
||||
glavniNaslov.innerText = statusFilter ? `Filtrirano: ${formatStatus(statusFilter)}` : originalNaslov;
|
||||
}
|
||||
|
||||
// 6. 🚀 SINKRONIZACIJA PROPSA: Otključavamo kontejner unutar NaslovList.astro prebacivanjem u 'loaded'
|
||||
const statCardsContainer = sekcija.querySelector('#stat-cards-container');
|
||||
if (statCardsContainer) {
|
||||
statCardsContainer.setAttribute('data-state', 'loaded');
|
||||
}
|
||||
|
||||
// Isključivanje općeg loading stanja s krovne sekcije
|
||||
sekcija.setAttribute('data-state', 'loaded');
|
||||
|
||||
// 7. Generiranje HTML kartica radnih naloga unutar tijela
|
||||
if (prikazaniNalozi.length === 0) {
|
||||
container.innerHTML = `<div class="p-20 text-center uppercase font-black opacity-20 italic text-xs tracking-widest">Nema zapisa za odabrani status</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = prikazaniNalozi.map((n) => {
|
||||
const statusMalo = n.status?.toLowerCase();
|
||||
const statusKlasa = getStatusColorClass(statusMalo);
|
||||
const ikona = statusMalo === 'u_radu' ? 'fa-screwdriver-wrench' : 'fa-file-invoice';
|
||||
|
||||
return `
|
||||
<a href="/operativa/radni-nalozi/${n.id}" class="flex flex-wrap items-center justify-between p-6 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700/50 last:border-none hover:bg-gray-50/50 dark:hover:bg-gray-900/20 transition-all gap-4 group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-2xl flex items-center justify-center text-lg shadow-sm group-hover:scale-105 transition-transform">
|
||||
<i class="fa-solid ${ikona}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">#${n.broj_naloga}</h4>
|
||||
<p class="text-[11px] text-gray-400 font-bold uppercase tracking-wider mt-0.5">${formatStatus(statusMalo)}</p>
|
||||
<p class="text-[10px] text-gray-400/70 font-medium mt-1">
|
||||
${n.klijent_naziv || 'Nepoznat klijent'} | ${n.vozilo_naziv || 'Bez vozila'} | ${n.stroj_naziv || 'Bez stroja'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[10px] font-black px-3 py-1.5 rounded-full border uppercase tracking-wider ${statusKlasa}">
|
||||
${formatStatus(statusMalo)}
|
||||
</span>
|
||||
<div class="w-9 h-9 flex items-center justify-center bg-gray-50 dark:bg-gray-700/50 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 rounded-xl border border-gray-100 dark:border-gray-600 shadow-sm transition-all">
|
||||
<i class="fa-solid fa-chevron-right text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Aktiviraj klijentske klikove na kartice filtera
|
||||
initFilters();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Kritična greška u klijentskom renderu radnih naloga:", err);
|
||||
sekcija.setAttribute('data-state', 'error');
|
||||
container.innerHTML = '<div class="p-10 text-center text-red-500 font-bold uppercase text-xs">Greška pri sinkronizaciji operativnih podataka.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Prvo pokretanje pri učitavanju stranice
|
||||
renderirajRadneNalogeKlijentski();
|
||||
|
||||
// Ponovno pokretanje prilikom SPA navigacije kroz aplikaciju
|
||||
document.addEventListener('astro:after-swap', renderirajRadneNalogeKlijentski);
|
||||
</script>
|
||||
242
001.FRONTEND/src/components/ServiserTerminal.astro
Normal file
242
001.FRONTEND/src/components/ServiserTerminal.astro
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
// src/components/ServiserTerminal.astro
|
||||
import Button from "./Button.astro";
|
||||
import Toast from "./Toast.astro";
|
||||
---
|
||||
|
||||
<div class="space-y-6 group" id="serviser-terminal-sekcija" data-state="loading">
|
||||
|
||||
<div class="group-data-[state=loading]:flex hidden items-center gap-3 text-xs font-black text-gray-400 uppercase tracking-widest animate-pulse bg-white dark:bg-gray-800 p-8 rounded-[2rem] border border-gray-100 dark:border-gray-700/50 shadow-md">
|
||||
<i class="fa-solid fa-circle-notch animate-spin text-blue-600 text-base"></i> Sinkronizacija radnog zadatka s baze...
|
||||
</div>
|
||||
|
||||
<div id="terminal-prazno-stanje" class="hidden p-20 text-center bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-[3rem] uppercase font-black opacity-30 italic text-xs tracking-widest shadow-2xl shadow-blue-500/5">
|
||||
Trenutno nemate dodijeljenih aktivnih radnih naloga na terenu.
|
||||
</div>
|
||||
|
||||
<div id="terminal-radni-prostor" class="hidden group-data-[state=loaded]:grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 p-8 shadow-xl shadow-blue-500/5">
|
||||
|
||||
<div class="flex justify-between items-start gap-4 border-b border-gray-100 dark:border-gray-700/50 pb-4 mb-6">
|
||||
<div>
|
||||
<h3 id="rn-broj" class="text-xl font-black uppercase tracking-tight text-gray-900 dark:text-white">#RN-UČITAVANJE</h3>
|
||||
<p id="rn-klijent" class="text-xs font-bold text-gray-400 mt-0.5 uppercase tracking-wider">Klijent: ...</p>
|
||||
</div>
|
||||
<span id="rn-status-znacka" class="text-[10px] font-black px-4 py-2 rounded-full border uppercase tracking-wider bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/20 dark:text-amber-400 dark:border-amber-800/50">
|
||||
U RADU
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 mb-1 italic">Prijavljeni kvar / Pogonski stroj flote</h4>
|
||||
<div id="rn-stroj-detalji" class="p-4 bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800 rounded-2xl text-xs font-bold text-gray-700 dark:text-gray-300 shadow-inner">
|
||||
Učitavanje podataka o stroju...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<Button id="btn-zatvori-rn" variant="none" class="w-full py-4 bg-emerald-600 hover:bg-emerald-700 text-white rounded-2xl font-black uppercase text-xs tracking-[0.2em] shadow-lg shadow-emerald-500/10 active:scale-95 transition-all">
|
||||
<i class="fa-solid fa-check-double mr-2"></i> Dovrši servis i aktiviraj stroj
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div id="putni-nalog-kartica" class="bg-white dark:bg-gray-800 rounded-[3rem] border border-gray-100 dark:border-gray-700 p-8 shadow-xl shadow-blue-500/5 flex flex-col justify-between min-h-[300px]">
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 bg-blue-600/10 text-blue-600 dark:text-blue-400 rounded-xl flex items-center justify-center text-md shadow-sm">
|
||||
<i class="fa-solid fa-truck-ramp-box"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 id="pn-broj" class="text-sm font-black uppercase tracking-tight text-gray-900 dark:text-white">PN-UČITAVANJE</h4>
|
||||
<p id="pn-vozilo" class="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Vozilo: ...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4 border-gray-100 dark:border-gray-700/50" />
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="zavrsna-km" class="block text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 ml-2 italic">Završna KM Vozila</label>
|
||||
<input
|
||||
type="number"
|
||||
id="zavrsna-km"
|
||||
class="w-full px-4 py-3.5 bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-blue-600 rounded-xl outline-none text-xs font-bold text-gray-900 dark:text-white shadow-inner"
|
||||
placeholder="Unesi stanje brojača (km)..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6">
|
||||
<Button id="btn-zatvori-pn" variant="danger" class="w-full py-3.5 rounded-2xl text-[10px] tracking-wider italic">
|
||||
<i class="fa-solid fa-flag-checkered mr-2"></i> Zaključaj putni list
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { fetchRadniNalozi, patchNalog } from "../lib/api";
|
||||
|
||||
const API_BASE = import.meta.env.PUBLIC_API_URL;
|
||||
let aktivniNalog: any = null;
|
||||
|
||||
async function inicijalizirajTerminalServisera() {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||||
const sekcija = document.getElementById('serviser-terminal-sekcija');
|
||||
const radniProstor = document.getElementById('terminal-radni-prostor');
|
||||
const praznoStanje = document.getElementById('terminal-prazno-stanje');
|
||||
|
||||
if (!token || !sekcija || !radniProstor || !praznoStanje) return;
|
||||
|
||||
try {
|
||||
sekcija.setAttribute('data-state', 'loading');
|
||||
|
||||
// 🛠️ OPTIMIZIRANO: Više ne dohvaćamo korisnika klijentski jer WelcomeHeader to rješava na serveru.
|
||||
// Izravno dohvaćamo radne naloge kroz postojeće REST rute
|
||||
const naloziData = await fetchRadniNalozi();
|
||||
|
||||
// Filtriramo naloge koji su u radu
|
||||
const mojiNalozi = naloziData.filter((n: any) => n.status?.toLowerCase() === 'u_radu');
|
||||
|
||||
if (mojiNalozi.length === 0) {
|
||||
sekcija.setAttribute('data-state', 'loaded');
|
||||
radniProstor.classList.add('hidden');
|
||||
praznoStanje.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
aktivniNalog = mojiNalozi[0];
|
||||
praznoStanje.classList.add('hidden');
|
||||
radniProstor.classList.remove('hidden');
|
||||
|
||||
// Injekcija podataka u DOM
|
||||
document.getElementById('rn-broj')!.textContent = `#${aktivniNalog.broj_naloga}`;
|
||||
document.getElementById('rn-klijent')!.textContent = `Klijent: ${aktivniNalog.klijent_naziv || 'Opći klijent'}`;
|
||||
document.getElementById('rn-stroj-detalji')!.innerHTML = `
|
||||
<div class="space-y-1">
|
||||
<p><span class="opacity-50 font-black">STROJ U KVARU:</span> ${aktivniNalog.stroj_naziv || 'Nije specificirano'}</p>
|
||||
<p><span class="opacity-50 font-black">DODIJELJENO VOZILO:</span> ${aktivniNalog.vozilo_naziv || 'Bez servisnog vozila'}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Provjera postojanja povezanog putnog naloga
|
||||
const putniKartica = document.getElementById('putni-nalog-kartica');
|
||||
if (putniKartica) {
|
||||
if (aktivniNalog.putni_nalog) {
|
||||
putniKartica.classList.remove('hidden');
|
||||
document.getElementById('pn-broj')!.textContent = `PN-${aktivniNalog.putni_nalog}`;
|
||||
document.getElementById('pn-vozilo')!.textContent = `Vozilo: ${aktivniNalog.vozilo_naziv || 'Servisni kombi'}`;
|
||||
} else {
|
||||
putniKartica.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Prebacivanje u loaded stanje -> Tailwind CSS automatski pali radni prostor
|
||||
sekcija.setAttribute('data-state', 'loaded');
|
||||
|
||||
} catch (err) {
|
||||
console.error("Krah servisnog terminala:", err);
|
||||
sekcija.setAttribute('data-state', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// --- AKCIJA A: ZATVARANJE PUTNOG NALOGA (PATCH na osnovnu stazu) ---
|
||||
document.getElementById('btn-zatvori-pn')?.addEventListener('click', async (e) => {
|
||||
const btn = e.currentTarget as HTMLButtonElement;
|
||||
const kmInput = document.getElementById('zavrsna-km') as HTMLInputElement;
|
||||
const km = kmInput?.value;
|
||||
|
||||
if (!km || parseInt(km, 10) <= 0) {
|
||||
window.showToast?.("Unesite ispravnu završnu kilometražu vozila.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Knjiženje kilometara...';
|
||||
|
||||
const url = `${API_BASE}${API_BASE.endsWith('/') ? '' : '/'}operativa/putni-nalozi/${aktivniNalog.putni_nalog}/`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
zavrsna_km: parseInt(km, 10),
|
||||
status: 'završen'
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.showToast?.("Putni nalog uspješno zaključen i arhiviran.", "success");
|
||||
document.getElementById('putni-nalog-kartica')?.classList.add('opacity-40');
|
||||
btn.classList.add('hidden');
|
||||
kmInput.disabled = true;
|
||||
} else {
|
||||
window.showToast?.("Greška pri zatvaranju putnog lista.", "error");
|
||||
}
|
||||
} catch {
|
||||
window.showToast?.("Mrežna greška u komunikaciji s DRF-om.", "error");
|
||||
} {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
|
||||
// --- AKCIJA B: ZATVARANJE RADNOG NALOGA + PROMJENA STATUSI STROJA ---
|
||||
document.getElementById('btn-zatvori-rn')?.addEventListener('click', async (e) => {
|
||||
const btn = e.currentTarget as HTMLButtonElement;
|
||||
|
||||
btn.disabled = true;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch animate-spin mr-2"></i> Zatvaranje naloga i ažuriranje flote...';
|
||||
|
||||
// 1. Koristimo postojeću metodu patchNalog za prebacivanje radnog naloga u 'zavrsen'
|
||||
const resNalog = await patchNalog(aktivniNalog.id, { status: 'zavrsen' });
|
||||
|
||||
if (resNalog) {
|
||||
// 2. REST LANČANA REAKCIJA: Šaljemo PATCH izravno na stazu stroja i prebacujemo ga u 'aktivno'
|
||||
if (aktivniNalog.stroj) {
|
||||
const urlStroj = `${API_BASE}${API_BASE.endsWith('/') ? '' : '/'}fleet/strojevi/${aktivniNalog.stroj}/`;
|
||||
await fetch(urlStroj, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
},
|
||||
body: JSON.stringify({ status: 'aktivno' }) // Stroj je popravljen i spreman za pogon
|
||||
});
|
||||
}
|
||||
|
||||
window.showToast?.("Radni nalog dovršen. Stroj je uspješno pušten u rad!", "success");
|
||||
|
||||
// Re-render cijelog terminala (prebaciće u prazno stanje jer više nema naloga 'u_radu')
|
||||
setTimeout(() => inicijalizirajTerminalServisera(), 1200);
|
||||
} else {
|
||||
window.showToast?.("Sinkronizacija s DRF poslužiteljem nije uspjela.", "error");
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
|
||||
// Re-inicijalizacija i stabilnost kroz View Transitions SPA navigaciju
|
||||
inicijalizirajTerminalServisera();
|
||||
document.addEventListener('astro:after-swap', inicijalizirajTerminalServisera);
|
||||
</script>
|
||||
@@ -54,7 +54,7 @@
|
||||
"icon": "fa-address-book",
|
||||
"welcomeHeaderDisplay": true,
|
||||
"welcomeHeaderTextH1": "Pregled",
|
||||
"welcomeHeaderTextH1dodatno": "klijenata",
|
||||
"welcomeHeaderTextH1dodatno": " klijenata",
|
||||
"welcomeHeaderPodnaslov": "Upravljanje bazom korisnika i partnera",
|
||||
"welcomeHeaderPovratniURL": "/"
|
||||
},
|
||||
@@ -64,7 +64,7 @@
|
||||
"icon": "fa-calendar-days",
|
||||
"welcomeHeaderDisplay": true,
|
||||
"welcomeHeaderTextH1": "Kalendar",
|
||||
"welcomeHeaderTextH1dodatno": "događaja",
|
||||
"welcomeHeaderTextH1dodatno": " događaja",
|
||||
"welcomeHeaderPodnaslov": "Pregled operacija u stvarnom vremenu",
|
||||
"welcomeHeaderPovratniURL": "/"
|
||||
},
|
||||
@@ -87,6 +87,16 @@
|
||||
"welcomeHeaderTextH1dodatno": " Radni nalog",
|
||||
"welcomeHeaderPodnaslov": "Otvaranje novog servisnog ili radnog naloga u sustavu",
|
||||
"welcomeHeaderPovratniURL": "/operativa/radni-nalozi"
|
||||
},
|
||||
{
|
||||
"name": "Evidencija Servisera",
|
||||
"url": "/operativa/serviseri",
|
||||
"icon": "fa-calendar-days",
|
||||
"welcomeHeaderDisplay": true,
|
||||
"welcomeHeaderTextH1": "Evidencija",
|
||||
"welcomeHeaderTextH1dodatno": " servisera",
|
||||
"welcomeHeaderPodnaslov": "Središnji pregled i administracija terenskih servisnih tehničara",
|
||||
"welcomeHeaderPovratniURL": "/"
|
||||
}
|
||||
],
|
||||
"api_endpoints": {
|
||||
|
||||
@@ -4,27 +4,38 @@
|
||||
const RAW_BASE = import.meta.env.PUBLIC_API_URL;
|
||||
const API_BASE = RAW_BASE.endsWith('/') ? RAW_BASE : `${RAW_BASE}/`;
|
||||
|
||||
|
||||
/**
|
||||
* POMOĆNA FUNKCIJA:端 Dohvaća token i postavlja Headere
|
||||
* POMOĆNA FUNKCIJA: Dohvaća token i postavlja Headere.
|
||||
* Podržava i klijentski localStorage i serverski (SSR) proslijeđeni token.
|
||||
* @param {Object|FormData} bodyData - Podaci koji se šalju u body-ju zahtjeva
|
||||
* @param {string|null} ssrToken - Izborni token proslijeđen sa serverske strane (Astro SSR)
|
||||
*/
|
||||
function getAuthHeaders(bodyData = {}) {
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||||
function getAuthHeaders(bodyData = {}, ssrToken = null) {
|
||||
const headers = {};
|
||||
|
||||
// 🚀 PAMETAN DOHVAT TOKENA: Ako je proslijeđen ssrToken (server), koristi njega.
|
||||
// Ako nije, a nalazimo se u pregledniku (klijent), povuci ga iz localStorage.
|
||||
let token = ssrToken;
|
||||
if (!token && typeof window !== 'undefined') {
|
||||
token = localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`; // Usklađeno sa Simple JWT standardom
|
||||
}
|
||||
|
||||
// Provjera je li bodyData FormData (za slike/naloge)
|
||||
// Provjera je li bodyData FormData (za slike/naloge s terena)
|
||||
const isFormData = bodyData instanceof FormData;
|
||||
|
||||
// Ako NIJE FormData, šaljemo JSON
|
||||
// Ako NIJE FormData, šaljemo standardni JSON content type
|
||||
if (!isFormData) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralizirana obrada odgovora s Toast podrškom
|
||||
*/
|
||||
@@ -69,9 +80,13 @@ async function handleResponse(res) {
|
||||
|
||||
// --- RUTE ---
|
||||
export const routes = {
|
||||
// Autentifikacija (Maknute početne kose crte!)
|
||||
// Autentifikacija
|
||||
login: () => 'token/',
|
||||
trenutniKorisnik: () => 'users/me/',
|
||||
|
||||
// Korisnici i operativni tehničari
|
||||
serviseriSvi: () => 'users/',
|
||||
serviserTerminal: (id) => `users/${id}/terminal/`,
|
||||
|
||||
// Radni nalozi
|
||||
radniNalozi: (params = {}) => {
|
||||
@@ -109,22 +124,72 @@ export const routes = {
|
||||
// --- API METODE ---
|
||||
|
||||
/**
|
||||
* Dohvat profila trenutno prijavljenog korisnika
|
||||
* Dohvat profila korisnika iz opće liste /api/users/
|
||||
* @param {string|null} ssrToken - Opcionalni token sa servera (Astro.cookies ili slično)
|
||||
*/
|
||||
export async function fetchCurrentUser() {
|
||||
export async function fetchCurrentUser(ssrToken = null) {
|
||||
if (typeof window === 'undefined' && !ssrToken) return null;
|
||||
|
||||
try {
|
||||
const url = `${API_BASE}${routes.trenutniKorisnik()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
headers: getAuthHeaders({}, ssrToken)
|
||||
});
|
||||
return await handleResponse(res);
|
||||
|
||||
const user = await handleResponse(res);
|
||||
if (user && user.detail) return null;
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error("fetchCurrentUser Failure:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 NOVO (DRY): Dohvaća sve korisnike i vraća isključivo filtrirane servisere s terena
|
||||
*/
|
||||
export async function fetchServiseri() {
|
||||
try {
|
||||
const url = `${API_BASE}${routes.serviseriSvi()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
const korisnici = await handleResponse(res);
|
||||
if (!korisnici) return [];
|
||||
|
||||
const lista = Array.isArray(korisnici) ? korisnici : (korisnici.results || []);
|
||||
|
||||
// Filtriranje na temelju polja 'uloga' iz tvog CustomUser modela
|
||||
return lista.filter(u => String(u.uloga).toUpperCase().trim() === 'SERVISER');
|
||||
} catch (e) {
|
||||
console.error("fetchServiseri Failure:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 NOVO (DRY): Dohvaća ugniježđene terminal podatke za specifičnog servisera (Nalozi, Putni, Vozila)
|
||||
* @param {number|string} id - ID servisera čiji se terminal učitava
|
||||
*/
|
||||
export async function fetchServiserTerminalData(id) {
|
||||
try {
|
||||
const url = `${API_BASE}${routes.serviserTerminal(id)}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
return await handleResponse(res);
|
||||
} catch (e) {
|
||||
console.error(`fetchServiserTerminalData za ID ${id} neuspješan:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dohvat i filtriranje radnih naloga (za tablice i liste u operativi)
|
||||
*/
|
||||
|
||||
@@ -8,19 +8,14 @@ import StatsGrid from "../components/StatsGrid.astro";
|
||||
import Kalendar from "../components/Kalendar/Kalendar.astro";
|
||||
|
||||
// API i Centralizirani Utils iz api.js
|
||||
import { fetchDashboardData, fetchCurrentUser } from "../lib/api";
|
||||
import { fetchDashboardData } from "../lib/api";
|
||||
|
||||
// 1. Dohvat podataka (paralelno radi brzine i performansi u homelabu)
|
||||
const [data, user] = await Promise.all([
|
||||
fetchDashboardData(),
|
||||
fetchCurrentUser()
|
||||
]);
|
||||
// 1. Dohvat podataka s backenda (Samo bazični dashboard podaci koji se mogu poslužiti asinkrono)
|
||||
const data = await fetchDashboardData();
|
||||
|
||||
const nalozi = data?.nalozi || [];
|
||||
const imeKorisnika = user?.first_name || "Kolega";
|
||||
|
||||
// 2. POPRAVLJENO: Kalkulacija statistike tolerantna na velika/mala slova (.toLowerCase())
|
||||
// Osiguravamo da se podaci iz baze (PLANIRANO, U_RADU) savršeno upare sa StatsGrid filterima
|
||||
// 2. Kalkulacija statistike tolerantna na velika/mala slova (.toLowerCase())
|
||||
const naloziURadu = nalozi.filter(n => n.status?.toLowerCase() === 'u_radu').length;
|
||||
const planiraniNalozi = nalozi.filter(n => n.status?.toLowerCase() === 'planirano').length;
|
||||
|
||||
@@ -33,7 +28,7 @@ const zavrseniNalozi = nalozi.filter(n => {
|
||||
<Layout title="Dashboard | ServisLog">
|
||||
<div class="w-full space-y-5">
|
||||
|
||||
<WelcomeHeader ime={imeKorisnika} />
|
||||
<WelcomeHeader naslov="Kontrolna ploča" />
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 sm:gap-6 px-2">
|
||||
<StatsGrid
|
||||
@@ -84,10 +79,19 @@ const zavrseniNalozi = nalozi.filter(n => {
|
||||
|
||||
<script>
|
||||
import { initFilters } from "../scripts/filters.js";
|
||||
import { provjeriPristupStranici } from "../utils/auth.js";
|
||||
|
||||
// Inicijalizacija filtera za StatsGrid klikove na klijentu
|
||||
initFilters();
|
||||
// 🚀 POKREĆEMO SIGURNOSNI KLIJENTSKI GUARD:
|
||||
// On odmah na klijentu provjerava is_serviser. Ako je ulogiran običan serviser,
|
||||
// prisilno ga i trenutno preusmjerava na njegov dinamički terminal /operativa/serviseri/[id]
|
||||
async function inicijalizirajStranicu() {
|
||||
await provjeriPristupStranici();
|
||||
initFilters();
|
||||
}
|
||||
|
||||
// Re-inicijalizacija kod navigacije (Astro View Transitions)
|
||||
document.addEventListener('astro:after-swap', initFilters);
|
||||
// Pokreni odmah pri učitavanju u preglednik
|
||||
inicijalizirajStranicu();
|
||||
|
||||
// Re-inicijalizacija kod klijentske navigacije (Astro View Transitions)
|
||||
document.addEventListener('astro:after-swap', inicijalizirajStranicu);
|
||||
</script>
|
||||
@@ -3,23 +3,31 @@
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import WelcomeHeader from "../components/WelcomeHeader.astro";
|
||||
import Kalendar from "../components/Kalendar/Kalendar.astro";
|
||||
|
||||
// API i Utils
|
||||
import { fetchCurrentUser } from "../lib/api";
|
||||
|
||||
// 1. Dohvaćamo samo korisnika za pozdrav u headeru
|
||||
const user = await fetchCurrentUser();
|
||||
const imeKorisnika = user?.first_name || "Kolega";
|
||||
---
|
||||
|
||||
<Layout title="Kalendar | ServisLog">
|
||||
<div class="w-full space-y-10">
|
||||
|
||||
<WelcomeHeader ime={imeKorisnika} />
|
||||
<WelcomeHeader naslov="Kalendar događaja" />
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 sm:gap-6 px-2">
|
||||
<Kalendar />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { provjeriPristupStranici } from "../utils/auth.js";
|
||||
|
||||
async function inicijalizirajStranicu() {
|
||||
// 🚀 SIGURNOSNI UKLOP: Guard provjerava sesiju na klijentu i štiti kalendar od neautoriziranih upada
|
||||
await provjeriPristupStranici();
|
||||
}
|
||||
|
||||
// Pokreni odmah pri učitavanju u preglednik
|
||||
inicijalizirajStranicu();
|
||||
|
||||
// Re-inicijalizacija kod klijentske navigacije kroz ERP (Astro View Transitions)
|
||||
document.addEventListener('astro:after-swap', inicijalizirajStranicu);
|
||||
</script>
|
||||
51
001.FRONTEND/src/pages/operativa/serviseri/[id].astro
Normal file
51
001.FRONTEND/src/pages/operativa/serviseri/[id].astro
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
// src/pages/operativa/serviseri/[id].astro
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
||||
import RadniNalogLista from "../../../components/RadniNalogLista.astro";
|
||||
import AkcijePanel from "../../../components/AkcijePanel.astro";
|
||||
|
||||
// 🚀 Hvatamo ID servisera iz same putanje URL-a
|
||||
const { id } = Astro.params;
|
||||
|
||||
// 🎯 POPRAVAK: Uklonjen serverski fetchCurrentUser i redirect jer se token nalazi na klijentu!
|
||||
---
|
||||
|
||||
<Layout title="Servisni Terminal | Operativa">
|
||||
<div class="w-full space-y-10">
|
||||
|
||||
<WelcomeHeader />
|
||||
|
||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
||||
|
||||
<div class="md:col-span-7 space-y-6">
|
||||
|
||||
{/* Univerzalna CSR lista koja sama povlači naloge preko api.js ovisno o ulogi */}
|
||||
<RadniNalogLista
|
||||
limit={10}
|
||||
naslov="Dodijeljeni zadaci servisera"
|
||||
prikaziNaslov={true}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<AkcijePanel tip="dashboard" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { initFilters } from "../../../scripts/filters.js";
|
||||
|
||||
// 🎯 POPRAVAK: Potpuno izbačen provjeriPristupStranici.
|
||||
// Ako korisnik nema pravo gledati ovaj profil, Django backend će presresti zahtjev unutar RadniNalogLista!
|
||||
const pokreniSveKlijentskeSkripte = () => {
|
||||
initFilters();
|
||||
};
|
||||
|
||||
pokreniSveKlijentskeSkripte();
|
||||
document.addEventListener('astro:after-swap', pokreniSveKlijentskeSkripte);
|
||||
</script>
|
||||
136
001.FRONTEND/src/pages/operativa/serviseri/index.astro
Normal file
136
001.FRONTEND/src/pages/operativa/serviseri/index.astro
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
// src/pages/operativa/serviseri/index.astro
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import WelcomeHeader from "../../../components/WelcomeHeader.astro";
|
||||
import RadniNalogLista from "../../../components/RadniNalogLista.astro";
|
||||
---
|
||||
|
||||
<Layout title="Evidencija Servisera | Operativa">
|
||||
<div class="w-full space-y-10">
|
||||
|
||||
<WelcomeHeader />
|
||||
|
||||
<div class="grid md:grid-cols-10 gap-8 items-start mb-10 px-2">
|
||||
|
||||
<div id="direktorij-sekcija" class="hidden md:col-span-10 space-y-6">
|
||||
<div class="bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 rounded-[2.5rem] p-6 sm:p-8 shadow-xl shadow-gray-100/40 dark:shadow-none">
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 pb-4 border-b border-gray-50 dark:border-gray-800">
|
||||
<div>
|
||||
<h2 class="text-xl font-black text-gray-900 dark:text-white uppercase tracking-tight leading-none italic">
|
||||
Registrirani <span class="text-blue-600">Serviseri</span>
|
||||
</h2>
|
||||
<p class="text-xs text-gray-400 mt-2 italic font-medium">Popis tehničara s aktivnim ovlastima za radne naloge</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 border border-blue-100/30 dark:border-blue-900/30 italic font-black uppercase tracking-widest text-[11px] px-4 py-2 rounded-2xl w-max">
|
||||
Ukupno: <span id="serviseri-counter">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="serviseri-loading" class="py-12 flex flex-col items-center justify-center gap-3 text-gray-400 dark:text-gray-500 italic text-sm">
|
||||
<i class="fa-solid fa-circle-notch animate-spin text-blue-600 text-2xl"></i>
|
||||
Dohvaćanje operativnih podataka s poslužitelja...
|
||||
</div>
|
||||
|
||||
<div id="serviseri-grid" class="hidden grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
</div>
|
||||
|
||||
<div id="serviseri-empty" class="hidden py-12 text-center text-gray-400 dark:text-gray-500 italic text-sm">
|
||||
<i class="fa-solid fa-user-slash text-2xl mb-2 text-gray-300 dark:text-gray-700 block"></i>
|
||||
Nema registriranih servisera u bazi podataka flote.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="moji-nalozi-sekcija" class="hidden md:col-span-10 space-y-6">
|
||||
<RadniNalogLista
|
||||
limit={10}
|
||||
prikaziNaslov={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { fetchCurrentUser, fetchServiseri } from "../../../lib/api";
|
||||
|
||||
async function ucitajStranicuServisera() {
|
||||
const direktorijSekcija = document.getElementById('direktorij-sekcija');
|
||||
const loadingEl = document.getElementById('serviseri-loading');
|
||||
const gridEl = document.getElementById('serviseri-grid');
|
||||
const emptyEl = document.getElementById('serviseri-empty');
|
||||
const counterEl = document.getElementById('serviseri-counter');
|
||||
const mojiNaloziSekcija = document.getElementById('moji-nalozi-sekcija');
|
||||
|
||||
try {
|
||||
const user = await fetchCurrentUser();
|
||||
const ulogaGore = user?.uloga ? String(user.uloga).toUpperCase().trim() : '';
|
||||
|
||||
// 1. Ako je običan serviser, prikaži sekciju s univerzalnom listom
|
||||
if (ulogaGore === 'SERVISER') {
|
||||
if (direktorijSekcija) direktorijSekcija.classList.add('hidden');
|
||||
if (mojiNaloziSekcija) mojiNaloziSekcija.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Ako je Admin / Voditelj, prikaži imenik tehničara
|
||||
if (direktorijSekcija) direktorijSekcija.classList.remove('hidden');
|
||||
if (mojiNaloziSekcija) mojiNaloziSekcija.classList.add('hidden');
|
||||
|
||||
const serviseri = await fetchServiseri();
|
||||
if (loadingEl) loadingEl.classList.add('hidden');
|
||||
|
||||
if (!serviseri || serviseri.length === 0) {
|
||||
if (emptyEl) emptyEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (counterEl) counterEl.textContent = serviseri.length.toString();
|
||||
|
||||
if (gridEl) {
|
||||
gridEl.innerHTML = serviseri.map((s: any) => {
|
||||
const inicijali = `${s.first_name?.[0] || ''}${s.last_name?.[0] || ''}`.toUpperCase() || 'ST';
|
||||
const imePrezime = `${s.first_name || ''} ${s.last_name || ''}`.trim() || s.email;
|
||||
|
||||
return `
|
||||
<div class="bg-gray-50/50 dark:bg-gray-800/30 border border-gray-100 dark:border-gray-800/60 rounded-[1.8rem] p-5 flex items-center justify-between gap-4 hover:border-blue-500/30 dark:hover:border-blue-500/20 hover:bg-white dark:hover:bg-gray-800/60 transition-all duration-300 group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-600/10 text-blue-600 dark:text-blue-400 font-black text-xs tracking-widest rounded-2xl flex items-center justify-center border border-blue-500/10 shadow-sm">
|
||||
${inicijali}
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h3 class="text-sm font-black text-gray-900 dark:text-white uppercase tracking-tight leading-none group-hover:text-blue-600 transition-colors italic">
|
||||
${imePrezime}
|
||||
</h3>
|
||||
<p class="text-[10px] font-medium text-gray-400 mt-2 lowercase select-all">${s.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/operativa/serviseri/${s.id}" class="w-10 h-10 flex items-center justify-center rounded-xl bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700/80 text-gray-400 group-hover:text-white group-hover:bg-blue-600 group-hover:border-blue-500 active:scale-90 shadow-sm transition-all cursor-pointer">
|
||||
<i class="fa-solid fa-chevron-right text-xs group-hover:translate-x-0.5 transition-transform"></i>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
gridEl.classList.remove('hidden');
|
||||
gridEl.classList.add('grid');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Greška pri učitavanju stranice:", err);
|
||||
if (loadingEl) loadingEl.classList.add('hidden');
|
||||
|
||||
// 🚀 TOAST OKIDAMO SAMO OVDJE NA GLAVNOJ STRANICI ZA IMENIK
|
||||
// @ts-ignore
|
||||
window.showToast?.("Sinkronizacija imenika flote neuspješna.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
ucitajStranicuServisera();
|
||||
document.addEventListener('astro:after-swap', ucitajStranicuServisera);
|
||||
</script>
|
||||
61
001.FRONTEND/src/utils/auth.js
Normal file
61
001.FRONTEND/src/utils/auth.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// src/utils/auth.js
|
||||
import { fetchCurrentUser } from "../lib/api";
|
||||
|
||||
/**
|
||||
* Pomoćna funkcija za dohvat sigurne rute (Single Source of Truth)
|
||||
*/
|
||||
export function dohvatiRutuPoUlozi(uloga, userId = '') {
|
||||
const ulogaGore = uloga ? String(uloga).toUpperCase().trim() : '';
|
||||
if (ulogaGore === 'SERVISER') {
|
||||
return userId ? `/operativa/serviseri/${userId}` : '/operativa/serviseri/';
|
||||
}
|
||||
return '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Jednostavan klijentski guard koji samo provjerava valjanost sesije i ovlasti
|
||||
* @param {string[]} dozvoljeneUloge - Niz uloga koje smiju vidjeti stranicu
|
||||
*/
|
||||
export async function provjeriPristupStranici(dozvoljeneUloge = []) {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
// 1. Ako uopće nema tokena, vrati ga na prijavu
|
||||
if (!token) {
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Dohvaćamo profil trenutnog korisnika s backenda
|
||||
const user = await fetchCurrentUser();
|
||||
|
||||
// 3. Ako je token nevažeći/istekao, počisti local pohranu i izbaci ga van
|
||||
if (!user || !user.uloga) {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const korisnikUlogaGore = String(user.uloga).toUpperCase().trim();
|
||||
|
||||
// 4. Provjera eksplicitnih ovlasti (Samo ako ih je stranica zatražila)
|
||||
if (dozvoljeneUloge.length > 0) {
|
||||
const dozvoljeneUlogeGore = dozvoljeneUloge.map(u => String(u).toUpperCase().trim());
|
||||
|
||||
// Ako korisnik nema ulogu koja je navedena u dozvoljenima za tu stranicu
|
||||
if (!dozvoljeneUlogeGore.includes(korisnikUlogaGore)) {
|
||||
// Saznaj kamo pripada s obzirom na ulogu i preusmjeri ga
|
||||
window.location.href = dohvatiRutuPoUlozi(korisnikUlogaGore, user.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Kritičan krah u provjeri pristupa:", err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user