Initial commit
This commit is contained in:
commit
c44fc36fd5
20
.env.example
Normal file
20
.env.example
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
VITE_APP_NAME=GOLSCONTROL
|
||||||
|
VITE_APP_URL=http://frontend.golscontrol.test
|
||||||
|
VITE_APP_LANG=es-MX
|
||||||
|
VITE_APP_PORT=3000
|
||||||
|
VITE_PAGINATION=25
|
||||||
|
|
||||||
|
VITE_APP_API=http://localhost:8080 #Colocar http://localhost:8080 con el puerto del backend / http://backend.golscontrol.test
|
||||||
|
VITE_APP_API_SECURE=false
|
||||||
|
|
||||||
|
VITE_MICROSERVICE_STOCK=http://localhost:3000/api
|
||||||
|
VITE_APP_NOTIFICATIONS=false
|
||||||
|
|
||||||
|
VITE_REVERB_APP_ID=
|
||||||
|
VITE_REVERB_APP_KEY=
|
||||||
|
VITE_REVERB_APP_SECRET=
|
||||||
|
VITE_REVERB_HOST="backend.golscontrol.test"
|
||||||
|
VITE_REVERB_PORT=8080
|
||||||
|
VITE_REVERB_SCHEME=http
|
||||||
|
|
||||||
|
APP_PORT=3001 # Puerto para la aplicación frontend
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
colors.css
|
||||||
|
notes.md
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
34
colors.css.example
Normal file
34
colors.css.example
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
@theme {
|
||||||
|
--color-page: #fff;
|
||||||
|
--color-page-t: #000;
|
||||||
|
--color-page-d: #292524;
|
||||||
|
--color-page-dt: #fff;
|
||||||
|
--color-primary: #374151;
|
||||||
|
--color-primary-t: #fff;
|
||||||
|
--color-primary-d: #1c1917;
|
||||||
|
--color-primary-dt: #fff;
|
||||||
|
--color-secondary: #3b82f6;
|
||||||
|
--color-secondary-t: #fff;
|
||||||
|
--color-secondary-d: #312e81;
|
||||||
|
--color-secondary-dt: #fff;
|
||||||
|
--color-primary-info: #06b6d4;
|
||||||
|
--color-primary-info-t: #fff;
|
||||||
|
--color-primary-info-d: #06b6d4;
|
||||||
|
--color-primary-info-dt: #fff;
|
||||||
|
--color-secondary-info: #06b6d4;
|
||||||
|
--color-secondary-info-t: #fff;
|
||||||
|
--color-secondary-info-d: #06b6d4;
|
||||||
|
--color-secondary-info-dt: #fff;
|
||||||
|
--color-success: #22c55e;
|
||||||
|
--color-success-t: #fff;
|
||||||
|
--color-success-d: #22c55e;
|
||||||
|
--color-success-dt: #fff;
|
||||||
|
--color-danger: #ef4444;
|
||||||
|
--color-danger-t: #fff;
|
||||||
|
--color-danger-d: #ef4444;
|
||||||
|
--color-danger-dt: #fecaca;
|
||||||
|
--color-warning: #eab308;
|
||||||
|
--color-warning-t: #fff;
|
||||||
|
--color-warning-d: #eab308;
|
||||||
|
--color-warning-dt: #fff;
|
||||||
|
}
|
||||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
holos-frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: dockerfile
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT}:5173"
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/holos.frontend
|
||||||
|
- /var/www/holos.frontend/node_modules
|
||||||
|
networks:
|
||||||
|
- holos-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
holos-network:
|
||||||
|
driver: bridge
|
||||||
17
dockerfile
Normal file
17
dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /var/www/holos.frontend
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
COPY install.sh /usr/local/bin/install.sh
|
||||||
|
RUN chmod +x /usr/local/bin/install.sh
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/install.sh"]
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
14
index.html
Normal file
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html id="main-page" lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Holos</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
13
install.sh
Executable file
13
install.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f colors.json ]; then
|
||||||
|
cp colors.json.example colors.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
|
|
||||||
|
echo "Done!"
|
||||||
3716
package-lock.json
generated
Normal file
3716
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "notsoweb.frontend",
|
||||||
|
"copyright": "Notsoweb Software Inc.",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.9.10",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.0.9",
|
||||||
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"axios": "^1.8.1",
|
||||||
|
"laravel-echo": "^2.0.2",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"pusher-js": "^8.4.0",
|
||||||
|
"tailwindcss": "^4.0",
|
||||||
|
"toastr": "^2.1.4",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^11.1.1",
|
||||||
|
"vue-multiselect": "^3.2.0",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"ziggy-js": "^2.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"vite-plugin-html": "^3.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
23
src/components/App.vue
Normal file
23
src/components/App.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import useLoader from '@Stores/Loader';
|
||||||
|
import { hasToken } from '@Services/Api';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
const loader = useLoader();
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
if(!hasToken()) {
|
||||||
|
return router.push({ name: 'auth.index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.boot()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
22
src/components/Holos/ActionSection.vue
Normal file
22
src/components/Holos/ActionSection.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
import SectionTitle from './SectionTitle.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="md:grid md:grid-cols-3 md:gap-6">
|
||||||
|
<SectionTitle>
|
||||||
|
<template #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<slot name="description" />
|
||||||
|
</template>
|
||||||
|
</SectionTitle>
|
||||||
|
|
||||||
|
<div class="mt-5 md:mt-0 md:col-span-2">
|
||||||
|
<div class="px-4 py-5 sm:p-6 shadow-sm dark:shadow-xs dark:shadow-white/50 sm:rounded-sm">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
18
src/components/Holos/Button/Danger.vue
Normal file
18
src/components/Holos/Button/Danger.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
default: 'submit',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="btn bg-danger"
|
||||||
|
:type="type"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
32
src/components/Holos/Button/Icon.vue
Normal file
32
src/components/Holos/Button/Icon.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue'
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
icon: String,
|
||||||
|
fill: Boolean,
|
||||||
|
style: {
|
||||||
|
type: String,
|
||||||
|
default: 'rounded'
|
||||||
|
},
|
||||||
|
title: String,
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'button'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="flex justify-center items-center h-7 w-7 rounded-sm btn-icon"
|
||||||
|
:title="title"
|
||||||
|
:type="type"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
:fill="fill"
|
||||||
|
:name="icon"
|
||||||
|
:style="style"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
18
src/components/Holos/Button/Primary.vue
Normal file
18
src/components/Holos/Button/Primary.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
default: 'submit',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
:type="type"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
18
src/components/Holos/Button/Secondary.vue
Normal file
18
src/components/Holos/Button/Secondary.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
default: 'submit',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
:type="type"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
34
src/components/Holos/Card/Indicator.vue
Normal file
34
src/components/Holos/Card/Indicator.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
icon: String,
|
||||||
|
title: String,
|
||||||
|
to: String,
|
||||||
|
value: Number
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterLink
|
||||||
|
class="relative flex-1 flex flex-col gap-2 p-4 rounded-sm -md bg-gray-200 dark:bg-transparent dark:border"
|
||||||
|
:to="to"
|
||||||
|
>
|
||||||
|
<label class="text-base font-semibold tracking-wider">
|
||||||
|
{{ title }}
|
||||||
|
</label>
|
||||||
|
<label class="text-primary dark:text-primary-dt text-4xl font-bold">
|
||||||
|
{{ value }}
|
||||||
|
</label>
|
||||||
|
<div class="absolute bg-primary dark:bg-primary-d rounded-md font-semibold text-xs text-gray-100 p-2 right-4 bottom-4">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-3xl md:text-2xl lg:text-3xl"
|
||||||
|
:name="icon"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
19
src/components/Holos/Card/Simple.vue
Normal file
19
src/components/Holos/Card/Simple.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup>
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
icon: String,
|
||||||
|
title: String
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col justify-center items-center p-4 bg-primary text-white rounded-md">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-4xl"
|
||||||
|
:name="icon"
|
||||||
|
/>
|
||||||
|
<h4>{{ title }}</h4>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
36
src/components/Holos/Checkbox.vue
Normal file
36
src/components/Holos/Checkbox.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:checked']);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
checked: {
|
||||||
|
type: [Array, Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const proxyChecked = computed({
|
||||||
|
get() {
|
||||||
|
return props.checked;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(val) {
|
||||||
|
emit('update:checked', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
v-model="proxyChecked"
|
||||||
|
type="checkbox"
|
||||||
|
:value="value"
|
||||||
|
class="rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
99
src/components/Holos/ConfirmsPassword.vue
Normal file
99
src/components/Holos/ConfirmsPassword.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
|
||||||
|
import Input from './Form/Input.vue';
|
||||||
|
import DialogModal from './DialogModal.vue';
|
||||||
|
import PrimaryButton from './Button/Primary.vue';
|
||||||
|
import SecondaryButton from './Button/Secondary.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirmed']);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: Lang('confirm'),
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: Lang('account.password.verify'),
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
type: String,
|
||||||
|
default: Lang('confirm'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmingPassword = ref(false);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordInput = ref(null);
|
||||||
|
|
||||||
|
const startConfirmingPassword = () => {
|
||||||
|
confirmingPassword.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmPassword = () => {
|
||||||
|
form.post(route('user.password-confirm'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
closeModal();
|
||||||
|
nextTick(() => emit('confirmed'));
|
||||||
|
},
|
||||||
|
onFail: () => {
|
||||||
|
passwordInput.value.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
confirmingPassword.value = false;
|
||||||
|
form.password = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<span @click="startConfirmingPassword">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<DialogModal :show="confirmingPassword" @close="closeModal">
|
||||||
|
<template #title>
|
||||||
|
{{ title }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
{{ form }}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<Input
|
||||||
|
v-model="form.password"
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
:onError="form.errors.password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<SecondaryButton @click="closeModal">
|
||||||
|
{{ $t('cancel') }}
|
||||||
|
</SecondaryButton>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
|
class="ms-3"
|
||||||
|
:class="{ 'opacity-25': form.processing }"
|
||||||
|
:disabled="form.processing"
|
||||||
|
@click="confirmPassword"
|
||||||
|
>
|
||||||
|
{{ button }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
45
src/components/Holos/DialogModal.vue
Normal file
45
src/components/Holos/DialogModal.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '2xl',
|
||||||
|
},
|
||||||
|
closeable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:show="show"
|
||||||
|
:max-width="maxWidth"
|
||||||
|
:closeable="closeable"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="text-lg px-4 font-medium">
|
||||||
|
<slot name="title" />
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-center p-2 text-end">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
104
src/components/Holos/Dropdown.vue
Normal file
104
src/components/Holos/Dropdown.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
align: {
|
||||||
|
default: 'right',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
contentClasses: {
|
||||||
|
default: () => [
|
||||||
|
'pt-1',
|
||||||
|
'bg-primary text-primary-t dark:bg-primary-d dark:text-primary-dt'
|
||||||
|
],
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: '48',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
|
const closeOnEscape = (e) => {
|
||||||
|
if (open.value && e.key === 'Escape') {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
|
||||||
|
|
||||||
|
const widthClass = computed(() => {
|
||||||
|
return {
|
||||||
|
'48': 'w-48',
|
||||||
|
'52': 'w-52',
|
||||||
|
'56': 'w-56',
|
||||||
|
'60': 'w-60',
|
||||||
|
'64': 'w-64',
|
||||||
|
'72': 'w-52 md:w-72',
|
||||||
|
}[props.width.toString()];
|
||||||
|
});
|
||||||
|
|
||||||
|
const alignmentClasses = computed(() => {
|
||||||
|
if (props.align === 'left') {
|
||||||
|
return 'origin-top-left left-0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.align === 'right') {
|
||||||
|
return 'origin-top-right right-0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.align === 'icon') {
|
||||||
|
const size = {
|
||||||
|
'48': '-right-20',
|
||||||
|
'52': '-right-22',
|
||||||
|
'56': '-right-24',
|
||||||
|
'60': '-right-26',
|
||||||
|
'64': '-right-28',
|
||||||
|
'72': '-right-36',
|
||||||
|
}[props.width.toString()];
|
||||||
|
|
||||||
|
return `origin-top-right ${size}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'origin-top';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div @click="open = ! open">
|
||||||
|
<slot name="trigger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Full Screen Dropdown Overlay -->
|
||||||
|
<div
|
||||||
|
v-show="open"
|
||||||
|
class=" fixed inset-0 z-40"
|
||||||
|
@click="open = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition ease-out duration-200"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="open"
|
||||||
|
class="absolute z-[1000] mt-2 rounded-t-md shadow-lg"
|
||||||
|
:class="[widthClass, alignmentClasses]"
|
||||||
|
style="display: none;"
|
||||||
|
@click="open = false"
|
||||||
|
>
|
||||||
|
<div class="rounded-sm ring-1 ring-black/5" :class="contentClasses">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
39
src/components/Holos/DropdownLink.vue
Normal file
39
src/components/Holos/DropdownLink.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
as: String,
|
||||||
|
to: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = 'block px-4 py-2 text-sm leading-5 hover:bg-secondary/80 dark:hover:bg-secondary-d/80 focus:outline-hidden focus:bg-gray-100 cursor-pointer transition';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="as == 'button'"
|
||||||
|
class="w-full text-left"
|
||||||
|
:class="style"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-else-if="as =='a'"
|
||||||
|
:href="href"
|
||||||
|
:class="style"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<RouterLink
|
||||||
|
v-else
|
||||||
|
:to="$view({ name: to })"
|
||||||
|
:class="style"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
44
src/components/Holos/Form/Checkbox.vue
Normal file
44
src/components/Holos/Form/Checkbox.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const uuid = uuidv4();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: String,
|
||||||
|
modelValue: Object | Boolean,
|
||||||
|
value: Object | Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vModel = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full h-8">
|
||||||
|
<input
|
||||||
|
class="appearance-none rounded-sm bg-primary cursor-pointer h-full w-full checked:bg-secondary dark:checked:bg-secondary-d transition-all duration-200 peer"
|
||||||
|
type="checkbox"
|
||||||
|
:id="uuid"
|
||||||
|
v-model="vModel"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="uuid"
|
||||||
|
class="absolute top-[50%] left-3 text-primary-t dark:text-primary-dt -translate-y-[50%] peer-checked:text-white dark:peer-checked:text-primary-dt transition-all duration-200 select-none"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
src/components/Holos/Form/Elements/Error.vue
Normal file
14
src/components/Holos/Form/Elements/Error.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
onError: String | Array
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p v-if="onError"
|
||||||
|
class="mt-1 pl-2 text-xs text-red-500 dark:text-red-300"
|
||||||
|
>
|
||||||
|
{{ Array.isArray(onError) ? onError[0] : onError }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
22
src/components/Holos/Form/Elements/Label.vue
Normal file
22
src/components/Holos/Form/Elements/Label.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
required: Boolean
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label v-if="title"
|
||||||
|
class="block text-sm font-medium text-page-t dark:text-page-dt"
|
||||||
|
:for="id"
|
||||||
|
>
|
||||||
|
{{ $t(title) }}
|
||||||
|
<span v-if="required"
|
||||||
|
class="text-danger dark:text-danger-d"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
107
src/components/Holos/Form/Image.vue
Normal file
107
src/components/Holos/Form/Image.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue'
|
||||||
|
import Label from './Elements/Label.vue';
|
||||||
|
import SecondaryButton from '../Button/Secondary.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'photoInput'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
accept: {
|
||||||
|
default: 'image/png, image/jpeg',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
class: String,
|
||||||
|
required: Boolean,
|
||||||
|
title: {
|
||||||
|
default: 'photo.title',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileType = ref(null);
|
||||||
|
const photoInput = ref(null);
|
||||||
|
const photoPreview = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const selectNewPhoto = () => {
|
||||||
|
photoInput.value.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePhotoPreview = () => {
|
||||||
|
const image_file = photoInput.value.files[0];
|
||||||
|
|
||||||
|
if (! image_file) return;
|
||||||
|
|
||||||
|
emit('photoInput', image_file);
|
||||||
|
|
||||||
|
fileType.value = image_file.type;
|
||||||
|
|
||||||
|
if(image_file.type == "application/pdf"){
|
||||||
|
photoPreview.value = image_file.name;
|
||||||
|
}else{
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
photoPreview.value = e.target.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(image_file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="col-span-6">
|
||||||
|
<input
|
||||||
|
ref="photoInput"
|
||||||
|
class="hidden"
|
||||||
|
type="file"
|
||||||
|
:accept="accept"
|
||||||
|
:required="required"
|
||||||
|
@change="updatePhotoPreview"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
id="image_file"
|
||||||
|
class="dark:text-gray-800"
|
||||||
|
:required="required"
|
||||||
|
:title="title"
|
||||||
|
/>
|
||||||
|
<div v-show="! photoPreview" class="mt-2">
|
||||||
|
<!-- si existe una imagen cargada, entonces se muestra en este slot -->
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div v-show="photoPreview" class="mt-2">
|
||||||
|
<div v-if="fileType == 'application/pdf'" class="flex overflow-hidden max-w-full">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-gray-400"
|
||||||
|
name="picture_as_pdf"
|
||||||
|
:title="$t('crud.edit')"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<div class="ml-2 font-bold text-gray-400 flex-1">
|
||||||
|
{{ photoPreview }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span
|
||||||
|
class="block rounded-lg h-40 bg-cover bg-no-repeat bg-center"
|
||||||
|
:class="class"
|
||||||
|
:style="'background-image: url(\'' + photoPreview + '\');'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<SecondaryButton
|
||||||
|
v-text="$t('photo.new')"
|
||||||
|
class="mt-2 mr-2"
|
||||||
|
type="button"
|
||||||
|
@click.prevent="selectNewPhoto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
88
src/components/Holos/Form/Input.vue
Normal file
88
src/components/Holos/Form/Input.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import Error from './Elements/Error.vue';
|
||||||
|
import Label from './Elements/Label.vue';
|
||||||
|
|
||||||
|
/** Opciones */
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
class: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
id: String,
|
||||||
|
modelValue: Number | String,
|
||||||
|
onError: String | Array,
|
||||||
|
placeholder: String,
|
||||||
|
required: Boolean,
|
||||||
|
title: String,
|
||||||
|
type: {
|
||||||
|
default: 'text',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = ref(null);
|
||||||
|
|
||||||
|
/** Propiedades calculadas */
|
||||||
|
const autoId = computed(() => {
|
||||||
|
return (props.id)
|
||||||
|
? props.id
|
||||||
|
: uuidv4()
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoTitle = computed(() => {
|
||||||
|
if(props.title) {
|
||||||
|
return props.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
focus: () => input.value.focus()
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
if (input.value.hasAttribute('autofocus')) {
|
||||||
|
input.value.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<Label
|
||||||
|
:id="autoId"
|
||||||
|
:required="required"
|
||||||
|
:title="autoTitle"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-bind="$attrs"
|
||||||
|
ref="input"
|
||||||
|
class="input-primary"
|
||||||
|
:class="{ 'cursor-not-allowed': disabled }"
|
||||||
|
:disabled="disabled"
|
||||||
|
:id="autoId"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:required="required"
|
||||||
|
:type="type"
|
||||||
|
:value="modelValue"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
>
|
||||||
|
<Error
|
||||||
|
:onError="onError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
88
src/components/Holos/Form/InputWithIcon.vue
Normal file
88
src/components/Holos/Form/InputWithIcon.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Error from './Elements/Error.vue';
|
||||||
|
|
||||||
|
/** Opciones */
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
class: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
id: String,
|
||||||
|
icon: String,
|
||||||
|
modelValue: Number | String,
|
||||||
|
onError: String | Array,
|
||||||
|
placeholder: String,
|
||||||
|
required: Boolean,
|
||||||
|
title: String,
|
||||||
|
type: {
|
||||||
|
default: 'text',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = ref(null);
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const autoId = computed(() => {
|
||||||
|
return (props.id)
|
||||||
|
? props.id
|
||||||
|
: uuidv4()
|
||||||
|
})
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
focus: () => input.value.focus()
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
if (input.value.hasAttribute('autofocus')) {
|
||||||
|
input.value.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex items-center border-2 py-2 px-3 rounded-sm">
|
||||||
|
<GoogleIcon
|
||||||
|
:name="icon"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="value"
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="pl-2 w-full outline-hidden border-none bg-transparent"
|
||||||
|
:class="{ 'cursor-not-allowed': disabled }"
|
||||||
|
:disabled="disabled"
|
||||||
|
:id="autoId"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:type="type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Error
|
||||||
|
:onError="onError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
89
src/components/Holos/Form/Selectable.vue
Normal file
89
src/components/Holos/Form/Selectable.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import VueMultiselect from 'vue-multiselect';
|
||||||
|
|
||||||
|
import Error from './Elements/Error.vue';
|
||||||
|
import Label from './Elements/Label.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'select',
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
customLabel: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
label: {
|
||||||
|
default: 'name',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
modelValue: String | Number,
|
||||||
|
multiple: Boolean,
|
||||||
|
onError: String | Array,
|
||||||
|
options: Object,
|
||||||
|
placeholder: {
|
||||||
|
default: 'Buscar ...',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
required: Boolean,
|
||||||
|
trackBy: {
|
||||||
|
default: 'id',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
title: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiselect = ref();
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
clean: () => multiselect.value.removeLastElement()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Label
|
||||||
|
:required="required"
|
||||||
|
:title="title"
|
||||||
|
/>
|
||||||
|
<VueMultiselect
|
||||||
|
ref="multiselect"
|
||||||
|
v-model="value"
|
||||||
|
deselectLabel="Remover"
|
||||||
|
selectedLabel="Seleccionado"
|
||||||
|
selectLabel="Seleccionar"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:close-on-select="true"
|
||||||
|
:custom-label="customLabel"
|
||||||
|
:disabled="disabled"
|
||||||
|
:label="label"
|
||||||
|
:multiple="multiple"
|
||||||
|
:options="options"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:preserve-search="true"
|
||||||
|
:required="required && !value"
|
||||||
|
:track-by="trackBy"
|
||||||
|
@select="(x, y) => emit('select', x, y)"
|
||||||
|
>
|
||||||
|
<template #noOptions>
|
||||||
|
{{ $t('noRecords') }}
|
||||||
|
</template>
|
||||||
|
</VueMultiselect>
|
||||||
|
<Error
|
||||||
|
:onError="onError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
103
src/components/Holos/Form/SingleFile.vue
Normal file
103
src/components/Holos/Form/SingleFile.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue'
|
||||||
|
import Label from './Elements/Label.vue';
|
||||||
|
import SecondaryButton from '../Button/Secondary.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
accept: {
|
||||||
|
default: 'image/png, image/jpeg',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
class: String,
|
||||||
|
modelValue:Object|String,
|
||||||
|
required: Boolean,
|
||||||
|
title: {
|
||||||
|
default: 'photo.title',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileType = ref(null);
|
||||||
|
const fileName = ref(null);
|
||||||
|
const photoInput = ref(null);
|
||||||
|
const photoPreview = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const selectNewPhoto = () => {
|
||||||
|
photoInput.value.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePhotoPreview = () => {
|
||||||
|
const image_file = photoInput.value.files[0];
|
||||||
|
|
||||||
|
if (! image_file) return;
|
||||||
|
|
||||||
|
emit('update:modelValue', image_file);
|
||||||
|
|
||||||
|
fileType.value = image_file.type;
|
||||||
|
fileName.value = image_file.name;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
photoPreview.value = e.target.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(image_file);
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="col-span-6">
|
||||||
|
<input
|
||||||
|
ref="photoInput"
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
:accept="accept"
|
||||||
|
:required="required"
|
||||||
|
@change="updatePhotoPreview"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
id="image_file"
|
||||||
|
:title="title"
|
||||||
|
:required="required"
|
||||||
|
/>
|
||||||
|
<div v-show="! photoPreview" class="mt-2">
|
||||||
|
<!-- si existe una imagen cargada, entonces se muestra en este slot -->
|
||||||
|
<slot name="previous"/>
|
||||||
|
</div>
|
||||||
|
<div v-show="photoPreview" class="mt-2">
|
||||||
|
<div class="flex overflow-hidden max-w-full">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-gray-400"
|
||||||
|
name="picture_as_pdf"
|
||||||
|
:title="$t('crud.edit')"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<div class="ml-2 font-bold text-gray-400 flex-1">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
:href="photoPreview"
|
||||||
|
>
|
||||||
|
{{ fileName }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SecondaryButton
|
||||||
|
v-text="$t('files.select')"
|
||||||
|
class="mt-2 mr-2"
|
||||||
|
type="button"
|
||||||
|
@click.prevent="selectNewPhoto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
67
src/components/Holos/Form/Switch.vue
Normal file
67
src/components/Holos/Form/Switch.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:checked'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
checked: {
|
||||||
|
default: false,
|
||||||
|
type: [
|
||||||
|
Array,
|
||||||
|
Boolean
|
||||||
|
]
|
||||||
|
},
|
||||||
|
disabled: Boolean,
|
||||||
|
title: {
|
||||||
|
default: Lang('active'),
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
default: null,
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const uuid = uuidv4()
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const proxyChecked = computed({
|
||||||
|
get() {
|
||||||
|
return props.checked;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emit('update:checked', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
||||||
|
<input
|
||||||
|
v-model="proxyChecked"
|
||||||
|
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
|
||||||
|
name="toggle"
|
||||||
|
type="checkbox"
|
||||||
|
:id="uuid"
|
||||||
|
:disabled="disabled"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
|
||||||
|
:for="uuid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
class="text-xs text-gray-700"
|
||||||
|
:for="uuid"
|
||||||
|
>
|
||||||
|
{{ $t(title) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
80
src/components/Holos/Form/Textarea.vue
Normal file
80
src/components/Holos/Form/Textarea.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import Error from './Elements/Error.vue';
|
||||||
|
import Label from './Elements/Label.vue';
|
||||||
|
|
||||||
|
/** Opciones */
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
class: String,
|
||||||
|
id: String,
|
||||||
|
modelValue: Number | String,
|
||||||
|
onError: String,
|
||||||
|
placeholder: String,
|
||||||
|
required: Boolean,
|
||||||
|
title: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = ref(null);
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
focus: () => input.value.focus()
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const autoId = computed(() => {
|
||||||
|
return (props.id)
|
||||||
|
? props.id
|
||||||
|
: uuidv4()
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoTitle = computed(() => {
|
||||||
|
if(props.title) {
|
||||||
|
return props.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
if (input.value.hasAttribute('autofocus')) {
|
||||||
|
input.value.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<Label
|
||||||
|
:id="autoId"
|
||||||
|
:required="required"
|
||||||
|
:title="autoTitle"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
ref="input"
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="input-primary"
|
||||||
|
:id="autoId"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:required="required"
|
||||||
|
:value="modelValue"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
></textarea>
|
||||||
|
<Error
|
||||||
|
:onError="onError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
166
src/components/Holos/Form/Todo/ItemWithForm.vue
Normal file
166
src/components/Holos/Form/Todo/ItemWithForm.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||||
|
import { lang } from '@Lang/i18n';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import PrimaryButton from '../../Button/Primary.vue';
|
||||||
|
import Input from '../../Form/Input.vue';
|
||||||
|
import Selectable from '../../Form/Selectable.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
itemATitle: String,
|
||||||
|
itemBTitle: String,
|
||||||
|
items: Object,
|
||||||
|
modelValue: Object,
|
||||||
|
title: String,
|
||||||
|
type: {
|
||||||
|
default: 'text',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Elementos primarios (controlador)
|
||||||
|
const itemA = ref()
|
||||||
|
const itemsASelected = ref([]);
|
||||||
|
const itemsAUnselected = ref([]);
|
||||||
|
|
||||||
|
// Elementos secundarios
|
||||||
|
const itemB = ref();
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const values = computed({
|
||||||
|
get() {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function add() {
|
||||||
|
if (itemA.value) {
|
||||||
|
if(itemB.value) {
|
||||||
|
values.value.push({
|
||||||
|
item: {
|
||||||
|
_id: itemA.value._id,
|
||||||
|
name: itemA.value.name,
|
||||||
|
},
|
||||||
|
value: itemB.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
let x = itemsAUnselected.value.filter((o) => {
|
||||||
|
return o._id != itemA.value._id
|
||||||
|
})
|
||||||
|
|
||||||
|
itemsAUnselected.value = x
|
||||||
|
itemsASelected.value.push({...itemA.value}),
|
||||||
|
|
||||||
|
itemA.value = null
|
||||||
|
itemB.value = null
|
||||||
|
} else {
|
||||||
|
Notify.warning(Lang('todo.uniqueSub.b.required', {name:Lang('subclassification')}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Notify.warning(Lang('todo.uniqueSub.a.required', {name:Lang('classification')}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(index, provider) {
|
||||||
|
itemsAUnselected.value.push({...provider})
|
||||||
|
itemsASelected.value.splice(itemsASelected.value.indexOf(provider), 1)
|
||||||
|
values.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
itemA,
|
||||||
|
itemB,
|
||||||
|
add,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Observadores */
|
||||||
|
watchEffect(() => {
|
||||||
|
if(props.items.length > 0) {
|
||||||
|
itemsAUnselected.value = props.items
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(itemA, () => {
|
||||||
|
emit('updateItemsB', itemA.value?.id)
|
||||||
|
|
||||||
|
if(!itemA.value) {
|
||||||
|
itemB.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
if(values.value) {
|
||||||
|
values.value.forEach((i) => {
|
||||||
|
itemsASelected.value.push({...i})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-sm border border-primary dark:border-primary-d p-2">
|
||||||
|
<p>{{ title }}</p>
|
||||||
|
<div class="w-full grid gap-2 grid-cols-2 dark:bg-primary-d/50 rounded-md">
|
||||||
|
<Selectable
|
||||||
|
v-model="itemA"
|
||||||
|
:title="itemATitle"
|
||||||
|
:options="itemsAUnselected"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="itemB"
|
||||||
|
:title="itemBTitle"
|
||||||
|
:type="type"
|
||||||
|
@keyup.enter="add"
|
||||||
|
/>
|
||||||
|
<div class="col-span-2 flex justify-center">
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
{{ $t('add') }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 text-sm">
|
||||||
|
<p><b>{{ $t('items') }}</b> ({{ values.length }})</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 space-y-2 ">
|
||||||
|
<template v-for="item, index in values">
|
||||||
|
<div class="relative rounded-sm border border-primary/50">
|
||||||
|
<div class="grid gap-2 grid-cols-2 w-full items-center p-2 dark:bg-primary-d/50">
|
||||||
|
<Input
|
||||||
|
v-model="item.item.name"
|
||||||
|
:title="itemATitle"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="item.value"
|
||||||
|
:title="itemBTitle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="absolute right-1 top-1">
|
||||||
|
<GoogleIcon
|
||||||
|
class="btn-icon-primary"
|
||||||
|
name="close"
|
||||||
|
@click="remove(index, item.item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
38
src/components/Holos/FormSection.vue
Normal file
38
src/components/Holos/FormSection.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, useSlots } from 'vue';
|
||||||
|
import SectionTitle from './SectionTitle.vue';
|
||||||
|
|
||||||
|
defineEmits(['submitted']);
|
||||||
|
|
||||||
|
const hasActions = computed(() => !! useSlots().actions);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="md:grid md:grid-cols-3 md:gap-6">
|
||||||
|
<SectionTitle>
|
||||||
|
<template #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<slot name="description" />
|
||||||
|
</template>
|
||||||
|
</SectionTitle>
|
||||||
|
|
||||||
|
<div class="mt-5 md:mt-0 md:col-span-2">
|
||||||
|
<form @submit.prevent="$emit('submitted')">
|
||||||
|
<div
|
||||||
|
class="p-4 sm:p-6 shadow-sm dark:shadow-xs dark:shadow-white/50"
|
||||||
|
:class="hasActions ? 'sm:rounded-tl-md sm:rounded-tr-md' : 'sm:rounded-md'"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-6 gap-6">
|
||||||
|
<slot name="form" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasActions" class="flex items-center justify-end px-4 py-3 text-end sm:px-6 shadow-sm dark:shadow-xs dark:shadow-white/50 sm:rounded-bl-md sm:rounded-br-md">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
172
src/components/Holos/Inbox.vue
Executable file
172
src/components/Holos/Inbox.vue
Executable file
@ -0,0 +1,172 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import InboxItem from './Inbox/Item.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
inboxCtl: Object,
|
||||||
|
items: Object,
|
||||||
|
searcherCtl: Object,
|
||||||
|
withMultiSelection: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const filterMessages = ref(false);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const selectThisPage = () => props.inboxCtl.onSelectAll(props.items, false);
|
||||||
|
const unselectThisPage = () => props.inboxCtl.onUnselectAll(props.items)
|
||||||
|
|
||||||
|
const search = url => props.searcherCtl.searchWithInboxPagination(url);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="inboxCtl.inboxNumberSelected.value.length"
|
||||||
|
class="w-full p-1 rounded-b-md bg-primary text-white"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
({{ inboxCtl.inboxNumberSelected.value.length }}) Seleccionados: {{ inboxCtl.inboxNumberSelected.value.join(", ") }}.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex overflow-x-auto">
|
||||||
|
<div class="w-fit">
|
||||||
|
<div v-if="$slots['main-menu']"
|
||||||
|
class="w-48 xl:w-64"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
name="main-menu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots['menu']"
|
||||||
|
class="w-48 xl:w-64 pr-2 pb-8 border-r border-gray-300"
|
||||||
|
:class="{'mt-16':!$slots['main-menu']}"
|
||||||
|
>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<slot
|
||||||
|
name="menu"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div v-if="$slots['actions']"
|
||||||
|
class="h-16 flex items-center justify-between py-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div v-if="withMultiSelection"
|
||||||
|
class="relative flex items-center px-0.5 space-x-0.5"
|
||||||
|
>
|
||||||
|
<button class="px-2 pt-1" @click="filterMessages = !filterMessages">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl"
|
||||||
|
name="checklist"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
@click.away="filterMessages = false"
|
||||||
|
class="bg-gray-200 shadow-2xl absolute left-0 top-6 w-32 py-2 text-gray-900 rounded-sm z-10"
|
||||||
|
:class="{'hidden':!filterMessages}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inbox-check-all-option"
|
||||||
|
@click="selectThisPage()"
|
||||||
|
>
|
||||||
|
Seleccionar toda esta página
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inbox-check-all-option"
|
||||||
|
@click="unselectThisPage()"
|
||||||
|
>
|
||||||
|
Deseleccionar toda esta página
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="items.links">
|
||||||
|
<div v-if="items.links.length > 3"
|
||||||
|
class="flex w-full justify-end"
|
||||||
|
>
|
||||||
|
<div class="flex w-full justify-end flex-wrap space-x-1 -mb-1">
|
||||||
|
<template v-for="(link, k) in items.links" :key="k">
|
||||||
|
<div v-if="link.url === null && k == 0"
|
||||||
|
class="px-2 py-1 text-sm leading-4 text-gray-400 border rounded"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_back"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button v-else-if="k === 0"
|
||||||
|
class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary text-white': link.active }"
|
||||||
|
@click="search(link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_back"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-else-if="link.url === null && k == (items.links.length - 1)"
|
||||||
|
class="px-2 py-1 text-sm leading-4 text-gray-400 border rounded"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_forward"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button v-else-if="k === (items.links.length - 1)"
|
||||||
|
class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary text-white': link.active }"
|
||||||
|
@click="search(link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_forward"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button v-else class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
v-html="link.label"
|
||||||
|
:class="{ 'bg-primary text-white': link.active }"
|
||||||
|
@click="search(link.url)"
|
||||||
|
></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else class="w-full mt-4"></div>
|
||||||
|
<div v-if="items.total > 0"
|
||||||
|
class="bg-gray-100 "
|
||||||
|
>
|
||||||
|
<ul class="ml-1">
|
||||||
|
<slot
|
||||||
|
name="head"
|
||||||
|
:items="items.data"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
<ul class="ml-1">
|
||||||
|
<slot
|
||||||
|
name="items"
|
||||||
|
:items="items.data"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<InboxItem>
|
||||||
|
<template #item>
|
||||||
|
<span class="w-28 pr-2 truncate">-</span>
|
||||||
|
<span class="w-96 truncate">Sin resultados</span>
|
||||||
|
</template>
|
||||||
|
<template #date>
|
||||||
|
-
|
||||||
|
</template>
|
||||||
|
</InboxItem>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
69
src/components/Holos/Inbox/Item.vue
Executable file
69
src/components/Holos/Inbox/Item.vue
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
inboxCtl: Object, //Controller
|
||||||
|
item: Object,
|
||||||
|
selecteds: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const check = ref(false);
|
||||||
|
const messageHover = ref(false);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const select = () => (!check.value)
|
||||||
|
? props.inboxCtl.onSelectOne(props.item)
|
||||||
|
: props.inboxCtl.onUnselectOne(props.item);
|
||||||
|
|
||||||
|
const selected = computed(() => {
|
||||||
|
const status = (props.item)
|
||||||
|
? props.inboxCtl.inboxIdSelected.value.includes(props.item.id)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
check.value = status;
|
||||||
|
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
class="flex items-center rounded-sm border-y px-2 min-h-[35px] transition duration-300"
|
||||||
|
:class="{'bg-secondary text-secondary-t':selected, 'bg-primary/50 text-primary-t hover:bg-secondary/50 hover:text-secondary-t':!selected}"
|
||||||
|
>
|
||||||
|
<div class="pr-2">
|
||||||
|
<input
|
||||||
|
v-model="check"
|
||||||
|
class="focus:ring-0 border-2 border-gray-400"
|
||||||
|
type="checkbox"
|
||||||
|
@click="select"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full flex items-center justify-between cursor-pointer"
|
||||||
|
@mouseover="messageHover = true"
|
||||||
|
@mouseleave="messageHover = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<slot name="item" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-36 flex items-center justify-end"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center space-x-2"
|
||||||
|
:class="{'hidden':!messageHover}"
|
||||||
|
>
|
||||||
|
<slot name="actions" :check="check" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex space-x-4 text-xs"
|
||||||
|
:class="{'hidden':messageHover}"
|
||||||
|
>
|
||||||
|
<slot name="date" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
11
src/components/Holos/Inbox/ItemTitle.vue
Executable file
11
src/components/Holos/Inbox/ItemTitle.vue
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<li
|
||||||
|
class="flex items-center rounded-sm border-y px-2 min-h-[35px] transition duration-300"
|
||||||
|
>
|
||||||
|
<div class="pl-5 w-full flex items-center justify-between cursor-pointer">
|
||||||
|
<div class="flex items-center font-semibold">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
51
src/components/Holos/Inbox/Menu/Item.vue
Executable file
51
src/components/Holos/Inbox/Menu/Item.vue
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
icon: String,
|
||||||
|
counter: Number,
|
||||||
|
to: String,
|
||||||
|
toParam: {
|
||||||
|
default: {},
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
title: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const classes = computed(() => {
|
||||||
|
let status = route().current(props.to, props.toParam)
|
||||||
|
? 'bg-secondary/30'
|
||||||
|
: 'border-transparent hover:bg-secondary/30';
|
||||||
|
|
||||||
|
return ` text-primary flex items-center justify-between py-1.5 px-4 rounded-sm cursor-pointer ${status} transition`
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<RouterLink
|
||||||
|
v-if="to"
|
||||||
|
:class="classes"
|
||||||
|
:to="to"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-lg"
|
||||||
|
:name="icon"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="counter > 0" class="bg-primary text-gray-100 font-bold px-2 py-0.5 text-xs rounded-sm">
|
||||||
|
{{ counter }}
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
42
src/components/Holos/Inbox/Menu/Static.vue
Executable file
42
src/components/Holos/Inbox/Menu/Static.vue
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
icon: String,
|
||||||
|
to: String,
|
||||||
|
title: String,
|
||||||
|
type: {
|
||||||
|
default: 'primary',
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const classes = computed(() => {
|
||||||
|
return `inbox-menu-button-${props.type}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-16 flex items-center pr-2">
|
||||||
|
<RouterLink
|
||||||
|
:class="classes"
|
||||||
|
:to="to"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2 ">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-lg text-white font-bold"
|
||||||
|
:name="icon"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
12
src/components/Holos/InputLabel.vue
Normal file
12
src/components/Holos/InputLabel.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
value: String,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label class="block font-medium text-sm text-gray-700">
|
||||||
|
<span v-if="value">{{ value }}</span>
|
||||||
|
<span v-else><slot /></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
58
src/components/Holos/Layout/App.vue
Normal file
58
src/components/Holos/Layout/App.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import useDarkMode from '@Stores/DarkMode'
|
||||||
|
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||||
|
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||||
|
import useNotifier from '@Stores/Notifier'
|
||||||
|
|
||||||
|
import Header from '../Skeleton/Header.vue';
|
||||||
|
import LeftSidebar from '../Skeleton/Sidebar/Left.vue';
|
||||||
|
import NotificationSidebar from '../Skeleton/Sidebar/Notification.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const darkMode = useDarkMode();
|
||||||
|
const leftSidebar = useLeftSidebar();
|
||||||
|
const notificationSidebar = useNotificationSidebar();
|
||||||
|
const notifier = useNotifier();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
title: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
leftSidebar.boot();
|
||||||
|
darkMode.boot();
|
||||||
|
notifier.boot();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-full h-screen bg-page text-page-t dark:bg-page-d dark:text-page-dt">
|
||||||
|
<LeftSidebar
|
||||||
|
@open="leftSidebar.toggle()"
|
||||||
|
>
|
||||||
|
<slot name="leftSidebar"/>
|
||||||
|
</LeftSidebar>
|
||||||
|
<NotificationSidebar
|
||||||
|
@open="notificationSidebar.toggle()"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-full transition-all duration-300"
|
||||||
|
:class="{'md:w-[calc(100vw-rem)] md:ml-64':leftSidebar.isOpened, 'md:w-screen md:ml-0':leftSidebar.isClosed}"
|
||||||
|
>
|
||||||
|
<div class="h-2 md:h-14">
|
||||||
|
<Header
|
||||||
|
@open="leftSidebar.toggle()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<main class="flex h-full justify-center md:p-2">
|
||||||
|
<div class="mt-14 md:mt-0 w-full shadow-lg dark:shadow-xs md:dark:shadow-white h-[calc(100vh-4.5rem)] px-2 pb-4 md:rounded-sm overflow-y-auto overflow-x-auto transition-colors duration-300">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
69
src/components/Holos/Layout/Auth.vue
Normal file
69
src/components/Holos/Layout/Auth.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
|
||||||
|
import useDarkMode from '@Stores/DarkMode'
|
||||||
|
|
||||||
|
import Logo from '@Holos/Logo.vue'
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const darkMode = useDarkMode()
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
title: String
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
darkMode.boot()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-screen flex bg-primary dark:bg-primary-d">
|
||||||
|
<div
|
||||||
|
class="relative flex w-full lg:w-full justify-around items-center with-transition"
|
||||||
|
:class="{'app-bg-light':darkMode.isLight,'app-bg-dark':darkMode.isDark}"
|
||||||
|
>
|
||||||
|
<header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white">
|
||||||
|
<div>
|
||||||
|
<IconButton v-if="darkMode.isLight"
|
||||||
|
icon="light_mode"
|
||||||
|
:title="$t('app.theme.light')"
|
||||||
|
@click="darkMode.applyDark()"
|
||||||
|
/>
|
||||||
|
<IconButton v-else
|
||||||
|
icon="dark_mode"
|
||||||
|
:title="$t('app.theme.dark')"
|
||||||
|
@click="darkMode.applyLight()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="flex w-full flex-col items-center justify-center space-y-2">
|
||||||
|
<div class="flex space-x-2 items-center justify-start text-white">
|
||||||
|
<Logo
|
||||||
|
class="text-lg inline-flex"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="bg-white/10 w-full backdrop-blur-xs text-white px-4 py-4 rounded-sm max-w-80">
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="absolute bottom-0 flex w-full h-8 px-4 items-center justify-between bg-primary dark:bg-primary-d backdrop-blur-md text-white transition-colors duration-global">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
©2024 {{ APP_COPYRIGHT }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
APP {{ APP_VERSION }} API {{ $page.app.version }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
69
src/components/Holos/Layout/TermsLayout.vue
Normal file
69
src/components/Holos/Layout/TermsLayout.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
|
||||||
|
import useDarkMode from '@Stores/DarkMode'
|
||||||
|
|
||||||
|
import IconButton from '../Button/Icon.vue'
|
||||||
|
import Logo from '../Logo.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const darkMode = useDarkMode()
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
title: String
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
darkMode.boot()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex">
|
||||||
|
<div
|
||||||
|
class="relative flex w-full lg:w-full justify-around items-start with-transition"
|
||||||
|
:class="{'app-bg-light':darkMode.isLight,'app-bg-dark':darkMode.isDark}"
|
||||||
|
>
|
||||||
|
<header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white">
|
||||||
|
<div>
|
||||||
|
<IconButton v-if="darkMode.isLight"
|
||||||
|
icon="light_mode"
|
||||||
|
:title="$t('app.theme.light')"
|
||||||
|
@click="darkMode.applyDark()"
|
||||||
|
/>
|
||||||
|
<IconButton v-else
|
||||||
|
icon="dark_mode"
|
||||||
|
:title="$t('app.theme.dark')"
|
||||||
|
@click="darkMode.applyLight()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="flex w-full flex-col space-y-2">
|
||||||
|
<div class="flex space-x-2 items-center justify-start text-white">
|
||||||
|
<Logo
|
||||||
|
class="text-lg inline-flex"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="bg-white/10 w-full mx-auto sm:max-w-2xl backdrop-blur-xs text-white px-4 py-8 rounded-md">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="absolute bottom-0 flex w-full h-8 px-4 items-center justify-between bg-primary dark:bg-primary-d backdrop-blur-xs text-white transition-colors duration-global">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
©{{ APP_COPYRIGHT }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
Versión {{ APP_VERSION }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
src/components/Holos/Logo.vue
Normal file
24
src/components/Holos/Logo.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import { hasToken } from '@Services/Api';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const home = () => {
|
||||||
|
if(hasToken()) {
|
||||||
|
router.push({ name: 'dashboard.index' });
|
||||||
|
} else {
|
||||||
|
location.replace('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex w-full justify-center items-center space-x-2 cursor-pointer"
|
||||||
|
@click="home"
|
||||||
|
>
|
||||||
|
<img :src="$page.app.logo" class="h-20" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
87
src/components/Holos/Modal.vue
Normal file
87
src/components/Holos/Modal.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
closeable: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
default: '2xl',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
default: false,
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const close = () => {
|
||||||
|
if (props.closeable) {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeOnEscape = (e) => {
|
||||||
|
if (e.key === 'Escape' && props.show) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxWidthClass = computed(() => {
|
||||||
|
return {
|
||||||
|
'sm': 'sm:max-w-sm',
|
||||||
|
'md': 'sm:max-w-md',
|
||||||
|
'lg': 'sm:max-w-lg',
|
||||||
|
'xl': 'sm:max-w-xl',
|
||||||
|
'2xl': 'sm:max-w-2xl',
|
||||||
|
}[props.maxWidth];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Observadores */
|
||||||
|
watch(() => props.show, () => {
|
||||||
|
if (props.show) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', closeOnEscape);
|
||||||
|
document.body.style.overflow = null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<transition
|
||||||
|
enter-active-class="ease-out duration-300"
|
||||||
|
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-active-class="ease-in duration-300"
|
||||||
|
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div v-show="show" class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 bg-primary/90 z-50 transition-all" scroll-region>
|
||||||
|
<div
|
||||||
|
v-show="show"
|
||||||
|
class="mb-6 bg-page text-page-t dark:bg-page-d dark:text-page-dt rounded-sm overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto"
|
||||||
|
:class="maxWidthClass"
|
||||||
|
>
|
||||||
|
<slot v-if="show" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
57
src/components/Holos/Modal/Destroy.vue
Normal file
57
src/components/Holos/Modal/Destroy.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<script setup>
|
||||||
|
import DangerButton from '../Button/Danger.vue';
|
||||||
|
import SecondaryButton from '../Button/Secondary.vue';
|
||||||
|
import DialogModal from '../DialogModal.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
defineEmits([
|
||||||
|
'close',
|
||||||
|
'destroy',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
title: {
|
||||||
|
default: Lang('delete.title'),
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show">
|
||||||
|
<template #title>
|
||||||
|
<p
|
||||||
|
class="font-bold text-xl"
|
||||||
|
v-text="title"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="w-full right-0">
|
||||||
|
<div class="overflow-hidden space-y-2 shadow-lg">
|
||||||
|
<slot />
|
||||||
|
<div class="px-4 pb-2">
|
||||||
|
<p
|
||||||
|
class="mt-2 p-1 rounded-md text-justify bg-danger text-danger-t"
|
||||||
|
v-text="$t('delete.confirm')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<slot name="buttons" />
|
||||||
|
<DangerButton
|
||||||
|
v-text="$t('delete.title')"
|
||||||
|
@click="$emit('destroy')"
|
||||||
|
/>
|
||||||
|
<SecondaryButton
|
||||||
|
v-text="$t('cancel')"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
51
src/components/Holos/Modal/Edit.vue
Normal file
51
src/components/Holos/Modal/Edit.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import PrimaryButton from '../Button/Primary.vue';
|
||||||
|
import SecondaryButton from '../Button/Secondary.vue';
|
||||||
|
import DialogModal from '../DialogModal.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'update'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
title: {
|
||||||
|
default: Lang('edit'),
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show">
|
||||||
|
<template #title>
|
||||||
|
<p
|
||||||
|
class="font-bold text-xl"
|
||||||
|
v-text="title"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="w-full right-0">
|
||||||
|
<div class="overflow-hidden shadow-lg">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<slot name="buttons" />
|
||||||
|
<PrimaryButton
|
||||||
|
v-text="$t('update')"
|
||||||
|
@click="$emit('update')"
|
||||||
|
/>
|
||||||
|
<SecondaryButton
|
||||||
|
v-text="$t('close')"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
21
src/components/Holos/Modal/Elements/Header.vue
Normal file
21
src/components/Holos/Modal/Elements/Header.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
subtitle: String,
|
||||||
|
title: String
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-center p-6 bg-primary dark:bg-primary-d">
|
||||||
|
<slot />
|
||||||
|
<p class="pt-2 text-lg font-bold text-primary-t dark:text-primary-t-d">
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
<p v-if="subtitle"
|
||||||
|
class="text-sm text-primary-t dark:text-primary-t-d"
|
||||||
|
>
|
||||||
|
{{ subtitle }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
49
src/components/Holos/Modal/Show.vue
Normal file
49
src/components/Holos/Modal/Show.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup>
|
||||||
|
import SecondaryButton from '../Button/Secondary.vue';
|
||||||
|
import PrimaryButton from '../Button/Primary.vue';
|
||||||
|
import DialogModal from '../DialogModal.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'edit'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
editable: Boolean,
|
||||||
|
show: Boolean,
|
||||||
|
title: String
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogModal :show="show">
|
||||||
|
<template #title>
|
||||||
|
<p
|
||||||
|
class="font-bold text-xl"
|
||||||
|
v-text="title ?? $t('details')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="w-full right-0">
|
||||||
|
<div class="overflow-hidden shadow-lg">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<slot name="buttons" />
|
||||||
|
<PrimaryButton v-if="editable"
|
||||||
|
v-text="$t('update')"
|
||||||
|
@click="$emit('edit')"
|
||||||
|
/>
|
||||||
|
<SecondaryButton
|
||||||
|
v-text="$t('close')"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</template>
|
||||||
53
src/components/Holos/Modal/Template/Destroy.vue
Normal file
53
src/components/Holos/Modal/Template/Destroy.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script setup>
|
||||||
|
import { api } from '@Services/Api.js';
|
||||||
|
|
||||||
|
import DestroyModal from '../Destroy.vue';
|
||||||
|
import Header from '../Elements/Header.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'update'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
model: Object,
|
||||||
|
show: Boolean,
|
||||||
|
to: Function,
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'name'
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
default: 'description'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const destroy = (id) => api.delete(props.to(id), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('deleted'));
|
||||||
|
emit('close');
|
||||||
|
emit('update');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Notify.info(Lang('notFound'));
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DestroyModal
|
||||||
|
:show="show"
|
||||||
|
@close="$emit('close')"
|
||||||
|
@destroy="destroy(model.id)"
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
:title="model[title]"
|
||||||
|
:subtitle="model[subtitle]"
|
||||||
|
/>
|
||||||
|
</DestroyModal>
|
||||||
|
</template>
|
||||||
32
src/components/Holos/PageHeader.vue
Normal file
32
src/components/Holos/PageHeader.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: String
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="title" class="flex w-full justify-center">
|
||||||
|
<h2
|
||||||
|
class="font-bold text-xl uppercase"
|
||||||
|
v-text="title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-end py-[0.31rem] mb-2 border-y-2 border-page-t dark:border-page-dt">
|
||||||
|
<div id="buttons" class="flex items-center space-x-1 text-sm">
|
||||||
|
<slot />
|
||||||
|
<RouterLink :to="$view({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
:title="$t('home')"
|
||||||
|
class="text-white"
|
||||||
|
icon="home"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
87
src/components/Holos/Paginable.vue
Normal file
87
src/components/Holos/Paginable.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<script setup>
|
||||||
|
import GoogleIcon from '../Shared/GoogleIcon.vue';
|
||||||
|
import Loader from '../Shared/Loader.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'send-pagination'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
items: Object,
|
||||||
|
processing: Boolean
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="pb-2">
|
||||||
|
<div class="w-full overflow-hidden rounded-sm shadow-lg">
|
||||||
|
<div v-if="!processing" class="w-full overflow-x-auto">
|
||||||
|
<template v-if="items?.total > 0">
|
||||||
|
<slot
|
||||||
|
name="body"
|
||||||
|
:items="items?.data"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="$slots.empty">
|
||||||
|
<slot name="empty" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex p-2 items-center justify-center">
|
||||||
|
<p class="text-center text-page-t dark:text-page-dt">{{ $t('noRecords') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center justify-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template v-if="items?.links">
|
||||||
|
<div v-if="items.links.length > 3" class="flex w-full justify-end">
|
||||||
|
<div class="flex w-full justify-end flex-wrap space-x-1 -mb-1">
|
||||||
|
<template v-for="(link, k) in items.links" :key="k">
|
||||||
|
<div v-if="link.url === null && k == 0"
|
||||||
|
class="px-2 py-1 text-sm leading-4 text-gray-400 border rounded"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_back"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button v-else-if="k === 0" class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary dark:bg-primary-dark text-white': link.active }"
|
||||||
|
@click="$emit('send-pagination', link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_back"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-else-if="link.url === null && k == (items.links.length - 1)"
|
||||||
|
class="px-2 py-1 text-sm leading-4 text-gray-400 border rounded"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_forward"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button v-else-if="k === (items.links.length - 1)" class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary dark:bg-primary-dark text-white': link.active }"
|
||||||
|
@click="$emit('send-pagination', link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_forward"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button v-else class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary dark:bg-primary-dark text-white': link.active }"
|
||||||
|
v-html="link.label"
|
||||||
|
@click="$emit('send-pagination', link.url)"
|
||||||
|
></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
82
src/components/Holos/Searcher.vue
Normal file
82
src/components/Holos/Searcher.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'search'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
title: String,
|
||||||
|
placeholder: {
|
||||||
|
default: Lang('search'),
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const query = ref('');
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const search = () => {
|
||||||
|
emit('search', query.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
query.value = '';
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="title" class="flex w-full justify-center">
|
||||||
|
<h2
|
||||||
|
class="font-bold text-xl uppercase"
|
||||||
|
v-text="title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-between items-center border-y-2 border-page-t dark:border-page-dt">
|
||||||
|
<div>
|
||||||
|
<div class="relative py-1 z-0">
|
||||||
|
<div @click="search" class="absolute inset-y-0 right-2 flex items-center pl-3 cursor-pointer">
|
||||||
|
<GoogleIcon
|
||||||
|
:title="$t('search')"
|
||||||
|
class="text-xl text-gray-700 hover:scale-110 hover:text-danger"
|
||||||
|
name="search"
|
||||||
|
/>
|
||||||
|
<GoogleIcon
|
||||||
|
v-show="query"
|
||||||
|
:title="$t('clear')"
|
||||||
|
class="text-xl text-gray-700 hover:scale-110 hover:text-danger"
|
||||||
|
name="close"
|
||||||
|
@click="clear"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="search"
|
||||||
|
class="bg-gray-100 border border-gray-300 text-gray-700 text-sm rounded-sm outline-0 focus:ring-primary focus:border-primary block sm:w-56 md:w-72 lg:w-80 pr-10 px-2.5 py-1"
|
||||||
|
autocomplete="off"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
v-model="query"
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1 text-sm" id="buttons">
|
||||||
|
<slot />
|
||||||
|
<RouterLink :to="$view({name:'index'})">
|
||||||
|
<IconButton
|
||||||
|
:title="$t('home')"
|
||||||
|
class="text-white"
|
||||||
|
icon="home"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
9
src/components/Holos/SectionBorder.vue
Normal file
9
src/components/Holos/SectionBorder.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hidden sm:block">
|
||||||
|
<div class="py-8">
|
||||||
|
<div
|
||||||
|
class="border-t border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
src/components/Holos/SectionTitle.vue
Normal file
17
src/components/Holos/SectionTitle.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="md:col-span-1 flex justify-between">
|
||||||
|
<div class="px-4 sm:px-0">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
|
<slot name="title" />
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="mt-1 text-sm text-gray-600 dark:text-white/50">
|
||||||
|
<slot name="description" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 sm:px-0">
|
||||||
|
<slot name="aside" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
123
src/components/Holos/Skeleton/Header.vue
Normal file
123
src/components/Holos/Skeleton/Header.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script setup>
|
||||||
|
import { users } from '@Plugins/AuthUsers'
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission'
|
||||||
|
import { logout } from '@Services/Page';
|
||||||
|
import useDarkMode from '@Stores/DarkMode'
|
||||||
|
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||||
|
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||||
|
import useNotifier from '@Stores/Notifier'
|
||||||
|
import useLoader from '@Stores/Loader';
|
||||||
|
|
||||||
|
import Loader from '@Shared/Loader.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Dropdown from '../Dropdown.vue';
|
||||||
|
import DropdownLink from '../DropdownLink.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'open'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const darkMode = useDarkMode()
|
||||||
|
const leftSidebar = useLeftSidebar()
|
||||||
|
const notificationSidebar = useNotificationSidebar()
|
||||||
|
const notifier = useNotifier()
|
||||||
|
const loader = useLoader()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="fixed px-2 w-[calc(100vw)] bg-transparent transition-all duration-300 z-50"
|
||||||
|
:class="{'md:w-[calc(100vw-16rem)]':leftSidebar.isOpened,'md:w-[calc(100vw)]':!leftSidebar.isClosed}"
|
||||||
|
>
|
||||||
|
<div class="my-2 flex px-2 items-center justify-between h-[2.75rem] rounded-sm bg-primary dark:bg-primary-d text-white z-20 ">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-2xl mt-1 z-50"
|
||||||
|
name="list"
|
||||||
|
:title="$t('menu')"
|
||||||
|
@click="emit('open')"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<div class="flex w-fit justify-end items-center h-14 header-right">
|
||||||
|
<ul class="flex items-center space-x-2">
|
||||||
|
<li v-if="loader.isProcessing" class="flex items-center">
|
||||||
|
<Loader />
|
||||||
|
</li>
|
||||||
|
<template v-if="notifier.isEnabled">
|
||||||
|
<li v-if="hasPermission('users.online')">
|
||||||
|
<RouterLink :to="{ name: 'admin.users.online' }" class="flex items-center">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl mt-1"
|
||||||
|
name="connect_without_contact"
|
||||||
|
:title="$t('notifications.title')"
|
||||||
|
/>
|
||||||
|
<span class="text-xs">{{ users.length - 1 }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li class="flex items-center">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl mt-1"
|
||||||
|
name="notifications"
|
||||||
|
:title="$t('notifications.title')"
|
||||||
|
@click="notificationSidebar.toggle()"
|
||||||
|
/>
|
||||||
|
<span class="text-xs">{{ notifier.counter }}</span>
|
||||||
|
</li>
|
||||||
|
<li v-if="darkMode.isDark">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl mt-1"
|
||||||
|
name="light_mode"
|
||||||
|
:title="$t('notifications.title')"
|
||||||
|
@click="darkMode.applyLight()"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li v-else>
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl mt-1"
|
||||||
|
name="dark_mode"
|
||||||
|
:title="$t('notifications.title')"
|
||||||
|
@click="darkMode.applyDark()"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="relative">
|
||||||
|
<Dropdown align="right" width="48">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<button
|
||||||
|
class="flex items-center space-x-4 text-sm border-2 border-transparent rounded-full focus:outline-hidden cursor-pointer transition"
|
||||||
|
:title="$t('users.menu')"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="h-8 w-8 rounded-sm object-cover"
|
||||||
|
:alt="$page.user.name"
|
||||||
|
:src="$page.user.profile_photo_url"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="text-center block px-4 py-2 text-sm border-b truncate">
|
||||||
|
{{ $page.user.name }}
|
||||||
|
</div>
|
||||||
|
<DropdownLink to="profile.show">
|
||||||
|
{{$t('profile')}}
|
||||||
|
</DropdownLink>
|
||||||
|
<div class="border-t border-gray-100" />
|
||||||
|
<form @submit.prevent="logout">
|
||||||
|
<DropdownLink as="button">
|
||||||
|
{{$t('auth.logout')}}
|
||||||
|
</DropdownLink>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
60
src/components/Holos/Skeleton/Sidebar/Left.vue
Normal file
60
src/components/Holos/Skeleton/Sidebar/Left.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<script setup>
|
||||||
|
import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
|
||||||
|
import useLeftSidebar from '@Stores/LeftSidebar'
|
||||||
|
|
||||||
|
import Logo from '@Holos/Logo.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const leftSidebar = useLeftSidebar()
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['open']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
sidebar: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const year = (new Date).getFullYear();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="fixed top-[3.1rem] md:top-0 w-fit h-[calc(100vh-3.1rem)] md:h-screen transition-all duration-300 z-50"
|
||||||
|
:class="{'-translate-x-0':leftSidebar.isOpened, '-translate-x-64':leftSidebar.isClosed}"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="flex md:w-64 h-full transition-all duration-300 border-none"
|
||||||
|
:class="{'w-64': leftSidebar.isClosed, 'w-screen': leftSidebar.isOpened}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full p-2 md:w-64">
|
||||||
|
<div class="flex h-full flex-col w-[15.5rem] justify-between rounded-sm overflow-y-auto overflow-x-hidden bg-primary dark:bg-primary-d text-white">
|
||||||
|
<div>
|
||||||
|
<div class="flex w-full px-2 mt-2">
|
||||||
|
<Logo
|
||||||
|
class="text-lg inline-flex"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul class="flex h-full flex-col md:pb-4 space-y-1">
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 px-5 space-y-1">
|
||||||
|
<p class="block text-center text-xs">
|
||||||
|
© {{year}} {{ APP_COPYRIGHT }}
|
||||||
|
</p>
|
||||||
|
<p class="text-center text-xs text-yellow-500 cursor-pointer">
|
||||||
|
<RouterLink :to="{name:'changelogs.app'}"> APP {{ APP_VERSION }} </RouterLink> <RouterLink :to="{name:'changelogs.core'}"> API {{ $page.app.version }} </RouterLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-full"
|
||||||
|
:class="{'w-[calc(100vw-17rem)] md:w-0 bg-transparent':leftSidebar.isOpened,'md:w-0':leftSidebar.isClosed}"
|
||||||
|
@click="leftSidebar.toggle()"
|
||||||
|
></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
58
src/components/Holos/Skeleton/Sidebar/Link.vue
Normal file
58
src/components/Holos/Skeleton/Sidebar/Link.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { RouterLink, useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import useLeftSidebar from '@Stores/LeftSidebar';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const leftSidebar = useLeftSidebar();
|
||||||
|
const vroute = useRoute();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
icon: String,
|
||||||
|
name: String,
|
||||||
|
to: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = computed(() => {
|
||||||
|
let status = props.to === vroute.name
|
||||||
|
? 'bg-secondary/30 dark:bg-secondary-d/30 border-secondary dark:border-secondary-d'
|
||||||
|
: 'border-transparent';
|
||||||
|
|
||||||
|
return `flex items-center h-11 focus:outline-hidden hover:bg-secondary/30 dark:hover:bg-secondary-d/30 border-l-4 hover:border-secondary dark:hover:border-secondary-d pr-6 ${status} transition`
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeSidebar = () => {
|
||||||
|
if(TwScreen.isDevice('phone') || TwScreen.isDevice('tablet')) {
|
||||||
|
leftSidebar.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li @click="closeSidebar()">
|
||||||
|
<RouterLink
|
||||||
|
:class="classes"
|
||||||
|
:to="$view({name:to})"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="icon"
|
||||||
|
class="inline-flex justify-center items-center ml-4 mr-2"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl"
|
||||||
|
:name="icon"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="name"
|
||||||
|
v-text="$t(name)"
|
||||||
|
class="text-sm tracking-wide truncate"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
93
src/components/Holos/Skeleton/Sidebar/Notification.vue
Normal file
93
src/components/Holos/Skeleton/Sidebar/Notification.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
import useNotificationSidebar from '@Stores/NotificationSidebar'
|
||||||
|
import useNotifier from '@Stores/Notifier'
|
||||||
|
|
||||||
|
import ModalController from '@Controllers/ModalController.js';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Item from './Notification/Item.vue';
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import ShowView from '@Holos/Skeleton/Sidebar/Notification/Show.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const notifier = useNotifier();
|
||||||
|
const notificationSidebar = useNotificationSidebar()
|
||||||
|
|
||||||
|
/** Controladores */
|
||||||
|
const Modal = new ModalController();
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['open']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
sidebar: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = ref(Modal.showModal);
|
||||||
|
const modelModal = ref(Modal.modelModal);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="fixed top-[3.1rem] right-[0.1rem] md:right-[0.5rem] w-fit h-[calc(100vh-3.2rem)] transition-all duration-300 z-50"
|
||||||
|
:class="{'translate-x-0':notificationSidebar.isOpened, 'translate-x-64':notificationSidebar.isClosed}"
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
id="notifications"
|
||||||
|
class="flex md:w-64 h-full transition-all duration-300 border-none"
|
||||||
|
:class="{'w-64': notificationSidebar.isClosed}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full p-2 md:w-64">
|
||||||
|
<div class="flex h-full flex-col w-[15.5rem] justify-between rounded-sm overflow-y-auto overflow-x-hidden bg-primary/70 text-primary-t dark:bg-primary-d/70 dark:text-primary-dt">
|
||||||
|
<div class="flex justify-between px-2 items-center">
|
||||||
|
<div class="py-1">
|
||||||
|
<h4 class="text-md font-semibold">
|
||||||
|
{{ $t('notifications.title') }} <span class="text-xs">({{ notifier.counter }})</span>
|
||||||
|
</h4>
|
||||||
|
<h4 class="text-xs font-semibold" v-if="notifier.unreadClosedCounter > 0">
|
||||||
|
{{ $t('notifications.unreadClosed') }} <span class="text-xs"> ({{ notifier.unreadClosedCounter }})</span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon
|
||||||
|
name="close"
|
||||||
|
class="text-primary-t dark:text-primary-dt cursor-pointer"
|
||||||
|
@click="notificationSidebar.close()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex h-full flex-col space-y-1">
|
||||||
|
<ul class="px-2 space-y-1 overflow-y-auto"
|
||||||
|
:class="{
|
||||||
|
'h-[calc(100vh-10rem)]': notifier.unreadClosedCounter > 0,
|
||||||
|
'h-[calc(100vh-9rem)]': notifier.unreadClosedCounter === 0
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Item v-for="notification in notifier.notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
:notification="notification"
|
||||||
|
@openModal="Modal.switchShowModal(notification)"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center pb-1">
|
||||||
|
<RouterLink :to="$view({ name: 'profile.notifications.index' })">
|
||||||
|
<PrimaryButton type="button" @click="notificationSidebar.close()">
|
||||||
|
{{ $t('notifications.seeAll') }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ShowView
|
||||||
|
:show="showModal"
|
||||||
|
:model="modelModal"
|
||||||
|
@close="Modal.switchShowModal"
|
||||||
|
@reload="notifier.getUpdates()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
79
src/components/Holos/Skeleton/Sidebar/Notification/Item.vue
Normal file
79
src/components/Holos/Skeleton/Sidebar/Notification/Item.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getDateTime } from '@Controllers/DateController';
|
||||||
|
import useNotifier from '@Stores/Notifier';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const notifier = useNotifier();
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'openModal'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
notification: Object,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="flex flex-col w-full items-center p-2 bg-primary dark:bg-primary-d text-white rounded-sm shadow-md">
|
||||||
|
<div class="flex w-full justify-between text-gray-400">
|
||||||
|
<div>
|
||||||
|
<h6 class="text-[10px]">{{ getDateTime(notification.created_at) }}</h6>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<GoogleIcon
|
||||||
|
name="close"
|
||||||
|
class="text-xs text-white cursor-pointer"
|
||||||
|
@click="notifier.closeNotification(notification.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full cursor-pointer">
|
||||||
|
<div class="w-10 space-y-0" @click="emit('openModal', notification)">
|
||||||
|
<template v-if="notification.user">
|
||||||
|
<div class="w-10 h-10 bg-transparent rounded-full flex items-center justify-center">
|
||||||
|
<img v-if="notification.user"
|
||||||
|
class="rounded-full object-cover"
|
||||||
|
:alt="notification.user.name"
|
||||||
|
:src="notification.user.profile_photo_url"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="w-10 h-10 bg-secondary dark:bg-secondary-d rounded-sm flex items-center justify-center">
|
||||||
|
<img v-if="notification.user"
|
||||||
|
class="rounded-full object-cover"
|
||||||
|
:alt="notification.user.name"
|
||||||
|
:src="notification.user.profile_photo_url"
|
||||||
|
>
|
||||||
|
<GoogleIcon v-else
|
||||||
|
name="tag"
|
||||||
|
class="text-white text-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 w-full">
|
||||||
|
<div
|
||||||
|
v-text="notification.data.title"
|
||||||
|
class="text-sm font-medium truncate"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-text="notification.data.description"
|
||||||
|
class="text-xs w-40 font-thin truncate"
|
||||||
|
/>
|
||||||
|
<div v-if="notification.user"
|
||||||
|
v-text="`~ ${notification.user.name} ${notification.user.paternal}`"
|
||||||
|
class="text-xs text-gray-400 truncate"
|
||||||
|
/>
|
||||||
|
<div v-else
|
||||||
|
v-text="$t('system.title')"
|
||||||
|
class="text-xs text-gray-400 truncate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
74
src/components/Holos/Skeleton/Sidebar/Notification/Show.vue
Normal file
74
src/components/Holos/Skeleton/Sidebar/Notification/Show.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onUpdated } from 'vue';
|
||||||
|
import { getDateTime } from '@Controllers/DateController';
|
||||||
|
import useNotifier from '@Stores/Notifier';
|
||||||
|
|
||||||
|
import Header from '@Holos/Modal/Elements/Header.vue';
|
||||||
|
import ShowModal from '@Holos/Modal/Show.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const notifier = useNotifier();
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'reload'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
model: Object
|
||||||
|
});
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
if(!props.model.read_at && props.show) {
|
||||||
|
notifier.readNotification(props.model.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!props.model.read_at && !props.show) {
|
||||||
|
emit('reload');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ShowModal
|
||||||
|
:show="show"
|
||||||
|
@close="$emit('close')"
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
:title="model.data.title"
|
||||||
|
>
|
||||||
|
</Header>
|
||||||
|
<div class="py-2 border-b">
|
||||||
|
<div class="flex w-full px-4 py-2">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl text-success"
|
||||||
|
name="contact_mail"
|
||||||
|
/>
|
||||||
|
<div class="pl-3">
|
||||||
|
<p class="font-bold text-lg leading-none pb-2">
|
||||||
|
{{ $t('details') }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<b>{{ $t('description') }}: </b>
|
||||||
|
{{ model.data.description }}
|
||||||
|
</div>
|
||||||
|
<div v-if="model.data.message" class="flex flex-col">
|
||||||
|
<b>{{ $t('message') }}: </b>
|
||||||
|
{{ model.data.message }}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('created_at') }}: </b>
|
||||||
|
{{ getDateTime(model.created_at) }}
|
||||||
|
</p>
|
||||||
|
<p v-if="model.read_at">
|
||||||
|
<b>{{ $t('read_at') }}: </b>
|
||||||
|
{{ getDateTime(model.read_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ShowModal>
|
||||||
|
</template>
|
||||||
36
src/components/Holos/Skeleton/Sidebar/Right.vue
Normal file
36
src/components/Holos/Skeleton/Sidebar/Right.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
import useRightSidebar from '@Stores/RightSidebar'
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const rightSidebar = useRightSidebar()
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['open']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
sidebar: Boolean
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="fixed top-[3.1rem] right-[0.1rem] md:right-[0.5rem] w-fit h-[calc(100vh-3.1rem)] transition-all duration-300 z-50"
|
||||||
|
:class="{'translate-x-0':rightSidebar.isOpened, 'translate-x-64':rightSidebar.isClosed}"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="flex md:w-64 h-full transition-all duration-300 border-none"
|
||||||
|
:class="{'w-64': rightSidebar.isClosed, 'w-screen': rightSidebar.isOpened}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full p-2 md:w-64">
|
||||||
|
<div class="flex h-full flex-col w-[15.5rem] justify-between rounded-sm overflow-y-auto overflow-x-hidden bg-primary dark:bg-primary-d text-white">
|
||||||
|
<div>
|
||||||
|
<ul class="flex h-full flex-col md:pb-4 space-y-1">
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
19
src/components/Holos/Skeleton/Sidebar/Section.vue
Normal file
19
src/components/Holos/Skeleton/Sidebar/Section.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
name: String
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul v-if="$slots['default']">
|
||||||
|
<li class="px-5 hidden md:block">
|
||||||
|
<div class="flex flex-row items-center h-8">
|
||||||
|
<div class="text-sm font-light tracking-wide text-gray-400 uppercase">
|
||||||
|
{{name}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
106
src/components/Holos/Table.vue
Normal file
106
src/components/Holos/Table.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<script setup>
|
||||||
|
import GoogleIcon from '../Shared/GoogleIcon.vue';
|
||||||
|
import Loader from '../Shared/Loader.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'send-pagination'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
items: Object,
|
||||||
|
processing: Boolean
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="pb-2">
|
||||||
|
<div class="w-full overflow-hidden rounded-sm shadow-lg dark:shadow-xs dark:shadow-white">
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
<table v-if="!processing" class="w-full">
|
||||||
|
<thead class="bg-primary text-primary-t dark:bg-primary-d dark:text-primary-dt">
|
||||||
|
<tr>
|
||||||
|
<slot name="head" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="items?.total > 0">
|
||||||
|
<slot
|
||||||
|
name="body"
|
||||||
|
:items="items?.data"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr>
|
||||||
|
<slot name="empty" />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table v-else class="animate-pulse w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="100%" class="h-8 text-center">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="100%" class="table-cell h-7 text-center">
|
||||||
|
<div class="w-full h-4 bg-secondary/50 rounded-md"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template v-if="items?.links">
|
||||||
|
<div v-if="items.links.length > 3" class="flex w-full justify-end">
|
||||||
|
<div class="flex w-full justify-end flex-wrap space-x-1 -mb-1">
|
||||||
|
<template v-for="(link, k) in items.links" :key="k">
|
||||||
|
<div v-if="link.url === null && k == 0"
|
||||||
|
class="px-2 py-1 text-sm leading-4 text-gray-400 border rounded"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_back"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button v-else-if="k === 0" class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary dark:bg-primary-dark text-white': link.active }"
|
||||||
|
@click="$emit('send-pagination', link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_back"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-else-if="link.url === null && k == (items.links.length - 1)"
|
||||||
|
class="px-2 py-1 text-sm leading-4 text-gray-400 border rounded"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_forward"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button v-else-if="k === (items.links.length - 1)" class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary dark:bg-primary-dark text-white': link.active }"
|
||||||
|
@click="$emit('send-pagination', link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
name="arrow_forward"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button v-else class="px-2 py-1 text-sm leading-4 border rounded"
|
||||||
|
:class="{ 'bg-primary dark:bg-primary-dark text-white': link.active }"
|
||||||
|
v-html="link.label"
|
||||||
|
@click="$emit('send-pagination', link.url)"
|
||||||
|
></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
33
src/components/Holos/TableSimple.vue
Normal file
33
src/components/Holos/TableSimple.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'send-pagination'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
items: Object,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="py-4">
|
||||||
|
<div class="w-full overflow-hidden rounded-md shadow-lg">
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-primary text-primary-t dark:bg-primary-d dark:text-primary-dt">
|
||||||
|
<tr>
|
||||||
|
<slot name="head" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="">
|
||||||
|
<slot
|
||||||
|
name="body"
|
||||||
|
:items="items"
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
94
src/components/Holos/Timeline/Item.vue
Normal file
94
src/components/Holos/Timeline/Item.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { getDate, getTime } from '@Controllers/DateController';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'show',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
event: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
created: 'add',
|
||||||
|
updated: 'edit',
|
||||||
|
deleted: 'delete',
|
||||||
|
restored: 'restore',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
created: 'primary',
|
||||||
|
updated: 'primary',
|
||||||
|
deleted: 'danger',
|
||||||
|
restored: 'primary',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
|
||||||
|
const eventType = computed(() => {
|
||||||
|
return props.event.event.split('.')[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
const bgColor = computed(() => {
|
||||||
|
return `bg-${colors[eventType.value]} dark:bg-${colors[eventType.value]}-d text-${colors[eventType.value]}-t dark:text-${colors[eventType.value]}-t`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const borderColor = computed(() => {
|
||||||
|
return `border-${colors[eventType.value]} dark:border-${colors[eventType.value]}-d`;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li class="border-l-2" :class="borderColor">
|
||||||
|
<div class="relative flex w-full">
|
||||||
|
<div class="absolute -left-3.5 top-7 h-0.5 w-8" :class="bgColor"></div>
|
||||||
|
<div
|
||||||
|
class="absolute -mt-3 -left-3.5 top-7 w-6 h-6 flex items-center justify-center rounded-sm"
|
||||||
|
:class="bgColor"
|
||||||
|
@click="emit('show', event.data)"
|
||||||
|
>
|
||||||
|
<GoogleIcon
|
||||||
|
:name="icons[eventType]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full rounded-sm shadow-xl dark:shadow-page-dt dark:shadow-xs my-2 mx-4">
|
||||||
|
<div class="flex justify-between p-2 rounded-t-sm" :class="bgColor">
|
||||||
|
<span
|
||||||
|
class="font-medium text-sm cursor-pointer"
|
||||||
|
@click="emit('show', event.data)"
|
||||||
|
>
|
||||||
|
{{ $t('event')}}: <i class="underline">{{ event.event }}</i>
|
||||||
|
</span>
|
||||||
|
<span class="font-medium text-sm">
|
||||||
|
{{ getDate(event.created_at) }}, {{ getTime(event.created_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="flex flex-col justify-center items-center md:flex-row md:justify-start md:space-x-4">
|
||||||
|
<div v-if="event.user" class="w-32">
|
||||||
|
<div class="flex flex-col w-full justify-center items-center space-y-2">
|
||||||
|
<img :src="event.user?.profile_photo_url" alt="Photo" class="w-24 h-24 rounded-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col justify-start space-y-2">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">{{ $t('description') }}:</h4>
|
||||||
|
<p>{{ event.description }}.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">{{ $t('author') }}:</h4>
|
||||||
|
<p>{{ event.user?.full_name ?? $t('system.title') }} <span v-if="event.user?.deleted_at" class="text-xs text-gray-500">({{ $t('deleted') }})</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
30
src/components/Shared/GoogleIcon.vue
Normal file
30
src/components/Shared/GoogleIcon.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
fill: Boolean,
|
||||||
|
style: {
|
||||||
|
type: String,
|
||||||
|
default: 'rounded' // outlined, rounded, sharp
|
||||||
|
},
|
||||||
|
title: String
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Propiedades computadas */
|
||||||
|
const classes = computed(() => {
|
||||||
|
return props.fill
|
||||||
|
? `font-google-icon-${props.style}-fill`
|
||||||
|
: `font-google-icon-${props.style}`
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-text="name"
|
||||||
|
class="material-symbols cursor-pointer"
|
||||||
|
:class="classes"
|
||||||
|
translate="no"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
6
src/components/Shared/Loader.vue
Normal file
6
src/components/Shared/Loader.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg class="animate-spin -ml-1 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
11
src/config.js
Normal file
11
src/config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import config from '../package.json'
|
||||||
|
|
||||||
|
const APP_COPYRIGHT = config.copyright
|
||||||
|
const APP_NAME = import.meta.env.VITE_APP_NAME
|
||||||
|
const APP_VERSION = config.version
|
||||||
|
|
||||||
|
export {
|
||||||
|
APP_NAME,
|
||||||
|
APP_VERSION,
|
||||||
|
APP_COPYRIGHT
|
||||||
|
}
|
||||||
34
src/controllers/DateController.js
Normal file
34
src/controllers/DateController.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
// Obtener fecha en formato deseado
|
||||||
|
function getDate(value = null) {
|
||||||
|
const date = (value)
|
||||||
|
? DateTime.fromISO(value)
|
||||||
|
: DateTime.now();
|
||||||
|
|
||||||
|
return date.toLocaleString(DateTime.DATE_MED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener hora en formato deseado
|
||||||
|
function getTime(value = null) {
|
||||||
|
const date = (value)
|
||||||
|
? DateTime.fromISO(value)
|
||||||
|
: DateTime.now();
|
||||||
|
|
||||||
|
return date.toLocaleString(DateTime.TIME_24_SIMPLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener fecha y hora
|
||||||
|
function getDateTime(value) {
|
||||||
|
const date = (value)
|
||||||
|
? DateTime.fromISO(value)
|
||||||
|
: DateTime.now();
|
||||||
|
|
||||||
|
return date.toLocaleString(DateTime.DATETIME_SHORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getDate,
|
||||||
|
getDateTime,
|
||||||
|
getTime
|
||||||
|
}
|
||||||
102
src/controllers/InboxController.js
Normal file
102
src/controllers/InboxController.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controlador simple de las bandejas
|
||||||
|
*/
|
||||||
|
class InboxController
|
||||||
|
{
|
||||||
|
inboxIdSelected = ref([]);
|
||||||
|
inboxNumberSelected = ref([]);
|
||||||
|
selectAll = ref(false);
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selecciona todas las opciones visibles
|
||||||
|
*/
|
||||||
|
onSelectAll = (elements) => {
|
||||||
|
this.inboxIdSelected.value = [];
|
||||||
|
this.inboxNumberSelected.value = [];
|
||||||
|
|
||||||
|
this.selectAll.value = !this.selectAll.value;
|
||||||
|
|
||||||
|
if(this.selectAll.value) {
|
||||||
|
elements.data.forEach(element => {
|
||||||
|
this.inboxIdSelected.value.push(element.id);
|
||||||
|
this.inboxNumberSelected.value.push(element.number);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selecciona solo una opcion
|
||||||
|
*/
|
||||||
|
onSelectOne = (invoice) => {
|
||||||
|
this.inboxIdSelected.value.push(invoice.id);
|
||||||
|
this.inboxNumberSelected.value.push(invoice.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permite que siempresea solo una opcion seleccionada
|
||||||
|
*/
|
||||||
|
onlyOne = (invoice) => {
|
||||||
|
this.clear();
|
||||||
|
this.onSelectOne(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quita la seleccion
|
||||||
|
*/
|
||||||
|
onUnselectOne = (invoice) => {
|
||||||
|
this.inboxIdSelected.value = this.inboxIdSelected.value.filter(element => {
|
||||||
|
return element !== invoice.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inboxNumberSelected.value = this.inboxNumberSelected.value.filter(element => {
|
||||||
|
return element !== invoice.number;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna todos los IDs de los elementos seleccionados
|
||||||
|
*/
|
||||||
|
getIdSelections = () => {
|
||||||
|
return this.inboxIdSelected.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trata al ID como si fueran muchos
|
||||||
|
*
|
||||||
|
* Si no se pasa mimgun ID, se devolveran todos los elementos seleccionados almacenados
|
||||||
|
*/
|
||||||
|
getAsMany = (id) => {
|
||||||
|
return (id) ? [ id ] : this.getIdSelections();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna todos los numeros de las facturas/seleccionadas
|
||||||
|
*/
|
||||||
|
getNumberSelections = () => {
|
||||||
|
return this.inboxNumberSelected.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpia los valores seleccionados
|
||||||
|
*/
|
||||||
|
clear = () => {
|
||||||
|
this.inboxIdSelected.value = [];
|
||||||
|
this.inboxNumberSelected.value = [];
|
||||||
|
this.selectAll.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpia los valores seleccionados con una notificacion de exito
|
||||||
|
*/
|
||||||
|
clearWithSuccess = (message) => {
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
Notify.success(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InboxController;
|
||||||
92
src/controllers/ModalController.js
Normal file
92
src/controllers/ModalController.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controlador simple de las bandejas
|
||||||
|
*/
|
||||||
|
class ModalController
|
||||||
|
{
|
||||||
|
// Modals
|
||||||
|
confirmModal = ref(false);
|
||||||
|
destroyModal = ref(false);
|
||||||
|
editModal = ref(false);
|
||||||
|
noteModal = ref(false);
|
||||||
|
manyNotesModal = ref(false);
|
||||||
|
showModal = ref(false);
|
||||||
|
importModal = ref(false);
|
||||||
|
|
||||||
|
// Models
|
||||||
|
modelModal = ref({});
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el cambio entre show y edit
|
||||||
|
*/
|
||||||
|
switchShowEditModal = () => {
|
||||||
|
this.showModal.value = !this.showModal.value
|
||||||
|
this.editModal.value = !this.editModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de eliminar
|
||||||
|
*/
|
||||||
|
switchShowModal = (model) => {
|
||||||
|
this._setModel(model);
|
||||||
|
this.showModal.value = !this.showModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de importar
|
||||||
|
*/
|
||||||
|
switchImportModal = () => {
|
||||||
|
this.importModal.value = !this.importModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de eliminar
|
||||||
|
*/
|
||||||
|
switchEditModal = (model) => {
|
||||||
|
this._setModel(model);
|
||||||
|
this.editModal.value = !this.editModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de eliminar
|
||||||
|
*/
|
||||||
|
switchDestroyModal = (model) => {
|
||||||
|
this._setModel(model);
|
||||||
|
this.destroyModal.value = !this.destroyModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de nota
|
||||||
|
*/
|
||||||
|
switchNoteModal = () => {
|
||||||
|
this.noteModal.value = !this.noteModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de notas aplicadas a muchos
|
||||||
|
*/
|
||||||
|
switchManyNotesModal = () => {
|
||||||
|
this.manyNotesModal.value = !this.manyNotesModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla el switch de nota
|
||||||
|
*/
|
||||||
|
switchConfirmModal = () => {
|
||||||
|
this.confirmModal.value = !this.confirmModal.value
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guarda el modelo
|
||||||
|
*/
|
||||||
|
_setModel = (model) => {
|
||||||
|
if(model) {
|
||||||
|
this.modelModal.value = model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalController;
|
||||||
51
src/controllers/PrintController.js
Executable file
51
src/controllers/PrintController.js
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controla la generación de impresiones
|
||||||
|
*/
|
||||||
|
class PrintController
|
||||||
|
{
|
||||||
|
invoices = ref(false);
|
||||||
|
|
||||||
|
constructor({route, meta, params = {}, name = "Comprobante", type='pdf'}) {
|
||||||
|
this.route = route;
|
||||||
|
this.meta = meta;
|
||||||
|
this.params = params;
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manda la orden de impresión y descarga
|
||||||
|
*/
|
||||||
|
|
||||||
|
download = (data = {}) => {
|
||||||
|
Notify.info('Generando archivo, espere ...');
|
||||||
|
|
||||||
|
axios({
|
||||||
|
url: route(this.route, this.params),
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
meta: this.meta,
|
||||||
|
data: data
|
||||||
|
},
|
||||||
|
responseType: 'blob'
|
||||||
|
}).then((response) => {
|
||||||
|
const href = URL.createObjectURL(response.data);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = href;
|
||||||
|
link.setAttribute('download', `${this.name}.${this.type}`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(href);
|
||||||
|
Notify.info('Archivo generado');
|
||||||
|
}).catch(err => {
|
||||||
|
Notify.error('Error al generar');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrintController;
|
||||||
87
src/css/app.css
Normal file
87
src/css/app.css
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "../../colors.css";
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-google-icon-outlined: "Material Symbols Outlined";
|
||||||
|
--font-google-icon-outlined-fill: "Material Symbols Outlined Fill";
|
||||||
|
--font-google-icon-rounded: "Material Symbols Rounded";
|
||||||
|
--font-google-icon-rounded-fill: "Material Symbols rounded-sm Fill";
|
||||||
|
--font-google-icon-sharp: "Material Symbols Sharp";
|
||||||
|
--font-google-icon-sharp-fill: "Material Symbols Sharp Fill";
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bg-light {
|
||||||
|
@apply bg-primary
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bg-dark {
|
||||||
|
@apply bg-primary-d
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex justify-center items-center w-fit px-1.5 py-1.5 rounded-sm font-medium border border-transparent text-xs text-white uppercase tracking-widest hover:opacity-90 focus:outline-hidden active:saturate-150 disabled:opacity-25 cursor-pointer transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-primary dark:bg-primary-d text-primary-t dark:text-primary-dt hover:bg-secondary dark:hover:bg-secondary-d hover:text-secondary-t dark:hover:text-secondary-dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-secondary dark:bg-secondary-d text-secondary-t dark:text-secondary-dt hover:bg-secondary dark:hover:bg-secondary-d hover:text-secondary-t dark:hover:text-secondary-dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
@apply bg-success dark:bg-success-d text-success-t dark:text-success-dt hover:bg-success dark:hover:bg-success hover:text-success-t dark:hover:text-success-dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply bg-danger dark:bg-danger-d text-danger-t dark:text-danger-dt hover:bg-danger dark:hover:bg-danger hover:text-danger-t dark:hover:text-danger-dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
@apply bg-warning dark:bg-warning-d text-warning-t dark:text-warning-dt hover:bg-warning dark:hover:bg-warning hover:text-warning-t dark:hover:text-warning-dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
@apply flex w-fit min-h-6 px-1.5 py-1.5 rounded-sm font-medium bg-primary dark:bg-primary-d text-primary-t dark:text-primary-dt hover:bg-secondary dark:hover:bg-secondary-d hover:text-secondary-t dark:hover:text-secondary-dt cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-primary {
|
||||||
|
@apply w-full p-[6.5px] border-b border-page-t/50 dark:border-page-dt/50 bg-primary/5 rounded-sm outline-0
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
@apply p-1
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
@apply hover:bg-secondary/10 dark:hover:bg-secondary-d/10 transition-colors duration-100
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
@apply px-2 py-0.5 text-sm border border-primary/30 dark:border-primary-dt/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
@apply flex justify-center items-center space-x-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a.router-link-active {
|
||||||
|
@apply bg-secondary/30 dark:bg-secondary-d/30 border-secondary dark:border-secondary-d
|
||||||
|
}
|
||||||
|
|
||||||
|
.with-transition {
|
||||||
|
@apply transition-all duration-300
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch
|
||||||
|
*/
|
||||||
|
.toggle-checkbox:checked {
|
||||||
|
@apply right-0 border-slate-500
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-checkbox:checked + .toggle-label {
|
||||||
|
@apply bg-slate-500
|
||||||
|
}
|
||||||
5
src/css/base.css
Normal file
5
src/css/base.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import './app.css';
|
||||||
|
@import './icons.css';
|
||||||
|
@import './notifications.css';
|
||||||
|
@import "vue-multiselect/dist/vue-multiselect.css";
|
||||||
|
@import './multiselect.css';
|
||||||
77
src/css/icons.css
Normal file
77
src/css/icons.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Outlined
|
||||||
|
*
|
||||||
|
* https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0
|
||||||
|
* https://fonts.gstatic.com/s/materialsymbolsoutlined/v170/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOej.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./icons/google/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOej.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outlined fill
|
||||||
|
*
|
||||||
|
* https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,1,0
|
||||||
|
* https://fonts.gstatic.com/s/materialsymbolsoutlined/v170/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzazHD_dY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOej.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Outlined Fill';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./icons/google/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzazHD_dY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOej.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounded
|
||||||
|
*
|
||||||
|
* https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0
|
||||||
|
* https://fonts.gstatic.com/s/materialsymbolsrounded/v168/syl0-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvDmUSVOK7BDB_Qb9vUSzq3wzLK-P0J-V_Zs-QtQth3-jOcbTCVpeRL2w5rwZu2rIelXxc.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Rounded';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./icons/google/syl0-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvDmUSVOK7BDB_Qb9vUSzq3wzLK-P0J-V_Zs-QtQth3-jOcbTCVpeRL2w5rwZu2rIelXxc.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rounded-sm fill
|
||||||
|
*
|
||||||
|
* https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,1,0
|
||||||
|
* https://fonts.gstatic.com/s/materialsymbolsrounded/v168/syl0-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvDmUSVOK7BDJ_vb9vUSzq3wzLK-P0J-V_Zs-QtQth3-jOcbTCVpeRL2w5rwZu2rIelXxc.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols rounded-sm Fill';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./icons/google/syl0-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvDmUSVOK7BDJ_vb9vUSzq3wzLK-P0J-V_Zs-QtQth3-jOcbTCVpeRL2w5rwZu2rIelXxc.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sharp
|
||||||
|
*
|
||||||
|
* https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@24,400,0,0
|
||||||
|
* https://fonts.gstatic.com/s/materialsymbolssharp/v166/gNNBW2J8Roq16WD5tFNRaeLQk6-SHQ_R00k4c2_whPnoY9ruReaU4bHmz74m0ZkGH-VBYe1x0TV6x4yFH8F-H5OdzEL3sVTgJtfbYxOLojCL.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Sharp';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./icons/google/gNNBW2J8Roq16WD5tFNRaeLQk6-SHQ_R00k4c2_whPnoY9ruReaU4bHmz74m0ZkGH-VBYe1x0TV6x4yFH8F-H5OdzEL3sVTgJtfbYxOLojCL.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sharp fill
|
||||||
|
*
|
||||||
|
* https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@24,400,1,0
|
||||||
|
* https://fonts.gstatic.com/s/materialsymbolssharp/v166/gNNBW2J8Roq16WD5tFNRaeLQk6-SHQ_R00k4c2_whPnoY9ruReYU3rHmz74m0ZkGH-VBYe1x0TV6x4yFH8F-H5OdzEL3sVTgJtfbYxOLojCL.woff2
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Sharp Fill';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(./icons/google/gNNBW2J8Roq16WD5tFNRaeLQk6-SHQ_R00k4c2_whPnoY9ruReYU3rHmz74m0ZkGH-VBYe1x0TV6x4yFH8F-H5OdzEL3sVTgJtfbYxOLojCL.woff2) format('woff2');
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
57
src/css/multiselect.css
Normal file
57
src/css/multiselect.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.multiselect {
|
||||||
|
@apply dark:text-white min-h-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input,
|
||||||
|
.multiselect__single {
|
||||||
|
@apply bg-white dark:bg-transparent dark:text-white
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input::placeholder {
|
||||||
|
@apply text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag {
|
||||||
|
@apply bg-success dark:bg-success-d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag-icon::after {
|
||||||
|
@apply text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag-icon:focus::after,
|
||||||
|
.multiselect__tag-icon:hover::after {
|
||||||
|
@apply text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
@apply bg-primary/5 dark:bg-primary-d/5 min-h-8 border-0 border-b border-page-t/50 dark:border-page-dt/50 pt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--highlight {
|
||||||
|
@apply dark:bg-green-900 dark:text-white outline-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--highlight::after {
|
||||||
|
@apply dark:bg-green-800 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--selected.multiselect__option--highlight {
|
||||||
|
@apply bg-red-500 dark:bg-red-600 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--selected.multiselect__option--highlight::after {
|
||||||
|
@apply bg-red-400 dark:bg-red-500 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__content-wrapper {
|
||||||
|
@apply bg-page dark:bg-page-d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.multiselect--disabled,
|
||||||
|
.multiselect--disabled .multiselect__select,
|
||||||
|
.multiselect--disabled .multiselect__current,
|
||||||
|
.multiselect__option--disabled.multiselect__option--highlight{
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
234
src/css/notifications.css
Normal file
234
src/css/notifications.css
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
.toast-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.toast-message {
|
||||||
|
-ms-word-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.toast-message a,
|
||||||
|
.toast-message label {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.toast-message a:hover {
|
||||||
|
color: #CCCCCC;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.toast-close-button {
|
||||||
|
position: relative;
|
||||||
|
right: -0.3em;
|
||||||
|
top: -0.3em;
|
||||||
|
float: right;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
-webkit-text-shadow: 0 1px 0 #ffffff;
|
||||||
|
text-shadow: 0 1px 0 #ffffff;
|
||||||
|
opacity: 0.8;
|
||||||
|
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
|
||||||
|
filter: alpha(opacity=80);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.toast-close-button:hover,
|
||||||
|
.toast-close-button:focus {
|
||||||
|
color: #000000;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.4;
|
||||||
|
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
|
||||||
|
filter: alpha(opacity=40);
|
||||||
|
}
|
||||||
|
.rtl .toast-close-button {
|
||||||
|
left: -0.3em;
|
||||||
|
float: left;
|
||||||
|
right: 0.3em;
|
||||||
|
}
|
||||||
|
/*Additional properties for button version
|
||||||
|
iOS requires the button element instead of an anchor tag.
|
||||||
|
If you want the anchor version, it requires `href="#"`.*/
|
||||||
|
button.toast-close-button {
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
.toast-top-center {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toast-bottom-center {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toast-top-full-width {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toast-bottom-full-width {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toast-top-left {
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
.toast-top-right {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
.toast-bottom-right {
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
}
|
||||||
|
.toast-bottom-left {
|
||||||
|
bottom: 12px;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
#toast-container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999999;
|
||||||
|
pointer-events: none;
|
||||||
|
/*overrides*/
|
||||||
|
}
|
||||||
|
#toast-container * {
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
#toast-container > div {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
padding: 15px 15px 15px 50px;
|
||||||
|
width: 300px;
|
||||||
|
-moz-border-radius: 3px 3px 3px 3px;
|
||||||
|
-webkit-border-radius: 3px 3px 3px 3px;
|
||||||
|
border-radius: 3px 3px 3px 3px;
|
||||||
|
background-position: 15px center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
-moz-box-shadow: 0 0 12px #999999;
|
||||||
|
-webkit-box-shadow: 0 0 12px #999999;
|
||||||
|
box-shadow: 0 0 12px #999999;
|
||||||
|
color: #FFFFFF;
|
||||||
|
opacity: 0.8;
|
||||||
|
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
|
||||||
|
filter: alpha(opacity=80);
|
||||||
|
}
|
||||||
|
#toast-container > div.rtl {
|
||||||
|
direction: rtl;
|
||||||
|
padding: 15px 50px 15px 15px;
|
||||||
|
background-position: right 15px center;
|
||||||
|
}
|
||||||
|
#toast-container > div:hover {
|
||||||
|
-moz-box-shadow: 0 0 12px #000000;
|
||||||
|
-webkit-box-shadow: 0 0 12px #000000;
|
||||||
|
box-shadow: 0 0 12px #000000;
|
||||||
|
opacity: 1;
|
||||||
|
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||||
|
filter: alpha(opacity=100);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#toast-container > .toast-info {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important;
|
||||||
|
}
|
||||||
|
#toast-container > .toast-error {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
|
||||||
|
}
|
||||||
|
#toast-container > .toast-success {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important;
|
||||||
|
}
|
||||||
|
#toast-container > .toast-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
|
||||||
|
}
|
||||||
|
#toast-container.toast-top-center > div,
|
||||||
|
#toast-container.toast-bottom-center > div {
|
||||||
|
width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
#toast-container.toast-top-full-width > div,
|
||||||
|
#toast-container.toast-bottom-full-width > div {
|
||||||
|
width: 96%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
background-color: #030303;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
@apply bg-success dark:bg-success-d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
@apply bg-danger dark:bg-danger-d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
@apply bg-primary-info dark:bg-primary-info-d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warning {
|
||||||
|
@apply bg-warning dark:bg-warning-d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-progress {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #000000;
|
||||||
|
opacity: 0.4;
|
||||||
|
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
|
||||||
|
filter: alpha(opacity=40);
|
||||||
|
}
|
||||||
|
/*Responsive Design*/
|
||||||
|
@media all and (max-width: 240px) {
|
||||||
|
#toast-container > div {
|
||||||
|
padding: 8px 8px 8px 50px;
|
||||||
|
width: 11em;
|
||||||
|
}
|
||||||
|
#toast-container > div.rtl {
|
||||||
|
padding: 8px 50px 8px 8px;
|
||||||
|
}
|
||||||
|
#toast-container .toast-close-button {
|
||||||
|
right: -0.2em;
|
||||||
|
top: -0.2em;
|
||||||
|
}
|
||||||
|
#toast-container .rtl .toast-close-button {
|
||||||
|
left: -0.2em;
|
||||||
|
right: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 241px) and (max-width: 480px) {
|
||||||
|
#toast-container > div {
|
||||||
|
padding: 8px 8px 8px 50px;
|
||||||
|
width: 18em;
|
||||||
|
}
|
||||||
|
#toast-container > div.rtl {
|
||||||
|
padding: 8px 50px 8px 8px;
|
||||||
|
}
|
||||||
|
#toast-container .toast-close-button {
|
||||||
|
right: -0.2em;
|
||||||
|
top: -0.2em;
|
||||||
|
}
|
||||||
|
#toast-container .rtl .toast-close-button {
|
||||||
|
left: -0.2em;
|
||||||
|
right: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 481px) and (max-width: 768px) {
|
||||||
|
#toast-container > div {
|
||||||
|
padding: 15px 15px 15px 50px;
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
#toast-container > div.rtl {
|
||||||
|
padding: 15px 50px 15px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
74
src/index.js
Normal file
74
src/index.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import './css/base.css'
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { useRoute, ZiggyVue } from 'ziggy-js';
|
||||||
|
import { i18n, lang } from '@Lang/i18n.js';
|
||||||
|
import router from '@Router/Index'
|
||||||
|
import Notify from '@Plugins/Notify'
|
||||||
|
import { bootPermissions, bootRoles } from '@Plugins/RolePermission';
|
||||||
|
import TailwindScreen from '@Plugins/TailwindScreen'
|
||||||
|
import { pagePlugin } from '@Services/Page';
|
||||||
|
import { defineApp, reloadApp, view } from '@Services/Page';
|
||||||
|
import { apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import App from '@Components/App.vue'
|
||||||
|
import Error503 from '@Pages/Errors/503.vue'
|
||||||
|
import { hasToken } from './services/Api';
|
||||||
|
|
||||||
|
// Configurar axios
|
||||||
|
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
|
// Elementos globales
|
||||||
|
window.axios = axios;
|
||||||
|
window.Lang = lang;
|
||||||
|
window.Notify = new Notify();
|
||||||
|
window.TwScreen = new TailwindScreen();
|
||||||
|
|
||||||
|
async function boot() {
|
||||||
|
let initRoutes = false;
|
||||||
|
|
||||||
|
// Iniciar rutas
|
||||||
|
try {
|
||||||
|
const routes = await axios.get(apiURL('resources/routes'));
|
||||||
|
const appData = await axios.get(apiURL('resources/app'));
|
||||||
|
|
||||||
|
window.Ziggy = routes.data;
|
||||||
|
defineApp(appData.data);
|
||||||
|
window.route = useRoute();
|
||||||
|
window.view = view;
|
||||||
|
initRoutes = true;
|
||||||
|
} catch (error) {
|
||||||
|
window.Notify.error(window.Lang('server.api.noAvailable'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(initRoutes) {
|
||||||
|
// Iniciar permisos
|
||||||
|
if(hasToken()) {
|
||||||
|
await bootPermissions();
|
||||||
|
await bootRoles();
|
||||||
|
|
||||||
|
// Iniciar broadcast
|
||||||
|
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
|
||||||
|
await import('@Services/Broadcast')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadApp();
|
||||||
|
|
||||||
|
createApp(App)
|
||||||
|
.use(createPinia())
|
||||||
|
.use(i18n)
|
||||||
|
.use(pagePlugin)
|
||||||
|
.use(router)
|
||||||
|
.use(ZiggyVue)
|
||||||
|
.mount('#app');
|
||||||
|
} else {
|
||||||
|
createApp(Error503)
|
||||||
|
.mount('#app');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar aplicación
|
||||||
|
boot();
|
||||||
205
src/lang/en.js
Normal file
205
src/lang/en.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
export default {
|
||||||
|
'&':'and',
|
||||||
|
account: {
|
||||||
|
delete: {
|
||||||
|
confirm:'Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.',
|
||||||
|
description: 'Permanently delete your account.',
|
||||||
|
onDelete:'Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.',
|
||||||
|
title:'Delete Account',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
notifySendVerification:'A new verification link has been sent to your email address.',
|
||||||
|
sendVerification:'Click here to re-send the verification email',
|
||||||
|
unverify: 'Your email address is unverified.',
|
||||||
|
},
|
||||||
|
manage:'Manage Account',
|
||||||
|
password: {
|
||||||
|
description:'Ensure your account is using a long, random password to stay secure.',
|
||||||
|
new:'New password',
|
||||||
|
reset:'Reset password',
|
||||||
|
secure:'This is a secure area of the application. Please confirm your password before continuing.',
|
||||||
|
update: 'Update Password',
|
||||||
|
verify:'For your security, please confirm your password to continue.',
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
description:'Update your account\'s profile information and email address.',
|
||||||
|
title:'Profile Information',
|
||||||
|
},
|
||||||
|
sessions: {
|
||||||
|
confirm:'Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.',
|
||||||
|
description: 'Manage and log out your active sessions on other browsers and devices.',
|
||||||
|
last:'Last active',
|
||||||
|
logout:'Log Out Other Browser Sessions',
|
||||||
|
onLogout:'If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.',
|
||||||
|
this: 'This device',
|
||||||
|
title: 'Browser Sessions',
|
||||||
|
},
|
||||||
|
twoFactor: {
|
||||||
|
codes:{
|
||||||
|
regenerate:'Regenerate Recovery Codes',
|
||||||
|
show:'Show Recovery Codes',
|
||||||
|
store:'Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.',
|
||||||
|
},
|
||||||
|
description:'Add additional security to your account using two factor authentication.',
|
||||||
|
isEnable:'You have enabled two factor authentication.',
|
||||||
|
isNotEnable:{
|
||||||
|
title:'You have not enabled two factor authentication.',
|
||||||
|
description:'When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone\'s Google Authenticator application.',
|
||||||
|
},
|
||||||
|
key:'Setup Key',
|
||||||
|
login: {
|
||||||
|
onAuth: 'Please confirm access to your account by entering the authentication code provided by your authenticator application.',
|
||||||
|
onRecovery: 'Please confirm access to your account by entering one of your emergency recovery codes.',
|
||||||
|
},
|
||||||
|
onFinish:'Finish enabling two factor authentication.',
|
||||||
|
qr: {
|
||||||
|
isConfirmed: 'Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application or enter the setup key.',
|
||||||
|
onConfirmed: 'To finish enabling two factor authentication, scan the following QR code using your phone\'s authenticator application or enter the setup key and provide the generated OTP code.',
|
||||||
|
},
|
||||||
|
recovery: {
|
||||||
|
code: 'Recovery code',
|
||||||
|
useAuth: 'Use an authentication code',
|
||||||
|
useCode: 'Use a recovery code',
|
||||||
|
},
|
||||||
|
title:'Two Factor Authentication',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions:'Actions',
|
||||||
|
auth: {
|
||||||
|
forgotPassword: {
|
||||||
|
ask: 'Forgot your password?',
|
||||||
|
description: 'Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.',
|
||||||
|
sendLink: 'Email Password Reset Link',
|
||||||
|
title: 'Forgot Password',
|
||||||
|
},
|
||||||
|
login: 'Log In',
|
||||||
|
logout: 'Log Out',
|
||||||
|
register: {
|
||||||
|
already: 'Already registered?',
|
||||||
|
me: 'Register me',
|
||||||
|
},
|
||||||
|
remember: 'Remember me',
|
||||||
|
},
|
||||||
|
code:'Code',
|
||||||
|
cancel:'Cancel',
|
||||||
|
changelogs: {
|
||||||
|
title:'Changelogs',
|
||||||
|
description: 'List of changes made to the system.',
|
||||||
|
},
|
||||||
|
close:"Close",
|
||||||
|
confirm:'Confirm',
|
||||||
|
copyright:'All rights reserved.',
|
||||||
|
contact:'Contact',
|
||||||
|
description:'Description',
|
||||||
|
date:'Date',
|
||||||
|
delete: {
|
||||||
|
confirm:"By pressing DELETE the record will be permanently deleted and cannot be recovered.",
|
||||||
|
title:'Deleted',
|
||||||
|
},
|
||||||
|
deleted:'Deleted',
|
||||||
|
details:'Details',
|
||||||
|
disable:'Disable',
|
||||||
|
disabled:'Disable',
|
||||||
|
done:'Done.',
|
||||||
|
edit:'Edit',
|
||||||
|
email:'Email',
|
||||||
|
enable:'Enable',
|
||||||
|
endDate:'End date',
|
||||||
|
event:'Event',
|
||||||
|
help: {
|
||||||
|
description:'The following is a list of iconography to understand how the system works.',
|
||||||
|
home: 'Back to home page.',
|
||||||
|
title:'Help',
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
title:'Stock history',
|
||||||
|
description:'History of actions performed by users in chronological order.',
|
||||||
|
},
|
||||||
|
home:'Home',
|
||||||
|
hour:'Hour',
|
||||||
|
icon:'Icon',
|
||||||
|
maternal:'Mother\'s last name',
|
||||||
|
name:'Name',
|
||||||
|
noRecords:'No records',
|
||||||
|
notifications: {
|
||||||
|
deleted:'Notification deleted',
|
||||||
|
description:'User notifications',
|
||||||
|
notFound:'Notification not found',
|
||||||
|
title:'Notifications',
|
||||||
|
},
|
||||||
|
password:'Password',
|
||||||
|
passwordConfirmation:'Confirm Password',
|
||||||
|
passwordCurrent:'Current Password',
|
||||||
|
paternal:'Paternal surname',
|
||||||
|
phone:'Phone Number',
|
||||||
|
photo: {
|
||||||
|
new: 'Select A New Photo',
|
||||||
|
remove:'Remove Photo',
|
||||||
|
title:'Photo',
|
||||||
|
},
|
||||||
|
profile:'Profile',
|
||||||
|
readed:'Readed',
|
||||||
|
register: {
|
||||||
|
agree:'I agree to the',
|
||||||
|
privacy:'Privacy Policy',
|
||||||
|
signUp:'Sign Up',
|
||||||
|
terms:'Terms of Service',
|
||||||
|
},
|
||||||
|
role:'Role',
|
||||||
|
roles:{
|
||||||
|
create: {
|
||||||
|
title: 'Create role',
|
||||||
|
description: 'These roles will be used to give permissions in the system.',
|
||||||
|
onSuccess: 'Role successfully created',
|
||||||
|
onError: 'Error when creating the role',
|
||||||
|
},
|
||||||
|
deleted:'Role deleted',
|
||||||
|
title: 'Roles',
|
||||||
|
},
|
||||||
|
save:'Save',
|
||||||
|
saved:'Saved!',
|
||||||
|
search:'Search',
|
||||||
|
show: {
|
||||||
|
all:'Show All',
|
||||||
|
title:'Show',
|
||||||
|
},
|
||||||
|
startDate:'Start date',
|
||||||
|
status:'Status',
|
||||||
|
terms: {
|
||||||
|
agree:'I agree to the',
|
||||||
|
privacy:'Privacy Policy',
|
||||||
|
service:'Terms of Service',
|
||||||
|
},
|
||||||
|
unknown:'Unknown',
|
||||||
|
update:'Update',
|
||||||
|
updated:'Updated',
|
||||||
|
updateFail:'Error while updating',
|
||||||
|
unreaded:'Unreaded',
|
||||||
|
user:'User',
|
||||||
|
users:{
|
||||||
|
create:{
|
||||||
|
title:'Create user',
|
||||||
|
description:'Allows you to create new users. Don\'t forget to give them roles so that they can access the desired parts of the system.',
|
||||||
|
onSuccess:'User created',
|
||||||
|
onError:'An error occurred while creating the user',
|
||||||
|
},
|
||||||
|
deleted:'User deleted',
|
||||||
|
notFount:'User not found',
|
||||||
|
password: {
|
||||||
|
description:'Allows users\' passwords to be updated',
|
||||||
|
title:'Update password',
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
description:'Updates user roles, allowing or denying access to certain areas.',
|
||||||
|
error:{
|
||||||
|
min:'Select at least one role'
|
||||||
|
},
|
||||||
|
title:'Roles',
|
||||||
|
},
|
||||||
|
select:'Select a user',
|
||||||
|
settings:'User setting',
|
||||||
|
system:'System users',
|
||||||
|
title:'Users',
|
||||||
|
},
|
||||||
|
version:'Version',
|
||||||
|
}
|
||||||
452
src/lang/es.js
Normal file
452
src/lang/es.js
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
import { success } from "toastr";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'&':'y',
|
||||||
|
account: {
|
||||||
|
delete: {
|
||||||
|
confirm:'¿Está seguro de que quiere eliminar su cuenta? Una vez eliminada su cuenta, todos sus recursos y datos se borrarán permanentemente. Por favor, introduzca su contraseña para confirmar que desea eliminar permanentemente su cuenta.',
|
||||||
|
description:'Eliminar permanentemente su cuenta.',
|
||||||
|
onDelete:'Una vez eliminada su cuenta, todos sus recursos y datos se borrarán permanentemente. Antes de eliminar su cuenta, descargue los datos o la información que desee conservar.',
|
||||||
|
title:'Eliminar cuenta',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
notifySendVerification:'Se ha enviado un nuevo enlace de verificación a su dirección de correo electrónico.',
|
||||||
|
sendVerification:'Haga clic aquí para volver a enviar el correo electrónico de verificación.',
|
||||||
|
unverify: 'Su dirección de correo electrónico no está verificada.',
|
||||||
|
},
|
||||||
|
manage:'Administrar cuenta',
|
||||||
|
password: {
|
||||||
|
description:'Asegúrese de que su cuenta utiliza una contraseña larga y aleatoria para estar seguro.',
|
||||||
|
new:'Nueva contraseña',
|
||||||
|
reset:'Restaurar contraseña',
|
||||||
|
secure:'Esta es una zona segura de la aplicación. Confirme su contraseña antes de continuar.',
|
||||||
|
update: 'Actualizar contraseña',
|
||||||
|
updated:'Contraseña actualizada',
|
||||||
|
verify:'Por su seguridad, confirme su contraseña para continuar.',
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
description:'Actualice la información del perfil de su cuenta y su dirección de correo electrónico.',
|
||||||
|
title:'Información del perfil',
|
||||||
|
updated:'Perfil actualizado',
|
||||||
|
},
|
||||||
|
sessions: {
|
||||||
|
confirm:'Por favor, introduzca su contraseña para confirmar que desea salir de sus otras sesiones de navegación en todos sus dispositivos.',
|
||||||
|
description: 'Gestiona y cierra tus sesiones activas en otros navegadores y dispositivos.',
|
||||||
|
last:'Último activo',
|
||||||
|
logout:'Cerrar otras sesiones del navegador',
|
||||||
|
done:'Sesiones cerradas',
|
||||||
|
onLogout:'Si es necesario, puede cerrar la sesión de todos sus otros navegadores en todos sus dispositivos. A continuación se enumeran algunas de sus sesiones recientes; sin embargo, esta lista puede no ser exhaustiva. Si crees que tu cuenta ha sido comprometida, también deberías actualizar tu contraseña.',
|
||||||
|
this: 'Dispositivo actual',
|
||||||
|
title: 'Sesiones del navegador',
|
||||||
|
},
|
||||||
|
twoFactor: {
|
||||||
|
codes:{
|
||||||
|
regenerate:'Regenerar los códigos de recuperación',
|
||||||
|
show:'Mostrar códigos de recuperación',
|
||||||
|
store:'Guarde estos códigos de recuperación en un gestor de contraseñas seguro. Pueden utilizarse para recuperar el acceso a su cuenta si se pierde su dispositivo de autenticación de dos factores.',
|
||||||
|
},
|
||||||
|
description:'Añada seguridad adicional a su cuenta mediante la autenticación de dos factores.',
|
||||||
|
isEnable:'Ha activado la autenticación de dos factores.',
|
||||||
|
isNotEnable:{
|
||||||
|
title:'No ha activado la autenticación de dos factores.',
|
||||||
|
description:'Cuando la autenticación de dos factores está activada, se le pedirá un token seguro y aleatorio durante la autenticación. Puedes recuperar este token desde la aplicación Google Authenticator de tu teléfono.',
|
||||||
|
},
|
||||||
|
key:'Llave de configuración',
|
||||||
|
login: {
|
||||||
|
onAuth: 'Por favor, confirme el acceso a su cuenta introduciendo el código de autentificación proporcionado por su aplicación de autentificación.',
|
||||||
|
onRecovery: 'Confirme el acceso a su cuenta introduciendo uno de sus códigos de recuperación de emergencia.',
|
||||||
|
},
|
||||||
|
onFinish:'Termina de habilitar la autenticación de dos factores.',
|
||||||
|
qr: {
|
||||||
|
isConfirmed: 'La autenticación de dos factores ya está activada. Escanee el siguiente código QR con la aplicación de autenticación de su teléfono o introduzca la clave de configuración.',
|
||||||
|
onConfirmed: 'Para terminar de habilitar la autenticación de dos factores, escanea el siguiente código QR utilizando la aplicación de autenticación de tu teléfono o introduce la clave de configuración y proporciona el código OTP generado.',
|
||||||
|
},
|
||||||
|
recovery: {
|
||||||
|
code: 'Código de recuperación',
|
||||||
|
useAuth: 'Utilizar un código de autentificación',
|
||||||
|
useCode: 'Utiliza un código de recuperación',
|
||||||
|
},
|
||||||
|
title:'Autenticación de dos factores',
|
||||||
|
},
|
||||||
|
title: 'Cuenta',
|
||||||
|
},
|
||||||
|
actions:'Acciones',
|
||||||
|
activity:'Actividad',
|
||||||
|
add: 'Agregar',
|
||||||
|
admin: {
|
||||||
|
title: 'Administración',
|
||||||
|
activity: {
|
||||||
|
title: 'Historial de acciones',
|
||||||
|
description: 'Historial de acciones realizadas por los usuarios en orden cronológico.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
theme: {
|
||||||
|
dark: 'Tema oscuro',
|
||||||
|
light: 'Tema claro'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assistances: {
|
||||||
|
create: {
|
||||||
|
title: 'Generar asistencia',
|
||||||
|
description: 'Genera una instantánea del Head Count actual de los empleados, por planta y por línea de producción.',
|
||||||
|
},
|
||||||
|
title: 'Asistencias',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
confirmPassword: {
|
||||||
|
description: 'Esta es una zona segura de la aplicación. Por favor, confirma tu contraseña antes de continuar.',
|
||||||
|
title: 'Confirmar contraseña',
|
||||||
|
},
|
||||||
|
forgotPassword: {
|
||||||
|
ask: '¿Olvidaste tu contraseña?',
|
||||||
|
description: '¿Ha olvidado su contraseña? No hay problema. Sólo tienes que indicarnos tu dirección de correo electrónico y te enviaremos un enlace para restablecer la contraseña que te permitirá elegir una nueva.',
|
||||||
|
sendLink: 'Enviar enlace de recuperación',
|
||||||
|
title: 'Contraseña olvidada',
|
||||||
|
success: 'Se ha enviado un enlace de recuperación a su dirección de correo electrónico.',
|
||||||
|
error: 'Error al enviar el enlace de recuperación, intente más tarde.',
|
||||||
|
},
|
||||||
|
login: 'Iniciar sesión',
|
||||||
|
logout: 'Cerrar sesión',
|
||||||
|
register: {
|
||||||
|
already: '¿Ya estas registrado?',
|
||||||
|
me: 'Registrarme',
|
||||||
|
},
|
||||||
|
reset: {
|
||||||
|
success: 'Contraseña actualizada',
|
||||||
|
title: 'Restaurar contraseña',
|
||||||
|
},
|
||||||
|
remember: 'Recuerdame',
|
||||||
|
verifyEmail: {
|
||||||
|
beforeContinue: 'Antes de continuar, ¿podrías verificar tu correo electrónico haciendo clic en el enlace que acabamos de enviar a ti? Si no recibiste el correo electrónico, estaremos encantados de enviarte otro.',
|
||||||
|
sendLink: 'Enviar correo de verificación',
|
||||||
|
title: 'Verificación de correo electrónico',
|
||||||
|
notifySendVerification: 'Se ha enviado un nuevo enlace de verificación a su dirección de correo electrónico.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
author:'Autor',
|
||||||
|
code:'Código',
|
||||||
|
contracted_at: 'Fecha contratación',
|
||||||
|
cancel:'Cancelar',
|
||||||
|
changes:'Cambios',
|
||||||
|
changelogs: {
|
||||||
|
title:'Historial de cambios',
|
||||||
|
description: 'Lista de los cambios realizados al sistema.',
|
||||||
|
},
|
||||||
|
clear: 'Limpiar',
|
||||||
|
close:"Cerrar",
|
||||||
|
confirm:'Confirmar',
|
||||||
|
copyright:'Todos los derechos reservados.',
|
||||||
|
contact:'Contacto',
|
||||||
|
create: 'Crear',
|
||||||
|
created: 'Registro creado',
|
||||||
|
created_at: 'Fecha creación',
|
||||||
|
crud: {
|
||||||
|
create: 'Nuevo registro',
|
||||||
|
edit: 'Editar registro',
|
||||||
|
destroy: 'Eliminar registro',
|
||||||
|
show: 'Más detalles',
|
||||||
|
import: 'Importación'
|
||||||
|
},
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
date: 'Fecha',
|
||||||
|
dates: {
|
||||||
|
start: 'Fecha Inicial',
|
||||||
|
end: 'Fecha Final'
|
||||||
|
},
|
||||||
|
days: {
|
||||||
|
title: 'Día',
|
||||||
|
monday: 'Lunes',
|
||||||
|
tuesday: 'Martes',
|
||||||
|
wednesday: 'Miércoles',
|
||||||
|
thursday: 'Jueves',
|
||||||
|
friday: 'Viernes',
|
||||||
|
saturday: 'Sábado',
|
||||||
|
sunday: 'Domingo'
|
||||||
|
},
|
||||||
|
delete:{
|
||||||
|
confirm: 'Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.',
|
||||||
|
title: 'Eliminar',
|
||||||
|
},
|
||||||
|
deleted:'Registro eliminado',
|
||||||
|
description:'Descripción',
|
||||||
|
details:'Detalles',
|
||||||
|
disable:'Deshabilitar',
|
||||||
|
disabled:'Deshabilitado',
|
||||||
|
done:'Hecho.',
|
||||||
|
edit:'Editar',
|
||||||
|
edited:'Registro creado',
|
||||||
|
email:{
|
||||||
|
title:'Correo',
|
||||||
|
verification:'Verificar correo'
|
||||||
|
},
|
||||||
|
employees: {
|
||||||
|
create: {
|
||||||
|
title: 'Crear empleado',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Editar empleado',
|
||||||
|
},
|
||||||
|
title: 'Empleados'
|
||||||
|
},
|
||||||
|
enable:'Habilitar',
|
||||||
|
enabled:'Habilitado',
|
||||||
|
endDate:'Fecha Fin',
|
||||||
|
event:'Evento',
|
||||||
|
files: {
|
||||||
|
excel: 'Archivo excel',
|
||||||
|
select: 'Seleccionar archivo'
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
description:'A continuación se lista la iconografía para entender el funcionamiento del sistema.',
|
||||||
|
home: 'Volver a la pagina de inicio.',
|
||||||
|
title:'Ayuda',
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
title:'Historial de acciones',
|
||||||
|
description:'Historial de acciones realizadas por los usuarios en orden cronológico.'
|
||||||
|
},
|
||||||
|
home:'Inicio',
|
||||||
|
hour:'Hora',
|
||||||
|
icon:'Icono',
|
||||||
|
import: 'Importar',
|
||||||
|
items: 'Elementos',
|
||||||
|
maternal:'Apellido materno',
|
||||||
|
message:'Mensaje',
|
||||||
|
menu:'Menú',
|
||||||
|
name:'Nombre',
|
||||||
|
noRecords:'Sin registros',
|
||||||
|
notification:'Notificación',
|
||||||
|
notifications: {
|
||||||
|
unreadClosed:'Ocultas',
|
||||||
|
readed:'Marcar como leído',
|
||||||
|
deleted:'Notificación eliminada',
|
||||||
|
description:'Notificaciones del usuario',
|
||||||
|
notFound:'Notificación no encontrada',
|
||||||
|
title:'Notificaciones',
|
||||||
|
seeAll:'Ver todas',
|
||||||
|
},
|
||||||
|
omitted:'Omitida',
|
||||||
|
password:'Contraseña',
|
||||||
|
passwordConfirmation:'Confirmar contraseña',
|
||||||
|
passwordCurrent:'Contraseña actual',
|
||||||
|
passwordReset:'Restaurar contraseña',
|
||||||
|
paternal:'Apellido paterno',
|
||||||
|
phone:'Teléfono',
|
||||||
|
photo: {
|
||||||
|
new: 'Seleccionar una nueva foto',
|
||||||
|
remove:'Remover foto',
|
||||||
|
title:'Foto',
|
||||||
|
},
|
||||||
|
plant: 'Continente',
|
||||||
|
plants: {
|
||||||
|
create: {
|
||||||
|
title: 'Crear continente',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Editar continente',
|
||||||
|
},
|
||||||
|
title: 'Continentes'
|
||||||
|
},
|
||||||
|
'production-line': 'Línea de producción',
|
||||||
|
'production-lines': {
|
||||||
|
create: {
|
||||||
|
title: 'Crear línea de producción',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Editar línea de producción',
|
||||||
|
},
|
||||||
|
title: 'Líneas de producción'
|
||||||
|
},
|
||||||
|
profile:'Perfil',
|
||||||
|
readed:'Leído',
|
||||||
|
read_at:'Fecha leído',
|
||||||
|
refresh: 'Recargar',
|
||||||
|
register: {
|
||||||
|
create: {
|
||||||
|
onError: 'Error al crear el registro',
|
||||||
|
onSuccess: 'Registro creado',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
onError: 'Error al actualizar el registro',
|
||||||
|
onSuccess: 'Registro actualizado',
|
||||||
|
},
|
||||||
|
agree:'Estoy de acuerdo con los',
|
||||||
|
privacy:'Política de Privacidad',
|
||||||
|
signUp:'Registrarme',
|
||||||
|
terms:'Términos de Servicio',
|
||||||
|
},
|
||||||
|
registers:{
|
||||||
|
title:'Registros',
|
||||||
|
empty:'Sin registros',
|
||||||
|
},
|
||||||
|
remove: 'Remover',
|
||||||
|
reports: {
|
||||||
|
description: 'Listado de reportes del sistema.',
|
||||||
|
plants: {
|
||||||
|
title: 'Reporte de empleados por continente',
|
||||||
|
description: 'Este reporte muestra el número de empleados contratados, por continente y línea de producción en función del tiempo.',
|
||||||
|
},
|
||||||
|
productionLines: {
|
||||||
|
title: 'Reporte de empleados por línea de producción',
|
||||||
|
description: 'Este reporte muestra el número de empleados contratados, por línea de producción y continente en función del tiempo.',
|
||||||
|
},
|
||||||
|
title: 'Reportes',
|
||||||
|
},
|
||||||
|
return: 'Regresar',
|
||||||
|
role:'Rol',
|
||||||
|
roles:{
|
||||||
|
create: {
|
||||||
|
title: 'Crear rol',
|
||||||
|
description: 'Este nombre sera necesario para identificar el rol en el sistema. Procura que sea algo simple.',
|
||||||
|
onSuccess: 'Rol creado exitosamente',
|
||||||
|
onError: 'Error al crear el role',
|
||||||
|
},
|
||||||
|
deleted:'Rol eliminado',
|
||||||
|
edit: {
|
||||||
|
title: 'Editar rol',
|
||||||
|
onSuccess: 'Rol actualizado exitosamente',
|
||||||
|
onError: 'Error al actualizar el role',
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
description: 'Si crees necesario, puedes actualizar el nombre del rol. No afecta a los permisos.',
|
||||||
|
},
|
||||||
|
title: 'Roles',
|
||||||
|
description: 'Gestión de roles del sistema. Puedes crear los roles con los permisos que necesites.',
|
||||||
|
permissions: {
|
||||||
|
title: 'Permisos',
|
||||||
|
description: 'Permisos del rol.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
save:'Guardar',
|
||||||
|
saved:'¡Guardado!',
|
||||||
|
search:'Buscar',
|
||||||
|
selected: 'Seleccionado',
|
||||||
|
select: 'Seleccionar',
|
||||||
|
server: {
|
||||||
|
api: {
|
||||||
|
noAvailable: 'No se encontró el servidor API.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
closed: 'Sesión cerrada',
|
||||||
|
},
|
||||||
|
setting: 'Configuración',
|
||||||
|
settings: {
|
||||||
|
assistances: {
|
||||||
|
snapshot: {
|
||||||
|
title: 'Captura instantánea del Head Count',
|
||||||
|
description: 'Esta captura genera un resumen del número de empleados contratados, por planta y por línea de producción en un momento específico.',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: 'Ajustes',
|
||||||
|
},
|
||||||
|
sex: 'Género',
|
||||||
|
shift: 'Turno',
|
||||||
|
shifts: {
|
||||||
|
create: {
|
||||||
|
title: 'Crear turno'
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Editar turno'
|
||||||
|
},
|
||||||
|
title: 'Turnos'
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
all:'Mostrar todo',
|
||||||
|
title:'Mostrar',
|
||||||
|
},
|
||||||
|
startDate:'Fecha de inicio',
|
||||||
|
status:'Estado',
|
||||||
|
system:{
|
||||||
|
title:'Núcleo de Holos',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
title: 'Meta',
|
||||||
|
total: 'Meta total'
|
||||||
|
},
|
||||||
|
technology: 'Tecnología',
|
||||||
|
technologies: {
|
||||||
|
create: {
|
||||||
|
title: 'Crear tecnología',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Editar tecnología',
|
||||||
|
},
|
||||||
|
title: 'Tecnologías'
|
||||||
|
},
|
||||||
|
terms: {
|
||||||
|
agree:'Estoy de acuerdo con los',
|
||||||
|
privacy:'Política de privacidad',
|
||||||
|
service:'Términos de servicio',
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
start: 'Hora inicial',
|
||||||
|
end: 'Hora final',
|
||||||
|
},
|
||||||
|
title: 'Título',
|
||||||
|
total: 'Total',
|
||||||
|
unknown:'Desconocido',
|
||||||
|
update:'Actualizar',
|
||||||
|
updated:'Actualizado',
|
||||||
|
updated_at:'Fecha actualización',
|
||||||
|
updateFail:'Error al actualizar',
|
||||||
|
unreaded:'No leído',
|
||||||
|
user:'Usuario',
|
||||||
|
users:{
|
||||||
|
activity: {
|
||||||
|
title: 'Actividad del usuario',
|
||||||
|
description: 'Historial de acciones realizadas por el usuario.',
|
||||||
|
},
|
||||||
|
create:{
|
||||||
|
title:'Crear usuario',
|
||||||
|
description:'Permite crear nuevos usuarios. No olvides otorgarle roles para que pueda acceder a las partes del sistema deseados.',
|
||||||
|
onSuccess:'Usuario creado',
|
||||||
|
onError:'Ocurrió un error al crear el usuario'
|
||||||
|
},
|
||||||
|
deleted:'Usuario eliminado',
|
||||||
|
remove: 'Remover usuario',
|
||||||
|
edit: {
|
||||||
|
title: 'Editar usuario'
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
description: 'Actualiza los datos del usuario.',
|
||||||
|
onError: 'Error al actualizar el usuario',
|
||||||
|
onSuccess: 'Usuario actualizado',
|
||||||
|
},
|
||||||
|
notFount:'Usuario no encontrado',
|
||||||
|
password: {
|
||||||
|
description:'Permite actualizar las contraseñas de los usuarios sobre escribiéndola.',
|
||||||
|
title:'Actualizar contraseña',
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
description:'Actualiza los roles de los usuarios, permitiendo o denegando los accesos a determinadas áreas.',
|
||||||
|
error:{
|
||||||
|
min:'Seleccionar mínimo un role'
|
||||||
|
},
|
||||||
|
title:'Roles de usuario',
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
description: 'Lista de usuarios conectados al sistema.',
|
||||||
|
title: 'Usuarios conectados',
|
||||||
|
count: 'Usuarios conectados.',
|
||||||
|
},
|
||||||
|
menu:'Menú de usuario',
|
||||||
|
select:'Seleccionar un usuario',
|
||||||
|
settings:'Ajustes del usuario',
|
||||||
|
system:'Usuarios del sistema',
|
||||||
|
title:'Usuarios',
|
||||||
|
},
|
||||||
|
version:'Versión',
|
||||||
|
welcome: 'Bienvenido',
|
||||||
|
workstation: 'Puesto de trabajo',
|
||||||
|
workstations: {
|
||||||
|
create: {
|
||||||
|
title: 'Crear puesto de trabajo',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: 'Editar puesto de trabajo',
|
||||||
|
},
|
||||||
|
title: 'Puestos de trabajos'
|
||||||
|
},
|
||||||
|
}
|
||||||
29
src/lang/i18n.js
Normal file
29
src/lang/i18n.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import en from './en.js';
|
||||||
|
import es from './es.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idioma local
|
||||||
|
*/
|
||||||
|
const locale = document.documentElement.lang;
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en,
|
||||||
|
es
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale,
|
||||||
|
fallbackLocale: locale,
|
||||||
|
messages
|
||||||
|
});
|
||||||
|
|
||||||
|
function lang(text, params = {}) {
|
||||||
|
return i18n.global.t(text, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
i18n,
|
||||||
|
lang
|
||||||
|
};
|
||||||
71
src/layouts/AppLayout.vue
Normal file
71
src/layouts/AppLayout.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import useLoader from '@Stores/Loader';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission';
|
||||||
|
|
||||||
|
import Layout from '@Holos/Layout/App.vue';
|
||||||
|
import Link from '@Holos/Skeleton/Sidebar/Link.vue';
|
||||||
|
import Section from '@Holos/Skeleton/Sidebar/Section.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const loader = useLoader()
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
title: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
loader.boot()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Layout
|
||||||
|
:title="title"
|
||||||
|
>
|
||||||
|
|
||||||
|
<template #leftSidebar>
|
||||||
|
<Section name="Principal">
|
||||||
|
<Link
|
||||||
|
icon="monitoring"
|
||||||
|
name="dashboard"
|
||||||
|
to="dashboard.index"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
icon="person"
|
||||||
|
name="profile"
|
||||||
|
to="profile.show"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Section
|
||||||
|
v-if="hasPermission('users.index')"
|
||||||
|
:name="$t('admin.title')"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
v-if="hasPermission('users.index')"
|
||||||
|
icon="people"
|
||||||
|
name="users.title"
|
||||||
|
to="admin.users.index"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="hasPermission('roles.index')"
|
||||||
|
icon="license"
|
||||||
|
name="roles.title"
|
||||||
|
to="admin.roles.index"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="hasPermission('activities.index')"
|
||||||
|
icon="event"
|
||||||
|
name="history.title"
|
||||||
|
to="admin.activities.index"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</template>
|
||||||
|
<!-- Contenido -->
|
||||||
|
<RouterView />
|
||||||
|
<!-- Fin contenido -->
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
151
src/pages/Admin/Activities/Index.vue
Normal file
151
src/pages/Admin/Activities/Index.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission';
|
||||||
|
import { useSearcher } from '@Services/Api';
|
||||||
|
import { apiTo, transl } from './Module';
|
||||||
|
|
||||||
|
import ModalController from '@Controllers/ModalController.js';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
import Header from '@Holos/PageHeader.vue';
|
||||||
|
import Paginable from '@Holos/Paginable.vue';
|
||||||
|
import Item from '@Holos/Timeline/Item.vue';
|
||||||
|
import ShowView from './Modals/Event.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const vroute = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Controladores */
|
||||||
|
const Modal = new ModalController();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const showModal = ref(Modal.showModal);
|
||||||
|
const modelModal = ref(Modal.modelModal);
|
||||||
|
|
||||||
|
const models = ref([]);
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
search: '',
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
user: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const searcher = useSearcher({
|
||||||
|
url: apiTo('index'),
|
||||||
|
filters,
|
||||||
|
onSuccess: (r) => models.value = r.models,
|
||||||
|
onError: () => models.value = []
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
filters.search = '';
|
||||||
|
filters.start_date = '';
|
||||||
|
filters.end_date = '';
|
||||||
|
|
||||||
|
searcher.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearUser = () => {
|
||||||
|
router.replace({ query: {} });
|
||||||
|
filters.user = '';
|
||||||
|
|
||||||
|
searcher.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
if(vroute.query?.user) {
|
||||||
|
filters.user = vroute.query.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
searcher.search('');
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Header :title="transl('title')">
|
||||||
|
<RouterLink v-if="filters.user && hasPermission('users.index')" :to="$view({ name: 'admin.users.index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</Header>
|
||||||
|
<p class="mt-2 text-sm">{{ transl('description') }}</p>
|
||||||
|
|
||||||
|
<div id="filters" class="grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-4">
|
||||||
|
<Input
|
||||||
|
v-model="filters.search"
|
||||||
|
title="event"
|
||||||
|
@keyup.enter="searcher.search()"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="filters.start_date"
|
||||||
|
type="date"
|
||||||
|
title="dates.start"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="filters.end_date"
|
||||||
|
title="dates.end"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
<div class="flex space-x-2 items-end">
|
||||||
|
<PrimaryButton
|
||||||
|
class="!w-full h-12"
|
||||||
|
@click="searcher.search()"
|
||||||
|
>
|
||||||
|
{{ $t('search') }}
|
||||||
|
</PrimaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
class="!w-full h-12"
|
||||||
|
@click="clearFilters()"
|
||||||
|
>
|
||||||
|
{{ $t('clear') }}
|
||||||
|
</PrimaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
v-if="filters.user"
|
||||||
|
class="!w-full h-12"
|
||||||
|
@click="clearUser()"
|
||||||
|
>
|
||||||
|
{{ $t('users.remove') }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-4">
|
||||||
|
<Paginable
|
||||||
|
:items="models"
|
||||||
|
:processing="searcher.processing"
|
||||||
|
@send-pagination="(page) => searcher.pagination(page)"
|
||||||
|
>
|
||||||
|
<template #body="{ items }">
|
||||||
|
<ol class="ml-4">
|
||||||
|
<template v-for="event in items">
|
||||||
|
<Item
|
||||||
|
:event="event"
|
||||||
|
@show="Modal.switchShowModal(event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ol>
|
||||||
|
</template>
|
||||||
|
</Paginable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ShowView
|
||||||
|
:show="showModal"
|
||||||
|
:model="modelModal"
|
||||||
|
@close="Modal.switchShowModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
59
src/pages/Admin/Activities/Modals/Event.vue
Normal file
59
src/pages/Admin/Activities/Modals/Event.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getDateTime } from '@Controllers/DateController';
|
||||||
|
|
||||||
|
import Header from '@Holos/Modal/Elements/Header.vue';
|
||||||
|
import ShowModal from '@Holos/Modal/Show.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
defineEmits([
|
||||||
|
'close',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
model: Object
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ShowModal
|
||||||
|
:show="show"
|
||||||
|
@close="$emit('close')"
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
:title="model.event"
|
||||||
|
/>
|
||||||
|
<div class="flex w-full p-4">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl text-success"
|
||||||
|
name="contact_mail"
|
||||||
|
/>
|
||||||
|
<div class="pl-3 w-full">
|
||||||
|
<p class="font-bold text-lg leading-none pb-2">
|
||||||
|
{{ $t('details') }}
|
||||||
|
</p>
|
||||||
|
<div class="text-sm">
|
||||||
|
<h4 class="font-semibold">{{ $t('event') }}:</h4>
|
||||||
|
<p>{{ model.event }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<h4 class="font-semibold">{{ $t('description') }}:</h4>
|
||||||
|
<p>{{ model.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col">
|
||||||
|
<p class="font-semibold">
|
||||||
|
{{ $t('changes') }}:
|
||||||
|
</p>
|
||||||
|
<div class="w-full text-xs p-2 border rounded-md">
|
||||||
|
<pre>{{ model.data }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<h4 class="font-semibold">{{ $t('created_at') }}:</h4>
|
||||||
|
<p>{{ getDateTime(model.created_at) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ShowModal>
|
||||||
|
</template>
|
||||||
21
src/pages/Admin/Activities/Module.js
Normal file
21
src/pages/Admin/Activities/Module.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { lang } from '@Lang/i18n';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||||
|
|
||||||
|
// Ruta API
|
||||||
|
const apiTo = (name, params = {}) => route(`admin.activities.${name}`, params)
|
||||||
|
|
||||||
|
// Ruta visual
|
||||||
|
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.activities.${name}`, params, query })
|
||||||
|
|
||||||
|
// Obtener traducción del componente
|
||||||
|
const transl = (str) => lang(`admin.activity.${str}`)
|
||||||
|
|
||||||
|
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||||
|
const can = (permission) => hasPermission(`activities.${permission}`)
|
||||||
|
|
||||||
|
export {
|
||||||
|
can,
|
||||||
|
viewTo,
|
||||||
|
apiTo,
|
||||||
|
transl
|
||||||
|
}
|
||||||
45
src/pages/Admin/Roles/Create.vue
Normal file
45
src/pages/Admin/Roles/Create.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useForm } from '@Services/Api';
|
||||||
|
import { apiTo, transl, viewTo } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import Form from './Form.vue'
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.post(apiTo('store'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.create.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader :title="transl('create.title')">
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
<Form
|
||||||
|
action="create"
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
54
src/pages/Admin/Roles/Edit.vue
Normal file
54
src/pages/Admin/Roles/Edit.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { viewTo, apiTo , transl } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import Form from './Form.vue'
|
||||||
|
|
||||||
|
/** Definiciones */
|
||||||
|
const vroute = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
id: null,
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.put(apiTo('update', { role: form.id }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.edit.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
api.get(apiTo('show', { role: vroute.params.id }), {
|
||||||
|
onSuccess: (r) => form.fill(r.model)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader :title="transl('edit.title')">
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
<Form
|
||||||
|
action="update"
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
50
src/pages/Admin/Roles/Form.vue
Normal file
50
src/pages/Admin/Roles/Form.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script setup>
|
||||||
|
import { transl } from './Module';
|
||||||
|
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'submit'
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
action: {
|
||||||
|
default: 'create',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
form: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
emit('submit')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm" v-text="transl(`${action}.description`)" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1">
|
||||||
|
<Input
|
||||||
|
v-model="form.description"
|
||||||
|
id="name"
|
||||||
|
:onError="form.errors.description"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
<div class="flex flex-col items-center justify-end space-y-4 mt-4">
|
||||||
|
<PrimaryButton
|
||||||
|
v-text="$t(action)"
|
||||||
|
:class="{ 'opacity-25': form.processing }"
|
||||||
|
:disabled="form.processing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user