Frontend Vuejs 3
This commit is contained in:
parent
a5bf51eb47
commit
d539eff25e
12
frontend/Dockerfile
Normal file
12
frontend/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sistema de Investigación de Huellas</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Aquí es donde Vue monta toda la interfaz del sistema -->
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
902
frontend/package-lock.json
generated
Normal file
902
frontend/package-lock.json
generated
Normal file
@ -0,0 +1,902 @@
|
||||
{
|
||||
"name": "sip-frontend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sip-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons-vue": "^1.11.3",
|
||||
"bootstrap-vue-next": "^0.16.6",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@floating-ui/vue": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.6",
|
||||
"@floating-ui/utils": "^0.2.11",
|
||||
"vue-demi": ">=0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.4",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/shared": "3.5.34",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/compiler-core": "3.5.34",
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/compiler-ssr": "3.5.34",
|
||||
"@vue/shared": "3.5.34",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.14",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.34",
|
||||
"@vue/runtime-core": "3.5.34",
|
||||
"@vue/shared": "3.5.34",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "10.11.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.16.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.8",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap-icons-vue": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons-vue/-/bootstrap-icons-vue-1.11.3.tgz",
|
||||
"integrity": "sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bootstrap-vue-next": {
|
||||
"version": "0.16.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/vue": "^1.0.6",
|
||||
"@vueuse/core": "^10.7.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap-vue-next"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.4.18"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.4",
|
||||
"@rollup/rollup-android-arm64": "4.60.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.4",
|
||||
"@rollup/rollup-darwin-x64": "4.60.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.4",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.4",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.4",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.34",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/compiler-sfc": "3.5.34",
|
||||
"@vue/runtime-dom": "3.5.34",
|
||||
"@vue/server-renderer": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.6.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "sip-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons-vue": "^1.11.3",
|
||||
"bootstrap-vue-next": "^0.16.6",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
18
frontend/src/App.vue
Normal file
18
frontend/src/App.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div id="app-container">
|
||||
<!-- El router-view actúa como el layout dinámico, renderizando la vista activa -->
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// No requiere lógica global por ahora
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Estilos globales básicos opcionales */
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
211
frontend/src/components/AdminLayout.vue
Normal file
211
frontend/src/components/AdminLayout.vue
Normal file
@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="dashboard-wrapper">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand d-flex align-items-center py-4 px-3" style="background-color: #111a22; border-bottom: 1px solid #243342;">
|
||||
<div class="fingerprint-icon-container me-3 d-flex align-items-center justify-content-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" fill="url(#brand-fingerprint-gradient)" class="bi bi-fingerprint icon-glow" viewBox="0 0 16 16">
|
||||
<defs>
|
||||
<linearGradient id="brand-fingerprint-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00f2fe" />
|
||||
<stop offset="100%" stop-color="#4facfe" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M8.06 6.5a.5.5 0 0 1 .5.5v.75a1.5 1.5 0 0 0 1.5 1.5h.5a.5.5 0 0 1 0 1h-.5A2.5 2.5 0 0 1 8.56 7.75V7a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M13.06 8a5.03 5.03 0 0 1-4.7 5c-.32 0-.64-.03-.95-.1a.5.5 0 1 1 .22-.98c.23.05.48.08.73.08A4.03 4.03 0 0 0 12.06 8a.5.5 0 0 1 1 0z"/>
|
||||
<path d="M11.06 8a3.02 3.02 0 0 1-2.7 3c-.56 0-1.1-.11-1.61-.33a.5.5 0 1 1 .39-.92c.38.16.79.25 1.22.25a2.02 2.02 0 0 0 1.7-2 .5.5 0 0 1 1 0z"/>
|
||||
<path d="M9.06 6a1.01 1.01 0 0 1 1 1v.5a.5.5 0 0 1-1 0V7a.01.01 0 0 0 0-.01.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.06 5.5A2.01 2.01 0 0 1 9.06 3.5c1.01 0 1.84.75 1.98 1.72a.5.5 0 1 1-.99.14A1.01 1.01 0 0 0 9.06 4.5a1.01 1.01 0 0 0-1 1V6a.5.5 0 0 1-1 0v-.5z"/>
|
||||
<path d="M5.06 6.5A4.01 4.01 0 0 1 9.06 2.5c1.96 0 3.59 1.42 3.93 3.31a.5.5 0 1 1-.98.18A3.01 3.01 0 0 0 9.06 3.5a3.01 3.01 0 0 0-3 3v1a.5.5 0 0 1-1 0v-.52-.48z"/>
|
||||
<path d="M3.06 7.5A6.01 6.01 0 0 1 9.06 1.5c2.89 0 5.33 2.05 5.88 4.83a.5.5 0 1 1-.98.2A5.01 5.01 0 0 0 9.06 2.5a5.01 5.01 0 0 0-5 5v2.5a.5.5 0 0 1-1 0v-.52-.48-1.5z"/>
|
||||
<path d="M1.06 8.5A8.01 8.01 0 0 1 9.06.5c3.84 0 7.11 2.7 7.84 6.43a.5.5 0 0 1-.98.2A7.01 7.01 0 0 0 9.06 1.5a7.01 7.01 0 0 0-7 7v4.5a.5.5 0 0 1-1 0v-.52-.48-2.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="brand-text-container">
|
||||
<h6 class="mb-0 text-white font-weight-bold tracking-normal" style="font-size: 13.5px; line-height: 1.3;">
|
||||
Sistema de Identificación Papiloscópica
|
||||
</h6>
|
||||
<span class="badge bg-dark-subtle text-info font-weight-bold mt-1" style="font-size: 10px; letter-spacing: 0.5px; padding: 3px 6px;">
|
||||
S.I.P
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-menu">
|
||||
<b-button to="/" variant="link" class="menu-item" active-class="active" exact>
|
||||
Panel Principal
|
||||
</b-button>
|
||||
|
||||
<b-button v-if="isAdmin" to="/admin/usuarios" variant="link" class="menu-item" active-class="active">
|
||||
Investigadores
|
||||
</b-button>
|
||||
|
||||
<b-button to="/huellas" variant="link" class="menu-item" active-class="active">
|
||||
Capturas de Huellas
|
||||
</b-button>
|
||||
|
||||
<b-button to="/informes" variant="link" class="menu-item" active-class="active">
|
||||
Informes y Métricas
|
||||
</b-button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info mb-2">
|
||||
<p class="mb-0 text-white text-truncate">{{ user.username }}</p>
|
||||
<b-badge variant="light" class="text-uppercase" style="font-size: 10px;">{{ user.role }}</b-badge>
|
||||
</div>
|
||||
<b-button variant="outline-danger" size="sm" class="w-100" @click="logout">
|
||||
Cerrar Sesión
|
||||
</b-button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-workspace">
|
||||
<header class="topbar shadow-sm">
|
||||
<div class="d-flex align-items-center">
|
||||
<h5 class="mb-0 text-secondary text-uppercase" style="font-size: 14px; letter-spacing: 1px;">
|
||||
Sistema de Identificación Papiloscópica
|
||||
</h5>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<b-badge variant="success" class="p-2">Laboratorio Activo</b-badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content-body">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const user = ref({ username: 'Investigador', role: 'inv' });
|
||||
const isAdmin = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const userData = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
user.value = userData;
|
||||
isAdmin.value = userData.role === 'admin';
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
router.push({ name: 'Login' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background-color: #f4f6f9;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background-color: #1a252f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #2c3e50;
|
||||
background-color: #141c22;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
padding: 15px 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 25px;
|
||||
color: #abc0d4;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
border-radius: 0;
|
||||
font-size: 15px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
color: #fff;
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 20px;
|
||||
background-color: #141c22;
|
||||
border-top: 1px solid #2c3e50;
|
||||
}
|
||||
|
||||
.main-workspace {
|
||||
margin-left: 260px;
|
||||
/* Desplaza el contenido para que no lo tape la barra fija */
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: 65px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* Estilos específicos para destacar el isotipo biométrico */
|
||||
.fingerprint-icon-container {
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
background-color: rgba(0, 242, 254, 0.06); /* Fondo sutil cian */
|
||||
border: 1px solid rgba(0, 242, 254, 0.15);
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0; /* Evita que el contenedor se deforme por textos largos */
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fingerprint-icon-container:hover {
|
||||
background-color: rgba(0, 242, 254, 0.1);
|
||||
border-color: rgba(0, 242, 254, 0.3);
|
||||
}
|
||||
|
||||
.icon-glow {
|
||||
/* Filtro de sombra para simular una pantalla de hardware bio-médico activa */
|
||||
filter: drop-shadow(0px 0px 6px rgba(0, 242, 254, 0.5));
|
||||
}
|
||||
|
||||
.brand-text-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
22
frontend/src/main.js
Normal file
22
frontend/src/main.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
// 1. Importar los estilos obligatorios de Bootstrap 5 y la librería
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
|
||||
|
||||
// 2. Importar el plugin global de componentes unificados
|
||||
import { createBootstrap } from 'bootstrap-vue-next';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
|
||||
// 3. Registrar globalmente todos los componentes (b-form, b-button, b-alert, etc.)
|
||||
app.use(createBootstrap({
|
||||
components: true, // Esto habilita el registro automático de todos los b-*
|
||||
directives: true // Habilita directivas como v-b-modal o v-b-tooltip
|
||||
}));
|
||||
|
||||
app.mount('#app');
|
||||
78
frontend/src/router/index.js
Normal file
78
frontend/src/router/index.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import Dashboard from '../views/Dashboard.vue';
|
||||
|
||||
// Módulo AUTH: Login y Register
|
||||
import Login from '../views/auth/Login.vue';
|
||||
import Register from '../views/auth/Register.vue'; // Si decidís usar el registro visual
|
||||
import Huellas from '../views/Huellas.vue';
|
||||
import Informes from '../views/Informes.vue';
|
||||
// Módulo USER: Gestión y CRUD
|
||||
import UsersCrud from '../views/user/UsersCrud.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: { guest: true }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: Register,
|
||||
meta: { guest: true } // Permite acceso solo a usuarios no autenticados
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/usuarios',
|
||||
name: 'UsersCrud',
|
||||
component: UsersCrud,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
// rutas registradas y protegidas por token
|
||||
{
|
||||
path: '/huellas',
|
||||
name: 'Huellas',
|
||||
component: Huellas,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/informes',
|
||||
name: 'Informes',
|
||||
component: Informes,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
// Guardia de Navegación Global (Middleware)
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = !!localStorage.getItem('token');
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const isAdmin = user.role === 'admin';
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
return next({ name: 'Login' });
|
||||
}
|
||||
|
||||
if (to.meta.guest && isAuthenticated) {
|
||||
return next({ name: 'Dashboard' });
|
||||
}
|
||||
|
||||
if (to.meta.requiresAdmin && !isAdmin) {
|
||||
return next({ name: 'Dashboard' });
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
37
frontend/src/services/api.js
Normal file
37
frontend/src/services/api.js
Normal file
@ -0,0 +1,37 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Captura la variable inyectada por docker-compose mediante Vite
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Interceptor: Inyecta el Token JWT "Bearer" automáticamente en cada petición (Estilo Laravel Sanctum)
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
}, (error) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// Interceptor: Manejo global de respuestas (Redirección si el token expira - Error 401)
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
46
frontend/src/views/Dashboard.vue
Normal file
46
frontend/src/views/Dashboard.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<b-row class="mb-4">
|
||||
<b-col>
|
||||
<h3 class="text-dark font-weight-bold">Panel de Control - Acceso de Investigador</h3>
|
||||
<p class="text-muted">Métricas globales y estado del banco de muestras analizadas.</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="mb-4">
|
||||
<b-col md="4">
|
||||
<b-card class="border-0 shadow-sm text-center py-3">
|
||||
<h2 class="text-primary font-weight-bold mb-0">30</h2>
|
||||
<span class="text-muted text-uppercase small font-weight-bold">Muestras Registradas</span>
|
||||
</b-card>
|
||||
</b-col>
|
||||
<b-col md="4">
|
||||
<b-card class="border-0 shadow-sm text-center py-3">
|
||||
<h2 class="text-success font-weight-bold mb-0">12</h2>
|
||||
<span class="text-muted text-uppercase small font-weight-bold">Informes Publicados</span>
|
||||
</b-card>
|
||||
</b-col>
|
||||
<b-col md="4">
|
||||
<b-card class="border-0 shadow-sm text-center py-3">
|
||||
<h2 class="text-warning font-weight-bold mb-0">0</h2>
|
||||
<span class="text-muted text-uppercase small font-weight-bold">Alertas del Sistema</span>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col md="8">
|
||||
<b-card title="Últimas Capturas en el Laboratorio" class="border-0 shadow-sm">
|
||||
<p class="text-muted small">Historial reciente de procesamiento biométrico.</p>
|
||||
<div class="p-5 text-center bg-light text-muted rounded border border-dashed">
|
||||
Próximamente: Lista en tiempo real de huellas cargadas por los investigadores.
|
||||
</div>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AdminLayout from '../components/AdminLayout.vue'; // Importamos el Layout estructurador
|
||||
</script>
|
||||
9
frontend/src/views/Huellas.vue
Normal file
9
frontend/src/views/Huellas.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<h3>Banco de Huellas Dactilares</h3>
|
||||
<p class="text-muted">Módulo de captura y almacenamiento biométrico.</p>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup>
|
||||
import AdminLayout from '../components/AdminLayout.vue';
|
||||
</script>
|
||||
9
frontend/src/views/Informes.vue
Normal file
9
frontend/src/views/Informes.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<h3>Informes de Investigación</h3>
|
||||
<p class="text-muted">Exportación de datos y reportes de métricas estadísticas.</p>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup>
|
||||
import AdminLayout from '../components/AdminLayout.vue';
|
||||
</script>
|
||||
141
frontend/src/views/auth/Login.vue
Normal file
141
frontend/src/views/auth/Login.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="login-bg d-flex justify-content-center align-items-center">
|
||||
<div class="login-overlay-card shadow-lg">
|
||||
<div class="card-header-accent text-center py-4">
|
||||
<h4 class="mb-0 text-white font-weight-bold">Panel de Control Biométrico</h4>
|
||||
<small class="text-light opacity-75">Autenticación de Investigadores</small>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<b-alert v-model="showError" variant="danger" dismissible class="mb-3" style="font-size: 14px;">
|
||||
{{ errorMessage }}
|
||||
</b-alert>
|
||||
|
||||
<b-form @submit.prevent="handleLogin">
|
||||
<b-form-group label="Identificador de Usuario" label-for="username" class="mb-3 font-weight-bold text-secondary">
|
||||
<b-input-group>
|
||||
<b-input-group-text class="input-group-icon-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill text-secondary" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
|
||||
</svg>
|
||||
</b-input-group-text>
|
||||
<b-form-input id="username" v-model="form.username" required placeholder="Ej: mrios_investigador" class="form-control-custom"></b-form-input>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Contraseña de Acceso" label-for="password" class="mb-4 font-weight-bold text-secondary">
|
||||
<b-input-group>
|
||||
<b-input-group-text class="input-group-icon-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock-fill text-secondary" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2"/>
|
||||
</svg>
|
||||
</b-input-group-text>
|
||||
<b-form-input id="password" type="password" v-model="form.password" required placeholder="••••••••••••" class="form-control-custom"></b-form-input>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
|
||||
<b-button type="submit" variant="info" class="w-100 py-2 text-white font-weight-bold tracking-wide shadow-sm" :disabled="loading">
|
||||
{{ loading ? 'Validando Credenciales...' : 'Iniciar Sesión' }}
|
||||
</b-button>
|
||||
</b-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import apiClient from '../../services/api';
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref({ username: '', password: '' });
|
||||
const loading = ref(false);
|
||||
const showError = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
showError.value = false;
|
||||
try {
|
||||
const response = await apiClient.post('/auth/login', form.value);
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(response.data.user));
|
||||
router.push({ name: 'Dashboard' });
|
||||
} catch (error) {
|
||||
showError.value = true;
|
||||
errorMessage.value = error.response?.data?.message || 'Error de comunicación con el laboratorio.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-bg {
|
||||
background: linear-gradient(135deg, #eef2f3 0%, #8e9eab 100%);
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* MARCA DE AGUA OPTIMIZADA: Relleno gris + Contornos negros puros definidos */
|
||||
.login-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 650px;
|
||||
height: 650px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* Agregamos propiedades stroke (contorno negro) y stroke-width al SVG embebido */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23b0b7bd' stroke='%23000000' stroke-width='0.35' stroke-linejoin='round'%3E%3Cpath d='M8.06 6.5a.5.5 0 0 1 .5.5v.75a1.5 1.5 0 0 0 1.5 1.5h.5a.5.5 0 0 1 0 1h-.5A2.5 2.5 0 0 1 8.56 7.75V7a.5.5 0 0 1 .5-.5z'/%3E%3Cpath d='M13.06 8a5.03 5.03 0 0 1-4.7 5c-.32 0-.64-.03-.95-.1a.5.5 0 1 1 .22-.98c.23.05.48.08.73.08A4.03 4.03 0 0 0 12.06 8a.5.5 0 0 1 1 0z'/%3E%3Cpath d='M11.06 8a3.02 3.02 0 0 1-2.7 3c-.56 0-1.1-.11-1.61-.33a.5.5 0 1 1 .39-.92c.38.16.79.25 1.22.25a2.02 2.02 0 0 0 1.7-2 .5.5 0 0 1 1 0z'/%3E%3Cpath d='M9.06 6a1.01 1.01 0 0 1 1 1v.5a.5.5 0 0 1-1 0V7a.01.01 0 0 0 0-.01.5.5 0 0 1 .5-.5z'/%3E%3Cpath d='M7.06 5.5A2.01 2.01 0 0 1 9.06 3.5c1.01 0 1.84.75 1.98 1.72a.5.5 0 1 1-.99.14A1.01 1.01 0 0 0 9.06 4.5a1.01 1.01 0 0 0-1 1V6a.5.5 0 0 1-1 0v-.5z'/%3E%3Cpath d='M5.06 6.5A4.01 4.01 0 0 1 9.06 2.5c1.96 0 3.59 1.42 3.93 3.31a.5.5 0 1 1-.98.18A3.01 3.01 0 0 0 9.06 3.5a3.01 3.01 0 0 0-3 3v1a.5.5 0 0 1-1 0v-.52-.48z'/%3E%3Cpath d='M3.06 7.5A6.01 6.01 0 0 1 9.06 1.5c2.89 0 5.33 2.05 5.88 4.83a.5.5 0 1 1-.98.2A5.01 5.01 0 0 0 9.06 2.5a5.01 5.01 0 0 0-5 5v2.5a.5.5 0 0 1-1 0v-.52-.48-1.5z'/%3E%3Cpath d='M1.06 8.5A8.01 8.01 0 0 1 9.06.5c3.84 0 7.11 2.7 7.84 6.43a.5.5 0 0 1-.98.2A7.01 7.01 0 0 0 9.06 1.5a7.01 7.01 0 0 0-7 7v4.5a.5.5 0 0 1-1 0v-.52-.48-2.5z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
opacity: 0.12; /* Subimos levemente la opacidad para que resalte la combinación */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-overlay-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card-header-accent {
|
||||
background: linear-gradient(135deg, #1f4068 0%, #162447 100%);
|
||||
}
|
||||
|
||||
.input-group-icon-text {
|
||||
background-color: #f8f9fa;
|
||||
border-right: none;
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
padding-left: 14px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.form-control-custom {
|
||||
padding: 10px 14px;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-left: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
border-color: #ced4da;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input-group:focus-within .input-group-icon-text,
|
||||
.input-group:focus-within .form-control-custom {
|
||||
border-color: #1f4068;
|
||||
}
|
||||
</style>
|
||||
48
frontend/src/views/auth/Register.vue
Normal file
48
frontend/src/views/auth/Register.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<b-container class="vh-100 d-flex justify-content-center align-items-center">
|
||||
<b-card title="Registro de Investigador" style="max-width: 400px; width: 100%;" class="shadow">
|
||||
<b-alert v-model="alert.show" :variant="alert.variant" dismissible>
|
||||
{{ alert.message }}
|
||||
</b-alert>
|
||||
|
||||
<b-form @submit.prevent="handleRegister">
|
||||
<b-form-group label="Usuario:" class="mb-3">
|
||||
<b-form-input v-model="form.username" required placeholder="Ej: mrios"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Correo Institucional:" class="mb-3">
|
||||
<b-form-input type="email" v-model="form.email" required placeholder="ejemplo@uader.edu.ar"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Contraseña:" class="mb-4">
|
||||
<b-form-input type="password" v-model="form.password" required></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-button type="submit" variant="success" class="w-100 mb-2">Registrarse</b-button>
|
||||
<b-button to="/login" variant="link" class="w-100 size-sm">¿Ya tenés cuenta? Iniciá sesión</b-button>
|
||||
</b-form>
|
||||
</b-card>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import apiClient from '../../services/api';
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref({ username: '', email: '', password: '' });
|
||||
const alert = ref({ show: false, variant: 'danger', message: '' });
|
||||
|
||||
const handleRegister = async () => {
|
||||
try {
|
||||
await apiClient.post('/auth/register', form.value);
|
||||
alert.value = { show: true, variant: 'success', message: 'Registro exitoso. Redirigiendo...' };
|
||||
setTimeout(() => {
|
||||
router.push({ name: 'Login' });
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
alert.value = { show: true, variant: 'danger', message: error.response?.data?.message || 'Error en el registro.' };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
193
frontend/src/views/user/UsersCrud.vue
Normal file
193
frontend/src/views/user/UsersCrud.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<b-container class="mt-4">
|
||||
<b-row class="mb-4 align-items-center">
|
||||
<b-col>
|
||||
<h2>Administración de Investigadores</h2>
|
||||
<p class="text-muted">Gestión de usuarios, roles y permisos del proyecto.</p>
|
||||
</b-col>
|
||||
<b-col class="text-end">
|
||||
<b-button variant="primary" @click="openCreateModal">
|
||||
+ Registrar Investigador
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Alertas de estado -->
|
||||
<b-alert v-model="alert.show" :variant="alert.variant" dismissible class="mb-3">
|
||||
{{ alert.message }}
|
||||
</b-alert>
|
||||
|
||||
<!-- Tabla de Usuarios (Estilo b-table reactiva) -->
|
||||
<b-card no-body class="shadow-sm">
|
||||
<b-table :items="users" :fields="fields" striped hover responsive class="mb-0">
|
||||
<!-- Celda personalizada para el Rol -->
|
||||
<template #cell(role)="data">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<b-badge :variant="data.item.role === 'admin' ? 'danger' : 'info'">
|
||||
{{ data.item.role || 'Sin Rol' }}
|
||||
</b-badge>
|
||||
<b-badge v-if="data.item.role === 'admin'" variant="warning" pill>
|
||||
No borrable
|
||||
</b-badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Celda de Acciones -->
|
||||
<template #cell(actions)="data">
|
||||
<b-button size="sm" variant="warning" class="me-2" @click="openEditModal(data.item)">
|
||||
Editar
|
||||
</b-button>
|
||||
<b-button
|
||||
size="sm"
|
||||
:variant="data.item.role === 'admin' ? 'secondary' : 'danger'"
|
||||
@click="deleteUser(data.item)">
|
||||
Dar de Baja
|
||||
</b-button>
|
||||
<b-badge v-if="data.item.role === 'admin'" variant="warning" class="ms-2">
|
||||
Admin no eliminable
|
||||
</b-badge>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card>
|
||||
|
||||
<!-- Modal informativo si no es posible borrar -->
|
||||
<b-modal
|
||||
id="cannot-delete-modal"
|
||||
v-model="cannotDeleteModal.show"
|
||||
title="Acción no permitida"
|
||||
ok-only
|
||||
ok-title="Cerrar">
|
||||
<p class="mb-0">{{ cannotDeleteModal.message }}</p>
|
||||
</b-modal>
|
||||
|
||||
<!-- Modal para Crear/Editar Usuario -->
|
||||
<b-modal id="user-modal" v-model="modal.show" :title="modal.isEdit ? 'Editar Usuario' : 'Nuevo Investigador'" @ok="handleModalOk">
|
||||
<b-form @submit.prevent>
|
||||
<b-form-group label="Nombre de Usuario:" class="mb-3">
|
||||
<b-form-input v-model="userForm.username" required placeholder="Ej: mrios"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Correo Electrónico:" class="mb-3">
|
||||
<b-form-input type="email" v-model="userForm.email" required placeholder="investigador@uader.edu.ar"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="modal.isEdit ? 'Contraseña (Dejar en blanco para no modificar):' : 'Contraseña:'" class="mb-3">
|
||||
<b-form-input type="password" v-model="userForm.password"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Asignar Rol de Investigación:" class="mb-3">
|
||||
<b-form-select v-model="userForm.role_id" :options="roleOptions" required></b-form-select>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
</b-modal>
|
||||
</b-container>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import apiClient from '../../services/api';
|
||||
import AdminLayout from '../../components/AdminLayout.vue'; // Importamos el Layout estructurador
|
||||
|
||||
|
||||
// Configuración de columnas para la tabla de BootstrapVue
|
||||
const fields = [
|
||||
{ key: 'id', label: 'ID', sortable: true },
|
||||
{ key: 'username', label: 'Usuario', sortable: true },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'role', label: 'Rol / Nivel' },
|
||||
{ key: 'actions', label: 'Acciones' }
|
||||
];
|
||||
|
||||
const users = ref([]);
|
||||
const roles = ref([]);
|
||||
const roleOptions = ref([]);
|
||||
|
||||
const alert = ref({ show: false, variant: 'success', message: '' });
|
||||
const modal = ref({ show: false, isEdit: false, currentId: null });
|
||||
|
||||
const userForm = ref({ username: '', email: '', password: '', role_id: null });
|
||||
const cannotDeleteModal = ref({ show: false, message: '' });
|
||||
|
||||
// Cargar datos iniciales de la API al montar el componente
|
||||
const fetchUsersAndRoles = async () => {
|
||||
try {
|
||||
const [usersRes, rolesRes] = await Promise.all([
|
||||
apiClient.get('/users'),
|
||||
apiClient.get('/roles')
|
||||
]);
|
||||
users.value = usersRes.data;
|
||||
roles.value = rolesRes.data;
|
||||
|
||||
// Formatear opciones para el componente b-form-select
|
||||
roleOptions.value = rolesRes.data.map(r => ({ value: r.id, text: r.name.toUpperCase() }));
|
||||
} catch (error) {
|
||||
showAlert('danger', 'Error al sincronizar datos con el servidor.');
|
||||
}
|
||||
};
|
||||
|
||||
const showAlert = (variant, message) => {
|
||||
alert.value = { show: true, variant, message };
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
modal.value = { show: true, isEdit: false, currentId: null };
|
||||
userForm.value = { username: '', email: '', password: '', role_id: roleOptions.value[0]?.value || null };
|
||||
};
|
||||
|
||||
const openEditModal = (user) => {
|
||||
modal.value = { show: true, isEdit: true, currentId: user.id };
|
||||
|
||||
// Buscar ID del rol correspondiente basándose en el nombre
|
||||
const matchingRole = roles.value.find(r => r.name === user.role);
|
||||
|
||||
userForm.value = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: '',
|
||||
role_id: matchingRole ? matchingRole.id : null
|
||||
};
|
||||
};
|
||||
|
||||
const handleModalOk = async (bvModalEvent) => {
|
||||
bvModalEvent.preventDefault(); // Previene que el modal se cierre antes de validar la petición
|
||||
try {
|
||||
if (modal.value.isEdit) {
|
||||
await apiClient.put(`/users/${modal.value.currentId}`, userForm.value);
|
||||
showAlert('success', 'Investigador actualizado correctamente.');
|
||||
} else {
|
||||
await apiClient.post('/users', userForm.value);
|
||||
showAlert('success', 'Nuevo investigador registrado en el proyecto.');
|
||||
}
|
||||
modal.value.show = false;
|
||||
fetchUsersAndRoles();
|
||||
} catch (error) {
|
||||
showAlert('danger', error.response?.data?.message || 'Error al procesar la solicitud.');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUser = async (user) => {
|
||||
if (user.role === 'admin') {
|
||||
cannotDeleteModal.value = {
|
||||
show: true,
|
||||
message: 'No se puede eliminar a un usuario con rol administrador. El rol admin tiene permisos especiales y su cuenta debe mantenerse activa.'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('¿Está seguro de que desea revocar el acceso a este usuario dentro de la investigación?')) {
|
||||
try {
|
||||
await apiClient.delete(`/users/${user.id}`);
|
||||
showAlert('warning', 'Acceso revocado y usuario eliminado.');
|
||||
fetchUsersAndRoles();
|
||||
} catch (error) {
|
||||
showAlert('danger', error.response?.data?.message || 'No se pudo completar la eliminación.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsersAndRoles();
|
||||
});
|
||||
</script>
|
||||
14
frontend/vite.config.js
Normal file
14
frontend/vite.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// Debes asegurarte de incluir el 'export default'
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0', // Forzamos a Vite a escuchar en la red interna de Docker
|
||||
port: 8080, // El puerto mapeado en tu docker-compose
|
||||
watch: {
|
||||
usePolling: true // Crucial en Docker para que detecte cuando editás archivos desde Windows/Linux host
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user