ADD: Plantilla Holos (#1)

This commit is contained in:
Moisés de Jesús Cortés Castellanos 2024-12-13 16:15:01 -06:00 committed by GitHub
parent 97b85a1f65
commit b6c8a347cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 1792 additions and 1036 deletions

View File

@ -1 +1,10 @@
VITE_API_URL=http://backend.holos.test VITE_API_URL=http://backend.holos.test
VITE_BASE_URL=http://frontend.holos.test
VITE_REVERB_APP_ID=
VITE_REVERB_APP_KEY=
VITE_REVERB_APP_SECRET=
VITE_REVERB_HOST="backend.holos.test"
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=http
VITE_REVERB_ACTIVE=false

2
.gitignore vendored
View File

@ -12,6 +12,8 @@ dist
dist-ssr dist-ssr
*.local *.local
.env .env
colors.json
notes.md
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

14
auth.html Normal file
View 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>Login</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/auth.js"></script>
</body>
</html>

34
colors.json.example Normal file
View File

@ -0,0 +1,34 @@
{
"page":"#fff",
"page-t":"#000",
"page-d":"#292524",
"page-dt":"#fff",
"primary":"#374151",
"primary-t":"#fff",
"primary-d":"#1c1917",
"primary-dt":"#fff",
"secondary":"#3b82f6",
"secondary-t":"#fff",
"secondary-d":"#312e81",
"secondary-dt":"#fff",
"primary-info":"#06b6d4",
"primary-info-t":"#fff",
"primary-info-d":"#06b6d4",
"primary-info-dt":"#fff",
"secondary-info":"#06b6d4",
"secondary-info-t":"#fff",
"secondary-info-d":"#06b6d4",
"secondary-info-dt":"#fff",
"success":"#22c55e",
"success-t":"#fff",
"success-d":"#22c55e",
"success-dt":"#fff",
"danger":"#ef4444",
"danger-t":"#fff",
"danger-d":"#ef4444",
"danger-dt":"#fecaca",
"warning":"#eab308",
"warning-t":"#fff",
"warning-d":"#eab308",
"warning-dt":"#fff"
}

View File

@ -4,11 +4,11 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title> <title>Holos</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/index.js"></script>
</body> </body>
</html> </html>

69
package-lock.json generated
View File

@ -1,22 +1,27 @@
{ {
"name": "holos.frontend", "name": "notsoweb.frontend",
"version": "0.0.0", "version": "0.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "holos.frontend", "name": "notsoweb.frontend",
"version": "0.0.0", "version": "0.0.1",
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.7.8", "axios": "^1.7.8",
"laravel-echo": "^1.17.1",
"luxon": "^3.5.0",
"pinia": "^2.2.8", "pinia": "^2.2.8",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"pusher-js": "^8.4.0-rc2",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"uuid": "^11.0.3",
"vite": "^6.0.1", "vite": "^6.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^10.0.5", "vue-i18n": "^10.0.5",
"vue-multiselect": "^3.1.0",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"ziggy-js": "^2.4.1" "ziggy-js": "^2.4.1"
}, },
@ -1687,6 +1692,15 @@
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/laravel-echo": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.17.1.tgz",
"integrity": "sha512-ORWc4vDfnBj/Oe5ThZ5kYyGItRjLDqAQUyhD/7UhehUOqc+s5x9HEBjtMVludNMP6VuXw6t7Uxt8bp63kaTofg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -1708,6 +1722,15 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/luxon": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.14", "version": "0.30.14",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
@ -2143,6 +2166,15 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pusher-js": {
"version": "8.4.0-rc2",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0-rc2.tgz",
"integrity": "sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==",
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.9.7", "version": "6.9.7",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
@ -2539,6 +2571,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
@ -2576,6 +2614,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.1.tgz",
@ -2688,6 +2739,16 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vue-multiselect": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.1.0.tgz",
"integrity": "sha512-+i/fjTqFBpaay9NP+lU7obBeNaw2DdFDFs4mqhsM0aEtKRdvIf7CfREAx2o2B4XDmPrBt1r7x1YCM3BOMLaUgQ==",
"license": "MIT",
"engines": {
"node": ">= 14.18.1",
"npm": ">= 6.14.15"
}
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",

View File

@ -1,7 +1,8 @@
{ {
"name": "holos.frontend", "name": "notsoweb.frontend",
"copyright": "Notsoweb Software Inc.",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -11,13 +12,18 @@
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.7.8", "axios": "^1.7.8",
"laravel-echo": "^1.17.1",
"luxon": "^3.5.0",
"pinia": "^2.2.8", "pinia": "^2.2.8",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"pusher-js": "^8.4.0-rc2",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"uuid": "^11.0.3",
"vite": "^6.0.1", "vite": "^6.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^10.0.5", "vue-i18n": "^10.0.5",
"vue-multiselect": "^3.1.0",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"ziggy-js": "^2.4.1" "ziggy-js": "^2.4.1"
}, },

2
public/images/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,12 +0,0 @@
<script setup>
console.log(route('api.routes'));
</script>
<template>
<h1 class="text-3xl font-bold underline">
Hello world!
</h1>
{{ route('api.routes') }}
</template>

View File

@ -5,14 +5,15 @@ import { createPinia } from 'pinia'
import { createApp } from 'vue' import { createApp } from 'vue'
import { useRoute, ZiggyVue } from 'ziggy-js'; import { useRoute, ZiggyVue } from 'ziggy-js';
import { i18n, lang } from '@/lang/i18n.js'; import { i18n, lang } from '@/lang/i18n.js';
import router from '@Router/Auth'
import Notify from '@Plugins/Notify' import Notify from '@Plugins/Notify'
import TailwindScreen from '@Plugins/TailwindScreen' import TailwindScreen from '@Plugins/TailwindScreen'
import { pagePlugin } from '@Services/Page';
import App from './App.vue' import Auth from '@Holos/Layout/Auth.vue'
// Configurar axios // Configurar axios
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
// Crear instancias globales // Crear instancias globales
window.Lang = lang; window.Lang = lang;
@ -21,7 +22,7 @@ window.TwScreen = new TailwindScreen();
async function boot() { async function boot() {
try { try {
const { data } = await axios.get('/api/routes'); const { data } = await axios.get(import.meta.env.VITE_API_URL + '/api/routes');
// Iniciar rutas // Iniciar rutas
window.Ziggy = data; window.Ziggy = data;
@ -31,9 +32,11 @@ async function boot() {
alert('Failed to load routes'); alert('Failed to load routes');
} }
createApp(App) createApp(Auth)
.use(createPinia()) .use(createPinia())
.use(i18n) .use(i18n)
.use(pagePlugin)
.use(router)
.use(ZiggyVue) .use(ZiggyVue)
.mount('#app'); .mount('#app');
} }

View File

@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -1,11 +0,0 @@
<template>
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<slot name="logo" />
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
<slot />
</div>
</div>
</template>

View File

@ -1,17 +0,0 @@
<script setup>
import { Link } from '@inertiajs/vue3';
</script>
<template>
<Link :href="'/'">
<svg
class="w-16 h-16"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5" />
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5" />
</svg>
</Link>
</template>

View File

@ -5,11 +5,11 @@ import GoogleIcon from '@Shared/GoogleIcon.vue'
const props = defineProps({ const props = defineProps({
icon: String, icon: String,
fill: Boolean, fill: Boolean,
title: String,
style: { style: {
type: String, type: String,
default: 'rounded' default: 'rounded'
}, },
title: String,
type: { type: {
type: String, type: String,
default: 'button' default: 'button'
@ -24,8 +24,8 @@ const props = defineProps({
:type="type" :type="type"
> >
<GoogleIcon <GoogleIcon
:name="icon"
:fill="fill" :fill="fill"
:name="icon"
:style="style" :style="style"
/> />
</button> </button>

View File

@ -1,21 +1,21 @@
<script setup> <script setup>
import { Link } from '@inertiajs/vue3'; import { RouterLink } from 'vue-router';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Propiedades */ /** Propiedades */
defineProps({ defineProps({
to: String, icon: String,
title: String, title: String,
value: Number, to: String,
icon: String value: Number
}); });
</script> </script>
<template> <template>
<Link <RouterLink
class="relative flex-1 flex flex-col gap-2 p-4 rounded -md bg-gray-200 dark:bg-transparent dark:border" class="relative flex-1 flex flex-col gap-2 p-4 rounded -md bg-gray-200 dark:bg-transparent dark:border"
:href="to" :to="to"
> >
<label class="text-base font-semibold tracking-wider"> <label class="text-base font-semibold tracking-wider">
{{ title }} {{ title }}
@ -30,5 +30,5 @@ defineProps({
filled filled
/> />
</div> </div>
</Link> </RouterLink>
</template> </template>

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import { ref, reactive, nextTick } from 'vue'; import { ref, nextTick } from 'vue';
import { api, useForm } from '@Services/Api';
import Input from './Form/Input.vue'; import Input from './Form/Input.vue';
import DialogModal from './DialogModal.vue'; import DialogModal from './DialogModal.vue';
@ -11,62 +12,45 @@ const emit = defineEmits(['confirmed']);
defineProps({ defineProps({
title: { title: {
type: String, type: String,
default: lang('confirm'), default: Lang('confirm'),
}, },
content: { content: {
type: String, type: String,
default: lang('account.password.verify'), default: Lang('account.password.verify'),
}, },
button: { button: {
type: String, type: String,
default: lang('confirm'), default: Lang('confirm'),
}, },
}); });
const confirmingPassword = ref(false); const confirmingPassword = ref(false);
const form = reactive({ const form = useForm({
password: '', password: '',
error: '',
processing: false,
}); });
const passwordInput = ref(null); const passwordInput = ref(null);
const startConfirmingPassword = () => { const startConfirmingPassword = () => {
axios.get(route('password.confirmation')).then(response => { confirmingPassword.value = true;
if (response.data.confirmed) {
emit('confirmed');
} else {
confirmingPassword.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
}
});
}; };
const confirmPassword = () => { const confirmPassword = () => {
form.processing = true; form.post(route('user.password-confirm'), {
onSuccess: () => {
axios.post(route('password.confirm'), { closeModal();
password: form.password, nextTick(() => emit('confirmed'));
}).then(() => { },
form.processing = false; onFail: () => {
passwordInput.value.focus();
closeModal(); }
nextTick().then(() => emit('confirmed'));
}).catch(error => {
form.processing = false;
form.error = error.response.data.errors.password[0];
passwordInput.value.focus();
}); });
}; };
const closeModal = () => { const closeModal = () => {
confirmingPassword.value = false; confirmingPassword.value = false;
form.password = ''; form.password = '';
form.error = '';
}; };
</script> </script>
@ -84,12 +68,14 @@ const closeModal = () => {
<template #content> <template #content>
{{ content }} {{ content }}
{{ form }}
<div class="mt-4"> <div class="mt-4">
<Input <Input
v-model="form.password" v-model="form.password"
id="password" id="password"
type="password" type="password"
:onError="form.error" :onError="form.errors.password"
/> />
</div> </div>
</template> </template>

View File

@ -1,9 +1,9 @@
<script setup> <script setup>
import { Link } from '@inertiajs/vue3'; import { RouterLink } from 'vue-router'
defineProps({ defineProps({
as: String, as: String,
href: 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-none focus:bg-gray-100 transition'; const style = 'block px-4 py-2 text-sm leading-5 hover:bg-secondary/80 dark:hover:bg-secondary-d/80 focus:outline-none focus:bg-gray-100 transition';
@ -28,12 +28,12 @@ const style = 'block px-4 py-2 text-sm leading-5 hover:bg-secondary/80 dark:hove
<slot /> <slot />
</a> </a>
<Link <RouterLink
v-else v-else
:href="href" :to="$view({ name: to })"
:class="style" :class="style"
> >
<slot /> <slot />
</Link> </RouterLink>
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
/** Propiedades */ /** Propiedades */
defineProps({ defineProps({
onError: String onError: String | Array
}); });
</script> </script>
@ -9,6 +9,6 @@ defineProps({
<p v-if="onError" <p v-if="onError"
class="mt-1 pl-2 text-xs text-red-500 dark:text-red-300" class="mt-1 pl-2 text-xs text-red-500 dark:text-red-300"
> >
{{onError}} {{ Array.isArray(onError) ? onError[0] : onError }}
</p> </p>
</template> </template>

View File

@ -9,8 +9,8 @@ defineProps({
<template> <template>
<label v-if="title" <label v-if="title"
:for="id"
class="block text-sm font-medium text-page-t dark:text-page-dt" class="block text-sm font-medium text-page-t dark:text-page-dt"
:for="id"
> >
{{ $t(title) }} {{ $t(title) }}
<span v-if="required" <span v-if="required"

View File

@ -12,12 +12,12 @@ const emit = defineEmits([
/** Propiedades */ /** Propiedades */
const props = defineProps({ const props = defineProps({
class: String,
required: Boolean,
accept: { accept: {
default: 'image/png, image/jpeg', default: 'image/png, image/jpeg',
type: String type: String
}, },
class: String,
required: Boolean,
title: { title: {
default: 'photo.title', default: 'photo.title',
type: String type: String
@ -60,17 +60,17 @@ const updatePhotoPreview = () => {
<div class="col-span-6"> <div class="col-span-6">
<input <input
ref="photoInput" ref="photoInput"
type="file"
class="hidden" class="hidden"
type="file"
:accept="accept" :accept="accept"
:required="required" :required="required"
@change="updatePhotoPreview" @change="updatePhotoPreview"
> >
<Label <Label
class="dark:text-gray-800"
id="image_file" id="image_file"
:title="title" class="dark:text-gray-800"
:required="required" :required="required"
:title="title"
/> />
<div v-show="! photoPreview" class="mt-2"> <div v-show="! photoPreview" class="mt-2">
<!-- si existe una imagen cargada, entonces se muestra en este slot --> <!-- si existe una imagen cargada, entonces se muestra en este slot -->
@ -79,9 +79,9 @@ const updatePhotoPreview = () => {
<div v-show="photoPreview" class="mt-2"> <div v-show="photoPreview" class="mt-2">
<div v-if="fileType == 'application/pdf'" class="flex overflow-hidden max-w-full"> <div v-if="fileType == 'application/pdf'" class="flex overflow-hidden max-w-full">
<GoogleIcon <GoogleIcon
:title="$t('crud.edit')"
class="text-gray-400" class="text-gray-400"
name="picture_as_pdf" name="picture_as_pdf"
:title="$t('crud.edit')"
outline outline
/> />
<div class="ml-2 font-bold text-gray-400 flex-1"> <div class="ml-2 font-bold text-gray-400 flex-1">
@ -90,17 +90,17 @@ const updatePhotoPreview = () => {
</div> </div>
<div v-else> <div v-else>
<span <span
:class="class"
class="block rounded-lg h-40 bg-cover bg-no-repeat bg-center" class="block rounded-lg h-40 bg-cover bg-no-repeat bg-center"
:class="class"
:style="'background-image: url(\'' + photoPreview + '\');'" :style="'background-image: url(\'' + photoPreview + '\');'"
/> />
</div> </div>
</div> </div>
<SecondaryButton <SecondaryButton
v-text="$t('photo.new')"
class="mt-2 mr-2" class="mt-2 mr-2"
type="button" type="button"
v-text="$t('photo.new')"
@click.prevent="selectNewPhoto" @click.prevent="selectNewPhoto"
/> />
</div> </div>

View File

@ -20,7 +20,7 @@ const props = defineProps({
class: String, class: String,
id: String, id: String,
modelValue: Number | String, modelValue: Number | String,
onError: String, onError: String | Array,
placeholder: String, placeholder: String,
required: Boolean, required: Boolean,
title: String, title: String,
@ -32,11 +32,6 @@ const props = defineProps({
const input = ref(null); const input = ref(null);
/** Exposiciones */
defineExpose({
focus: () => input.value.focus()
});
/** Propiedades calculadas */ /** Propiedades calculadas */
const autoId = computed(() => { const autoId = computed(() => {
return (props.id) return (props.id)
@ -52,6 +47,11 @@ const autoTitle = computed(() => {
return props.id; return props.id;
}); });
/** Exposiciones */
defineExpose({
focus: () => input.value.focus()
});
/** Ciclos */ /** Ciclos */
onMounted(() => { onMounted(() => {
if (input.value.hasAttribute('autofocus')) { if (input.value.hasAttribute('autofocus')) {
@ -68,14 +68,14 @@ onMounted(() => {
:title="autoTitle" :title="autoTitle"
/> />
<input <input
:id="autoId" v-bind="$attrs"
class="input-primary"
:placeholder="placeholder"
ref="input" ref="input"
class="input-primary"
:id="autoId"
:placeholder="placeholder"
:required="required" :required="required"
:type="type" :type="type"
:value="modelValue" :value="modelValue"
v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
> >
<Error <Error

View File

@ -21,7 +21,7 @@ const props = defineProps({
id: String, id: String,
icon: String, icon: String,
modelValue: Number | String, modelValue: Number | String,
onError: String, onError: String | Array,
placeholder: String, placeholder: String,
required: Boolean, required: Boolean,
title: String, title: String,
@ -33,16 +33,13 @@ const props = defineProps({
const input = ref(null); const input = ref(null);
/** /** Propiedades computadas */
* Propiedades calculadas
*/
const autoId = computed(() => { const autoId = computed(() => {
return (props.id) return (props.id)
? props.id ? props.id
: uuidv4() : uuidv4()
}) })
/** Propiedades computadas */
const value = computed({ const value = computed({
get() { get() {
return props.modelValue return props.modelValue
@ -73,9 +70,9 @@ onMounted(() => {
/> />
<input <input
ref="input" ref="input"
v-model="value"
v-bind="$attrs" v-bind="$attrs"
class="pl-2 w-full outline-none border-none bg-transparent" class="pl-2 w-full outline-none border-none bg-transparent"
v-model="value"
:id="autoId" :id="autoId"
:placeholder="placeholder" :placeholder="placeholder"
:type="type" :type="type"

View File

@ -14,25 +14,25 @@ const emit = defineEmits([
/** Propiedades */ /** Propiedades */
const props = defineProps({ const props = defineProps({
customLabel: String, customLabel: String,
trackBy: { disabled: Boolean,
default: 'id',
type: String
},
label: { label: {
default: 'name', default: 'name',
type: String type: String
}, },
modelValue: String | Number, modelValue: String | Number,
title: String, multiple: Boolean,
onError: String | Array,
options: Object, options: Object,
onError: String,
placeholder: { placeholder: {
default: 'Buscar ...', default: 'Buscar ...',
type: String type: String
}, },
required: Boolean, required: Boolean,
multiple: Boolean, trackBy: {
disabled: Boolean default: 'id',
type: String
},
title: String,
}); });
const multiselect = ref(); const multiselect = ref();
@ -56,8 +56,8 @@ defineExpose({
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<Label <Label
:title="title"
:required="required" :required="required"
:title="title"
/> />
<VueMultiselect <VueMultiselect
ref="multiselect" ref="multiselect"
@ -69,8 +69,8 @@ defineExpose({
:close-on-select="true" :close-on-select="true"
:custom-label="customLabel" :custom-label="customLabel"
:disabled="disabled" :disabled="disabled"
:multiple="multiple"
:label="label" :label="label"
:multiple="multiple"
:options="options" :options="options"
:placeholder="placeholder" :placeholder="placeholder"
:preserve-search="true" :preserve-search="true"

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref } from 'vue';
import GoogleIcon from '@Shared/GoogleIcon.vue' import GoogleIcon from '@Shared/GoogleIcon.vue'
import Label from './Elements/Label.vue'; import Label from './Elements/Label.vue';
@ -12,13 +12,13 @@ const emit = defineEmits([
/** Propiedades */ /** Propiedades */
const props = defineProps({ const props = defineProps({
modelValue:Object|String,
class: String,
required: Boolean,
accept: { accept: {
default: 'image/png, image/jpeg', default: 'image/png, image/jpeg',
type: String type: String
}, },
class: String,
modelValue:Object|String,
required: Boolean,
title: { title: {
default: 'photo.title', default: 'photo.title',
type: String type: String
@ -78,9 +78,9 @@ const updatePhotoPreview = () => {
<div v-show="photoPreview" class="mt-2"> <div v-show="photoPreview" class="mt-2">
<div class="flex overflow-hidden max-w-full"> <div class="flex overflow-hidden max-w-full">
<GoogleIcon <GoogleIcon
:title="$t('crud.edit')"
class="text-gray-400" class="text-gray-400"
name="picture_as_pdf" name="picture_as_pdf"
:title="$t('crud.edit')"
outline outline
/> />
<div class="ml-2 font-bold text-gray-400 flex-1"> <div class="ml-2 font-bold text-gray-400 flex-1">
@ -94,9 +94,9 @@ const updatePhotoPreview = () => {
</div> </div>
</div> </div>
<SecondaryButton <SecondaryButton
v-text="$t('files.select')"
class="mt-2 mr-2" class="mt-2 mr-2"
type="button" type="button"
v-text="$t('files.select')"
@click.prevent="selectNewPhoto" @click.prevent="selectNewPhoto"
/> />
</div> </div>

View File

@ -16,15 +16,15 @@ const props = defineProps({
Boolean Boolean
] ]
}, },
disabled: Boolean,
title: { title: {
default: lang('active'), default: Lang('active'),
type: String type: String
}, },
value: { value: {
default: null, default: null,
type: String type: String
}, }
disabled: Boolean
}); });
const uuid = uuidv4() const uuid = uuidv4()
@ -34,7 +34,6 @@ const proxyChecked = computed({
get() { get() {
return props.checked; return props.checked;
}, },
set(val) { set(val) {
emit('update:checked', val); emit('update:checked', val);
}, },
@ -45,16 +44,24 @@ const proxyChecked = computed({
<div class="flex items-center"> <div class="flex items-center">
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"> <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input <input
.id="uuid"
class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
type="checkbox"
name="toggle"
:value="value"
v-model="proxyChecked" 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" :disabled="disabled"
:value="value"
/>
<label
class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
:for="uuid"
/> />
<label :for="uuid" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div> </div>
<label :for="uuid" class="text-xs text-gray-700">{{ $t(title) }}</label> <label
class="text-xs text-gray-700"
:for="uuid"
>
{{ $t(title) }}
</label>
</div> </div>
</template> </template>

View File

@ -64,15 +64,17 @@ onMounted(() => {
:title="autoTitle" :title="autoTitle"
/> />
<textarea <textarea
:id="autoId"
class="input-primary"
:placeholder="placeholder"
ref="input" ref="input"
v-bind="$attrs"
class="input-primary"
:id="autoId"
:placeholder="placeholder"
:required="required" :required="required"
:value="modelValue" :value="modelValue"
v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
></textarea> ></textarea>
<Error :onError="onError"/> <Error
:onError="onError"
/>
</div> </div>
</template> </template>

View File

@ -65,10 +65,10 @@ function add() {
itemA.value = null itemA.value = null
itemB.value = null itemB.value = null
} else { } else {
Notify.warning(lang('todo.uniqueSub.b.required', {name:lang('subclassification')})) Notify.warning(Lang('todo.uniqueSub.b.required', {name:Lang('subclassification')}))
} }
} else { } else {
Notify.warning(lang('todo.uniqueSub.a.required', {name:lang('classification')})) Notify.warning(Lang('todo.uniqueSub.a.required', {name:Lang('classification')}))
} }
} }
@ -115,18 +115,23 @@ onMounted(() => {
<p>{{ title }}</p> <p>{{ title }}</p>
<div class="w-full grid gap-2 grid-cols-2 dark:bg-primary-d/50 rounded-md"> <div class="w-full grid gap-2 grid-cols-2 dark:bg-primary-d/50 rounded-md">
<Selectable <Selectable
:title="itemATitle"
v-model="itemA" v-model="itemA"
:title="itemATitle"
:options="itemsAUnselected" :options="itemsAUnselected"
/> />
<Input <Input
:title="itemBTitle"
v-model="itemB" v-model="itemB"
:title="itemBTitle"
:type="type" :type="type"
@keyup.enter="add" @keyup.enter="add"
/> />
<div class="col-span-2 flex justify-center"> <div class="col-span-2 flex justify-center">
<PrimaryButton type="button" @click="add">{{ $t('add') }}</PrimaryButton> <PrimaryButton
type="button"
@click="add"
>
{{ $t('add') }}
</PrimaryButton>
</div> </div>
<div class="col-span-2 text-sm"> <div class="col-span-2 text-sm">
<p><b>{{ $t('items') }}</b> ({{ values.length }})</p> <p><b>{{ $t('items') }}</b> ({{ values.length }})</p>
@ -136,18 +141,17 @@ onMounted(() => {
<div class="relative rounded border border-primary/50"> <div class="relative rounded border border-primary/50">
<div class="grid gap-2 grid-cols-2 w-full items-center p-2 dark:bg-primary-d/50"> <div class="grid gap-2 grid-cols-2 w-full items-center p-2 dark:bg-primary-d/50">
<Input <Input
:title="itemATitle"
v-model="item.item.name" v-model="item.item.name"
:title="itemATitle"
disabled disabled
/> />
<Input <Input
:title="itemBTitle"
v-model="item.value" v-model="item.value"
:title="itemBTitle"
/> />
</div> </div>
<div class="absolute right-1 top-1"> <div class="absolute right-1 top-1">
<GoogleIcon <GoogleIcon
type="button"
class="btn-icon-primary" class="btn-icon-primary"
name="close" name="close"
@click="remove(index, item.item)" @click="remove(index, item.item)"

View File

@ -13,9 +13,9 @@ const props = defineProps({
}); });
/** Propiedades */ /** Propiedades */
const check = ref(false);
const filterMessages = ref(false); const filterMessages = ref(false);
/** Métodos */
const selectThisPage = () => props.inboxCtl.onSelectAll(props.items, false); const selectThisPage = () => props.inboxCtl.onSelectAll(props.items, false);
const unselectThisPage = () => props.inboxCtl.onUnselectAll(props.items) const unselectThisPage = () => props.inboxCtl.onUnselectAll(props.items)
@ -60,11 +60,11 @@ const search = url => props.searcherCtl.searchWithInboxPagination(url);
class="relative flex items-center px-0.5 space-x-0.5" class="relative flex items-center px-0.5 space-x-0.5"
> >
<button class="px-2 pt-1" @click="filterMessages = !filterMessages"> <button class="px-2 pt-1" @click="filterMessages = !filterMessages">
<GoogleIcon <GoogleIcon
class="text-xl" class="text-xl"
name="checklist" name="checklist"
outline outline
/> />
</button> </button>
<div <div
@click.away="filterMessages = false" @click.away="filterMessages = false"

View File

@ -1,18 +1,17 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
// Propiedades /** Propiedades */
const props = defineProps({ const props = defineProps({
inboxCtl: Object, //Controller inboxCtl: Object, //Controller
item: Object, item: Object,
selecteds: Object selecteds: Object
}) })
// Variables generales
const check = ref(false); const check = ref(false);
const messageHover = ref(false); const messageHover = ref(false);
// Métodos /** Métodos */
const select = () => (!check.value) const select = () => (!check.value)
? props.inboxCtl.onSelectOne(props.item) ? props.inboxCtl.onSelectOne(props.item)
: props.inboxCtl.onUnselectOne(props.item); : props.inboxCtl.onUnselectOne(props.item);
@ -35,9 +34,9 @@ const selected = computed(() => {
> >
<div class="pr-2"> <div class="pr-2">
<input <input
type="checkbox"
class="focus:ring-0 border-2 border-gray-400"
v-model="check" v-model="check"
class="focus:ring-0 border-2 border-gray-400"
type="checkbox"
@click="select" @click="select"
> >
</div> </div>

View File

@ -1,9 +1,10 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { Link } from '@inertiajs/vue3'; import { RouterLink } from 'vue-router';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Propiedades */
const props = defineProps({ const props = defineProps({
icon: String, icon: String,
counter: Number, counter: Number,
@ -15,6 +16,7 @@ const props = defineProps({
title: String, title: String,
}); });
/** Propiedades computadas */
const classes = computed(() => { const classes = computed(() => {
let status = route().current(props.to, props.toParam) let status = route().current(props.to, props.toParam)
? 'bg-secondary bg-opacity-30' ? 'bg-secondary bg-opacity-30'
@ -22,15 +24,18 @@ const classes = computed(() => {
return ` text-primary flex items-center justify-between py-1.5 px-4 rounded cursor-pointer ${status} transition` return ` text-primary flex items-center justify-between py-1.5 px-4 rounded cursor-pointer ${status} transition`
}); });
</script> </script>
<template> <template>
<li> <li>
<Link v-if="to" :href="route(to, toParam)" :class="classes"> <RouterLink
v-if="to"
:class="classes"
:to="to"
>
<span class="flex items-center space-x-2"> <span class="flex items-center space-x-2">
<GoogleIcon <GoogleIcon
class="text-lg" class="text-lg"
:name="icon" :name="icon"
outline outline
/> />
@ -41,6 +46,6 @@ const classes = computed(() => {
<span v-if="counter > 0" class="bg-primary text-gray-100 font-bold px-2 py-0.5 text-xs rounded-lg"> <span v-if="counter > 0" class="bg-primary text-gray-100 font-bold px-2 py-0.5 text-xs rounded-lg">
{{ counter }} {{ counter }}
</span> </span>
</Link> </RouterLink>
</li> </li>
</template> </template>

View File

@ -1,9 +1,10 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { Link } from '@inertiajs/vue3'; import { RouterLink } from 'vue-router';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Propiedades */
const props = defineProps({ const props = defineProps({
icon: String, icon: String,
to: String, to: String,
@ -14,15 +15,18 @@ const props = defineProps({
} }
}); });
/** Propiedades computadas */
const classes = computed(() => { const classes = computed(() => {
return `inbox-menu-button-${props.type}`; return `inbox-menu-button-${props.type}`;
}); });
</script> </script>
<template> <template>
<div class="h-16 flex items-center pr-2"> <div class="h-16 flex items-center pr-2">
<Link :href="route(to)" :class="classes"> <RouterLink
:class="classes"
:to="to"
>
<span class="flex items-center space-x-2 "> <span class="flex items-center space-x-2 ">
<GoogleIcon <GoogleIcon
class="text-lg text-white font-bold" class="text-lg text-white font-bold"
@ -33,6 +37,6 @@ const classes = computed(() => {
{{ title }} {{ title }}
</span> </span>
</span> </span>
</Link> </RouterLink>
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { onBeforeMount, onMounted } from 'vue'; import { onBeforeMount, onMounted } from 'vue';
import { Head } from '@inertiajs/vue3';
import { bootPermissions } from '@Plugins/RolePermission.js'; import { bootPermissions } from '@Plugins/RolePermission.js';
import { reloadApp } from '@Services/Page';
import useDarkMode from '@Stores/DarkMode' import useDarkMode from '@Stores/DarkMode'
import useLeftSidebar from '@Stores/LeftSidebar' import useLeftSidebar from '@Stores/LeftSidebar'
import useNotificationSidebar from '@Stores/NotificationSidebar' import useNotificationSidebar from '@Stores/NotificationSidebar'
@ -18,15 +18,12 @@ const notificationSidebar = useNotificationSidebar();
/** Propiedades */ /** Propiedades */
defineProps({ defineProps({
title: String, title: String,
titlePage: {
default: true,
type: Boolean
}
}); });
/** Ciclos */ /** Ciclos */
onBeforeMount(() => { onBeforeMount(() => {
bootPermissions() bootPermissions()
reloadApp();
}) })
onMounted(()=> { onMounted(()=> {
@ -36,7 +33,6 @@ onMounted(()=> {
</script> </script>
<template> <template>
<Head :title="title" />
<div class="flex w-full h-screen bg-page text-page-t dark:bg-page-d dark:text-page-dt"> <div class="flex w-full h-screen bg-page text-page-t dark:bg-page-d dark:text-page-dt">
<LeftSidebar <LeftSidebar
@open="leftSidebar.toggle()" @open="leftSidebar.toggle()"
@ -56,13 +52,7 @@ onMounted(()=> {
/> />
</div> </div>
<main class="flex h-full justify-center md:p-2"> <main class="flex h-full justify-center md:p-2">
<div class="mt-14 md:mt-0 w-full shadow-lg dark:shadow-sm md:dark:shadow-white h-[calc(100vh-4.5rem)] px-2 md:rounded-lg md:overflow-y-auto md:overflow-x-auto transition-colors duration-300"> <div class="mt-14 md:mt-0 w-full shadow-lg dark:shadow-sm md:dark:shadow-white h-[calc(100vh-4.5rem)] px-2 md:rounded-lg overflow-y-auto overflow-x-auto transition-colors duration-300">
<div v-if="titlePage" class="flex w-full justify-center">
<h2
class="font-bold text-xl uppercase"
v-text="title"
/>
</div>
<slot /> <slot />
</div> </div>
</main> </main>

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { Head } from '@inertiajs/vue3'; import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
import useDarkMode from '@Stores/DarkMode' import useDarkMode from '@Stores/DarkMode'
import Logo from '@Holos/Logo.vue'
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '@Holos/Button/Icon.vue'
import Logo from '@Holos/Logo.vue';
/** Definidores */ /** Definidores */
const darkMode = useDarkMode() const darkMode = useDarkMode()
@ -14,17 +14,14 @@ defineProps({
title: String title: String
}) })
/** /** Ciclos */
* Ciclos
*/
onMounted(() => { onMounted(() => {
darkMode.boot() darkMode.boot()
}); });
</script> </script>
<template> <template>
<Head :title="title" /> <div class="h-screen flex bg-primary dark:bg-primary-d">
<div class="h-screen flex">
<div <div
class="relative flex w-full lg:w-full justify-around items-center with-transition" 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}" :class="{'app-bg-light':darkMode.isLight,'app-bg-dark':darkMode.isDark}"
@ -32,13 +29,13 @@ onMounted(() => {
<header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white"> <header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white">
<div> <div>
<IconButton v-if="darkMode.isLight" <IconButton v-if="darkMode.isLight"
:title="$t('app.theme.light')"
icon="light_mode" icon="light_mode"
:title="$t('app.theme.light')"
@click="darkMode.applyDark()" @click="darkMode.applyDark()"
/> />
<IconButton v-else <IconButton v-else
:title="$t('app.theme.dark')"
icon="dark_mode" icon="dark_mode"
:title="$t('app.theme.dark')"
@click="darkMode.applyLight()" @click="darkMode.applyLight()"
/> />
</div> </div>
@ -46,27 +43,27 @@ onMounted(() => {
<div class="flex w-full flex-col items-center justify-center space-y-2"> <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"> <div class="flex space-x-2 items-center justify-start text-white">
<Logo <Logo
class="text-lg inline-flex" class="text-lg inline-flex"
/> />
</div> </div>
<main class="bg-white/10 w-full backdrop-blur-sm text-white px-4 py-8 rounded-md max-w-80"> <main class="bg-white/10 w-full backdrop-blur-sm text-white px-4 py-4 rounded-md max-w-80">
<slot /> <RouterView />
</main> </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-sm text-white transition-colors duration-global"> <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> <div>
<span> <span>
&copy;2024 {{ $page.props.copyright }} &copy;2024 {{ APP_COPYRIGHT }}
</span> </span>
</div> </div>
<div> <div>
<span> <span>
Versión {{ $page.version }} Versión {{ APP_VERSION }}
</span> </span>
</div> </div>
</footer> </footer>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { Head } from '@inertiajs/vue3'; import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
import useDarkMode from '@Stores/DarkMode' import useDarkMode from '@Stores/DarkMode'
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '../Button/Icon.vue'
import Logo from '@Holos/Logo.vue'; import Logo from '../Logo.vue';
/** Definidores */ /** Definidores */
const darkMode = useDarkMode() const darkMode = useDarkMode()
@ -14,16 +14,13 @@ defineProps({
title: String title: String
}) })
/** /** Ciclos */
* Ciclos
*/
onMounted(() => { onMounted(() => {
darkMode.boot() darkMode.boot()
}); });
</script> </script>
<template> <template>
<Head :title="title" />
<div class="min-h-screen flex"> <div class="min-h-screen flex">
<div <div
class="relative flex w-full lg:w-full justify-around items-start with-transition" class="relative flex w-full lg:w-full justify-around items-start with-transition"
@ -32,13 +29,13 @@ onMounted(() => {
<header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white"> <header class="absolute top-0 flex w-full h-8 px-1 items-center justify-end text-white">
<div> <div>
<IconButton v-if="darkMode.isLight" <IconButton v-if="darkMode.isLight"
:title="$t('app.theme.light')"
icon="light_mode" icon="light_mode"
:title="$t('app.theme.light')"
@click="darkMode.applyDark()" @click="darkMode.applyDark()"
/> />
<IconButton v-else <IconButton v-else
:title="$t('app.theme.dark')"
icon="dark_mode" icon="dark_mode"
:title="$t('app.theme.dark')"
@click="darkMode.applyLight()" @click="darkMode.applyLight()"
/> />
</div> </div>
@ -50,19 +47,19 @@ onMounted(() => {
/> />
</div> </div>
<main class="bg-white/10 w-full mx-auto sm:max-w-2xl backdrop-blur-sm text-white px-4 py-8 rounded-md"> <main class="bg-white/10 w-full mx-auto sm:max-w-2xl backdrop-blur-sm text-white px-4 py-8 rounded-md">
<slot /> <slot />
</main> </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-sm text-white transition-colors duration-global"> <footer class="absolute bottom-0 flex w-full h-8 px-4 items-center justify-between bg-primary dark:bg-primary-d backdrop-blur-sm text-white transition-colors duration-global">
<div> <div>
<span> <span>
&copy;2024 {{ $page.props.copyright }} &copy;{{ APP_COPYRIGHT }}
</span> </span>
</div> </div>
<div> <div>
<span> <span>
Versión {{ $page.version }} Versión {{ APP_VERSION }}
</span> </span>
</div> </div>
</footer> </footer>

View File

@ -1,9 +1,18 @@
<script setup> <script setup>
import { Link } from '@inertiajs/vue3'; import { useRouter } from 'vue-router';
/** Definidores */
const router = useRouter();
/** Métodos */
const home = () => router.push(view({ name: 'index' }));
</script> </script>
<template> <template>
<Link :href="'/'" class="flex w-full justify-center items-center space-x-2"> <div
class="flex w-full justify-center items-center space-x-2 cursor-pointer"
@click="home"
>
<img src="/images/logo.png" class="h-20" /> <img src="/images/logo.png" class="h-20" />
</Link> </div>
</template> </template>

View File

@ -13,7 +13,7 @@ defineEmits([
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
title: { title: {
default: lang('delete.title'), default: Lang('delete.title'),
type: String type: String
} }
}); });
@ -42,12 +42,12 @@ const props = defineProps({
<div class="space-x-2"> <div class="space-x-2">
<slot name="buttons" /> <slot name="buttons" />
<DangerButton <DangerButton
@click="$emit('destroy')"
v-text="$t('delete.title')" v-text="$t('delete.title')"
@click="$emit('destroy')"
/> />
<SecondaryButton <SecondaryButton
@click="$emit('close')"
v-text="$t('cancel')" v-text="$t('cancel')"
@click="$emit('close')"
/> />
</div> </div>
</template> </template>

View File

@ -13,7 +13,7 @@ const emit = defineEmits([
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
title: { title: {
default: lang('edit'), default: Lang('edit'),
type: String type: String
} }
}); });
@ -38,12 +38,12 @@ const props = defineProps({
<div class="space-x-2"> <div class="space-x-2">
<slot name="buttons" /> <slot name="buttons" />
<PrimaryButton <PrimaryButton
@click="$emit('update')"
v-text="$t('update')" v-text="$t('update')"
@click="$emit('update')"
/> />
<SecondaryButton <SecondaryButton
@click="$emit('close')"
v-text="$t('close')" v-text="$t('close')"
@click="$emit('close')"
/> />
</div> </div>
</template> </template>

View File

@ -14,7 +14,7 @@ const props = defineProps({
editable: Boolean, editable: Boolean,
show: Boolean, show: Boolean,
title: { title: {
default: lang('details'), default: Lang('details'),
type: String type: String
} }
}); });
@ -38,14 +38,13 @@ const props = defineProps({
<template #footer> <template #footer>
<div class="space-x-2"> <div class="space-x-2">
<slot name="buttons" /> <slot name="buttons" />
<PrimaryButton <PrimaryButton v-if="editable"
v-if="editable"
@click="$emit('edit')"
v-text="$t('update')" v-text="$t('update')"
@click="$emit('edit')"
/> />
<SecondaryButton <SecondaryButton
@click="$emit('close')"
v-text="$t('close')" v-text="$t('close')"
@click="$emit('close')"
/> />
</div> </div>
</template> </template>

View File

@ -1,13 +1,13 @@
<script setup> <script setup>
import { router } from '@inertiajs/vue3'; import { api } from '@Services/Api.js';
import DestroyModal from '../Destroy.vue'; import DestroyModal from '../Destroy.vue';
import Header from '../Elements/Header.vue'; import Header from '../Elements/Header.vue';
/** Eventos */ /** Eventos */
const emit = defineEmits([ const emit = defineEmits([
'close', 'close',
'switchModal' 'update'
]); ]);
/** Propiedades */ /** Propiedades */
@ -18,15 +18,14 @@ const props = defineProps({
}); });
/** Métodos */ /** Métodos */
const destroy = (id) => router.delete(props.to(id), { const destroy = (id) => api.delete(props.apiTo(id), {
preserveScroll: true,
onSuccess: () => { onSuccess: () => {
props.model.pop; Notify.success(Lang('deleted'));
Notify.success(lang('deleted'));
emit('close'); emit('close');
emit('update');
}, },
onError: () => { onError: () => {
Notify.info(lang('notFound')); Notify.info(Lang('notFound'));
emit('close'); emit('close');
} }
}); });
@ -39,8 +38,8 @@ const destroy = (id) => router.delete(props.to(id), {
@destroy="destroy(model.id)" @destroy="destroy(model.id)"
> >
<Header <Header
:title="model.name"
:subtitle="model.full_last_name" :subtitle="model.full_last_name"
:title="model.name"
/> />
</DestroyModal> </DestroyModal>
</template> </template>

View File

@ -1,74 +0,0 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import GoogleIcon from '@/components/Shared/GoogleIcon.vue'
const emit = defineEmits([
'close'
])
const props = defineProps({
item: Object,
})
const isOpen = ref(false)
const close = () => {
isOpen.value = false
setTimeout(() => {
emit('close')
}, 500)
}
const typeClasses = computed(() => {
let nameClass = 'w-64 rounded-md text-white p-2';
switch (props.item.type) {
case 'info':
nameClass += ' bg-blue-500'
break;
case 'success':
nameClass += ' bg-green-500'
break;
case 'warning':
nameClass += ' bg-yellow-500'
break;
case 'error':
nameClass += ' bg-red-500'
break;
default:
nameClass += ' bg-blue-500'
break;
}
return nameClass
})
onMounted(() => {
isOpen.value = true
})
</script>
<template>
<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="isOpen" :class="typeClasses">
<div class="flex justify-between items-center">
<h4 class="font-bold text-sm truncate">{{ item.title }}</h4>
<GoogleIcon
class="cursor-pointer"
name="close"
@click="close()"
/>
</div>
<h4 class="text-sm ">{{ item.message }}</h4>
</div>
</Transition>
</template>

View File

@ -1,22 +1,31 @@
<script setup> <script setup>
import { Link } from '@inertiajs/vue3'; import { RouterLink } from 'vue-router';
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '@Holos/Button/Icon.vue'
defineProps({
title: String
});
</script> </script>
<template> <template>
<div class="flex w-full justify-end py-[0.31rem] border-y-2 border-page-t dark:border-page-dt"> <div v-if="title" class="flex w-full justify-center">
<div id="buttons" class="flex items-center space-x-2 text-sm py-0.5"> <h2
<slot /> class="font-bold text-xl uppercase"
<Link :href="route('dashboard.index')"> 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-2 text-sm">
<RouterLink :to="$view({ name: 'index' })">
<IconButton <IconButton
:title="$t('home')" :title="$t('home')"
class="text-white" class="text-white"
icon="home" icon="home"
filled filled
/> />
</Link> </RouterLink>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,29 +1,38 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { Link } from '@inertiajs/vue3';
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '@Holos/Button/Icon.vue'
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Eventos */
const emit = defineEmits([ const emit = defineEmits([
'search' 'search'
]); ]);
const query = ref(''); /** Propiedades */
const props = defineProps({ const props = defineProps({
title: String,
placeholder: { placeholder: {
default: lang('search'), default: Lang('search'),
type: String type: String
} }
}) })
const query = ref('');
/** Métodos */
const search = () => { const search = () => {
emit('search', query.value); emit('search', query.value);
} }
</script> </script>
<template> <template>
<div class="flex w-full justify-between items-center border-y-2 border-page-t dark:border-page-dt"> <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>
<div class="relative py-1 z-0"> <div class="relative py-1 z-0">
<div @click="search" class="absolute inset-y-0 right-2 flex items-center pl-3 cursor-pointer text-gray-700 hover:scale-110 hover:text-danger"> <div @click="search" class="absolute inset-y-0 right-2 flex items-center pl-3 cursor-pointer text-gray-700 hover:scale-110 hover:text-danger">
@ -47,14 +56,14 @@ const search = () => {
</div> </div>
<div class="flex items-center space-x-2 text-sm" id="buttons"> <div class="flex items-center space-x-2 text-sm" id="buttons">
<slot /> <slot />
<Link :href="route('dashboard.index')"> <RouterLink :to="$view({name:'index'})">
<IconButton <IconButton
:title="$t('home')" :title="$t('home')"
class="text-white" class="text-white"
icon="home" icon="home"
filled filled
/> />
</Link> </RouterLink>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,15 +1,13 @@
<script setup> <script setup>
import { onMounted} from 'vue'; import { logout } from '@Services/Page';
import { router } from '@inertiajs/vue3';
import { resetPermissions } from '@/Plugins/RolePermission';
import useDarkMode from '@Stores/DarkMode' import useDarkMode from '@Stores/DarkMode'
import useLeftSidebar from '@Stores/LeftSidebar' import useLeftSidebar from '@Stores/LeftSidebar'
import useNotificationSidebar from '@Stores/NotificationSidebar' import useNotificationSidebar from '@Stores/NotificationSidebar'
import useNotifier from '@Stores/Notifier' import useNotifier from '@Stores/Notifier'
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
import Dropdown from '../Dropdown.vue'; import Dropdown from '../Dropdown.vue';
import DropdownLink from '../DropdownLink.vue'; import DropdownLink from '../DropdownLink.vue';
// import NotificationLink from '.NotificationLink.vue';
/** Eventos */ /** Eventos */
const emit = defineEmits([ const emit = defineEmits([
@ -21,61 +19,45 @@ const darkMode = useDarkMode()
const leftSidebar = useLeftSidebar() const leftSidebar = useLeftSidebar()
const notificationSidebar = useNotificationSidebar() const notificationSidebar = useNotificationSidebar()
const notifier = useNotifier() const notifier = useNotifier()
// Métodos
const logout = () => {
resetPermissions()
router.post(route('logout'), {}, {
onBefore: () => {
}
});
};
/** Ciclos */
onMounted(()=>{
});
</script> </script>
<template> <template>
<header <header
class="fixed px-2 w-[calc(100vw)] bg-transparent transition-all duration-300 z-50" 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}" :class="{'md:w-[calc(100vw-16rem)]':leftSidebar.isOpened,'md:w-[calc(100vw)]':!leftSidebar.isClosed}"
> >
<div class="my-2 flex px-6 items-center justify-between h-[2.75rem] rounded-lg bg-primary dark:bg-primary-d text-white z-20 "> <div class="my-2 flex px-6 items-center justify-between h-[2.75rem] rounded-lg bg-primary dark:bg-primary-d text-white z-20 ">
<GoogleIcon <GoogleIcon
:title="$t('menu')" class="text-2xl mt-1 z-50"
class="text-2xl mt-1 z-50" name="list"
name="list" :title="$t('menu')"
@click="emit('open')" @click="emit('open')"
outline outline
/> />
<div class="flex w-fit justify-end items-center h-14 header-right"> <div class="flex w-fit justify-end items-center h-14 header-right">
<ul class="flex items-center space-x-2"> <ul class="flex items-center space-x-2">
<li class="flex items-center"> <li class="flex items-center">
<GoogleIcon <GoogleIcon
:title="$t('notifications.title')"
class="text-xl mt-1" class="text-xl mt-1"
name="notifications" name="notifications"
:title="$t('notifications.title')"
@click="notificationSidebar.toggle()" @click="notificationSidebar.toggle()"
/> />
<span class="text-xs">{{ notifier.counter }}</span> <span class="text-xs">{{ notifier.counter }}</span>
</li> </li>
<li v-if="darkMode.isDark"> <li v-if="darkMode.isDark">
<GoogleIcon <GoogleIcon
:title="$t('notifications.title')"
class="text-xl mt-1" class="text-xl mt-1"
name="light_mode" name="light_mode"
:title="$t('notifications.title')"
@click="darkMode.applyLight()" @click="darkMode.applyLight()"
/> />
</li> </li>
<li v-else> <li v-else>
<GoogleIcon <GoogleIcon
:title="$t('notifications.title')"
class="text-xl mt-1" class="text-xl mt-1"
name="dark_mode" name="dark_mode"
:title="$t('notifications.title')"
@click="darkMode.applyDark()" @click="darkMode.applyDark()"
/> />
</li> </li>
@ -85,27 +67,23 @@ onMounted(()=>{
<template #trigger> <template #trigger>
<div class="flex space-x-4"> <div class="flex space-x-4">
<button <button
v-if="$page.props.jetstream.managesProfilePhotos"
:title="$t('users.menu')"
class="flex items-center space-x-4 text-sm border-2 border-transparent rounded-full focus:outline-none transition" class="flex items-center space-x-4 text-sm border-2 border-transparent rounded-full focus:outline-none transition"
:title="$t('users.menu')"
> >
<img <img
class="h-8 w-8 rounded-full object-cover" class="h-8 w-8 rounded-full object-cover"
:alt="$page.props.auth.user.name" :alt="$page.user.name"
:src="$page.props.auth.user.profile_photo_url" :src="$page.user.profile_photo_url"
> >
</button> </button>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="text-center block px-4 py-2 text-sm border-b truncate"> <div class="text-center block px-4 py-2 text-sm border-b truncate">
{{ $page.props.auth.user.name }} {{ $page.user.name }}
</div> </div>
<DropdownLink :href="route('profile.show')"> <DropdownLink to="profile.show">
{{$t('profile')}} {{$t('profile')}}
</DropdownLink>
<DropdownLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')">
API Tokens
</DropdownLink> </DropdownLink>
<div class="border-t border-gray-100" /> <div class="border-t border-gray-100" />
<form @submit.prevent="logout"> <form @submit.prevent="logout">

View File

@ -1,67 +0,0 @@
<script setup>
import { computed } from 'vue';
import { Link } from '@inertiajs/vue3';
import GoogleIcon from '@Shared/GoogleIcon.vue';
const props = defineProps({
as: String,
href: String,
icon: {
default: 'notifications_active',
type: String
},
readAt: String
});
const classes = computed(()=> {
return 'inline-flex space-x-2 w-full px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition';
});
const readed = computed(()=> {
return (props.readAt)
? 'text-primary'
: 'text-warning';
});
</script>
<template>
<div>
<button
v-if="as == 'button'"
:class="classes"
type="button"
>
<GoogleIcon
:class="readed"
:name="icon"
/>
<slot />
</button>
<a
v-else-if="as =='a'"
:class="classes"
:href="href"
>
<GoogleIcon
:class="readed"
:name="icon"
/>
<slot />
</a>
<Link
v-else
:class="classes"
:href="href"
>
<GoogleIcon
class="text-primary hover:text-secondary"
:class="readed"
:name="icon"
/>
<slot />
</Link>
</div>
</template>

View File

@ -1,17 +1,22 @@
<script setup> <script setup>
import { APP_VERSION, APP_COPYRIGHT } from '@/config.js'
import useLeftSidebar from '@Stores/LeftSidebar' import useLeftSidebar from '@Stores/LeftSidebar'
import Logo from '@Holos/Logo.vue'; import Logo from '@Holos/Logo.vue';
/** Definidores */
const leftSidebar = useLeftSidebar()
/** Eventos */
const emit = defineEmits(['open']); const emit = defineEmits(['open']);
/** Propiedades */
const props = defineProps({ const props = defineProps({
sidebar: Boolean sidebar: Boolean
}); });
const leftSidebar = useLeftSidebar()
const year = (new Date).getFullYear(); const year = (new Date).getFullYear();
</script> </script>
<template> <template>
@ -37,10 +42,10 @@ const year = (new Date).getFullYear();
</div> </div>
<div class="mb-4 px-5 space-y-1"> <div class="mb-4 px-5 space-y-1">
<p class="block text-center text-xs"> <p class="block text-center text-xs">
&copy {{year}} {{$page.props.copyright}} &copy {{year}} {{ APP_COPYRIGHT }}
</p> </p>
<p class="text-center text-xs text-yellow-500 cursor-pointer"> <p class="text-center text-xs text-yellow-500 cursor-pointer">
V{{$page.version}} V{{ APP_VERSION }}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,12 +1,13 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { Link } from '@inertiajs/vue3'; import { RouterLink, useRoute } from 'vue-router';
import useLeftSidebar from '@/Stores/LeftSidebar';
import GoogleIcon from '@/Components/Shared/GoogleIcon.vue'; import useLeftSidebar from '@Stores/LeftSidebar';
import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Definidores */ /** Definidores */
const leftSidebar = useLeftSidebar(); const leftSidebar = useLeftSidebar();
const vroute = useRoute();
/** Propiedades */ /** Propiedades */
const props = defineProps({ const props = defineProps({
@ -16,7 +17,7 @@ const props = defineProps({
}); });
const classes = computed(() => { const classes = computed(() => {
let status = route().current(props.to) let status = props.to === vroute.name
? 'bg-secondary/30 dark:bg-secondary-d/30 border-secondary dark:border-secondary-d' ? 'bg-secondary/30 dark:bg-secondary-d/30 border-secondary dark:border-secondary-d'
: 'border-transparent'; : 'border-transparent';
@ -32,8 +33,11 @@ const closeSidebar = () => {
<template> <template>
<li @click="closeSidebar()"> <li @click="closeSidebar()">
<Link :href="route(to)" :class="classes"> <RouterLink
<span :class="classes"
:to="$view({name:to})"
>
<span
v-if="icon" v-if="icon"
class="inline-flex justify-center items-center ml-4 mr-2" class="inline-flex justify-center items-center ml-4 mr-2"
> >
@ -45,11 +49,10 @@ const closeSidebar = () => {
</span> </span>
<span <span
v-if="name" v-if="name"
v-text="$t(name)"
class="text-sm tracking-wide truncate" class="text-sm tracking-wide truncate"
> />
{{$t(name)}}
</span>
<slot /> <slot />
</Link> </RouterLink>
</li> </li>
</template> </template>

View File

@ -6,10 +6,8 @@ import useNotifier from '@Stores/Notifier'
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
import Item from './Notification/Item.vue'; import Item from './Notification/Item.vue';
/** /** Definidores */
* Definidores const notifier = useNotifier();
*/
const notifier = useNotifier();
const notificationSidebar = useNotificationSidebar() const notificationSidebar = useNotificationSidebar()
/** Eventos */ /** Eventos */
@ -20,9 +18,7 @@ const props = defineProps({
sidebar: Boolean sidebar: Boolean
}); });
/** /** Ciclos */
* Ciclos
*/
onMounted(() => { onMounted(() => {
notifier.boot(); notifier.boot();
}); });

View File

@ -1,12 +1,12 @@
<script setup> <script setup>
import { getDateTime } from '@Controllers/DateController'; import { getDateTime } from '@Controllers/DateController';
import useNotifier from '@Stores/Notifier'; import useNotifier from '@Stores/Notifier';
import GoogleIcon from '@Shared/GoogleIcon.vue';
/** Definidores */ /** Definidores */
const notifier = useNotifier(); const notifier = useNotifier();
import GoogleIcon from '@Shared/GoogleIcon.vue'; /** Propiedades */
defineProps({ defineProps({
notification: Object, notification: Object,
}); });
@ -30,35 +30,41 @@ defineProps({
<div class="w-10 space-y-0"> <div class="w-10 space-y-0">
<template v-if="notification.user"> <template v-if="notification.user">
<div class="w-10 h-10 bg-transparent rounded-full flex items-center justify-center"> <div class="w-10 h-10 bg-transparent rounded-full flex items-center justify-center">
<img <img v-if="notification.user"
v-if="notification.user" class="rounded-full object-cover"
class="rounded-full object-cover" :alt="notification.user.name"
:alt="notification.user.name" :src="notification.user.profile_photo_url"
:src="notification.user.profile_photo_url"
> >
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="w-10 h-10 bg-secondary dark:bg-secondary-d rounded-xl flex items-center justify-center"> <div class="w-10 h-10 bg-secondary dark:bg-secondary-d rounded-xl flex items-center justify-center">
<img <img v-if="notification.user"
v-if="notification.user" class="rounded-full object-cover"
class="rounded-full object-cover" :alt="notification.user.name"
:alt="notification.user.name" :src="notification.user.profile_photo_url"
:src="notification.user.profile_photo_url"
> >
<GoogleIcon <GoogleIcon v-else
v-else name="tag"
name="tag" class="text-white text-2xl"
class="text-white text-2xl"
/> />
</div> </div>
</template> </template>
</div> </div>
<div class="ml-3 w-full"> <div class="ml-3 w-full">
<div class="text-sm font-medium truncate">{{ notification.data.title }}</div> <div
<div v-if="notification.user" class="text-xs text-gray-400 truncate">~ {{ `${notification.user.name} ${notification.user.paternal}` }} </div> v-text="notification.data.title"
<div v-else class="text-xs text-gray-400 truncate">~ {{ $t('system') }} </div> class="text-sm font-medium 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')"
class="text-xs text-gray-400 truncate"
/>
</div> </div>
</div> </div>
</li> </li>
</template> </template>

View File

@ -1,6 +1,9 @@
<script setup> <script setup>
import useRightSidebar from '@Stores/RightSidebar' import useRightSidebar from '@Stores/RightSidebar'
/** Definidores */
const rightSidebar = useRightSidebar()
/** Eventos */ /** Eventos */
const emit = defineEmits(['open']); const emit = defineEmits(['open']);
@ -8,9 +11,6 @@ const emit = defineEmits(['open']);
const props = defineProps({ const props = defineProps({
sidebar: Boolean sidebar: Boolean
}); });
/** Definidores */
const rightSidebar = useRightSidebar()
</script> </script>
<template> <template>

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
defineProps({ /** Propiedades */
name: String const props = defineProps({
}); name: String
});
</script> </script>
<template> <template>

View File

@ -13,7 +13,7 @@ const props = defineProps({
</script> </script>
<template> <template>
<section class="py-4"> <section class="pb-2">
<div class="w-full overflow-hidden rounded-md shadow-lg"> <div class="w-full overflow-hidden rounded-md shadow-lg">
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<table class="w-full"> <table class="w-full">

View File

@ -1,22 +1,18 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
/** /** Propiedades */
* Propiedades
*/
const props = defineProps({ const props = defineProps({
name: String, name: String,
fill: Boolean, fill: Boolean,
title: String,
style: { style: {
type: String, type: String,
default: 'rounded' // outlined, rounded, sharp default: 'rounded' // outlined, rounded, sharp
} },
title: String
}) })
/** /** Propiedades computadas */
* Propiedades computadas
*/
const classes = computed(() => { const classes = computed(() => {
return props.fill return props.fill
? `font-google-icon-${props.style}-fill` ? `font-google-icon-${props.style}-fill`
@ -26,9 +22,9 @@ const classes = computed(() => {
<template> <template>
<span <span
v-text="name"
class="material-symbols cursor-pointer" class="material-symbols cursor-pointer"
:class="classes" :class="classes"
translate="no" translate="no"
v-text="name"
/> />
</template> </template>

11
src/config.js Normal file
View 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
}

View File

@ -1,6 +1,6 @@
import { DateTime } from "luxon"; import { DateTime } from "luxon";
/* Obtiene la fecha actual en el formato deseado */ // Obtener fecha en formato deseado
function getDate(value = null) { function getDate(value = null) {
const date = (value) const date = (value)
? DateTime.fromISO(value) ? DateTime.fromISO(value)
@ -9,7 +9,7 @@ function getDate(value = null) {
return date.toLocaleString(DateTime.DATE_MED); return date.toLocaleString(DateTime.DATE_MED);
} }
/* Obtiene la horaa actual en el formato deseado */ // Obtener hora en formato deseado
function getTime(value = null) { function getTime(value = null) {
const date = (value) const date = (value)
? DateTime.fromISO(value) ? DateTime.fromISO(value)
@ -18,7 +18,7 @@ function getTime(value = null) {
return date.toLocaleString(DateTime.TIME_24_SIMPLE); return date.toLocaleString(DateTime.TIME_24_SIMPLE);
} }
/** Obtener fecha y hora */ // Obtener fecha y hora
function getDateTime(value) { function getDateTime(value) {
const date = (value) const date = (value)
? DateTime.fromISO(value) ? DateTime.fromISO(value)
@ -27,4 +27,8 @@ function getDateTime(value) {
return date.toLocaleString(DateTime.DATETIME_SHORT); return date.toLocaleString(DateTime.DATETIME_SHORT);
} }
export { getDate, getTime, getDateTime } export {
getDate,
getDateTime,
getTime
}

View File

@ -1,5 +1,4 @@
import { ref } from 'vue'; import { ref } from 'vue';
import axios from 'axios';
/** /**
* Controla la generación de impresiones * Controla la generación de impresiones

View File

@ -1,15 +1,18 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { router } from '@inertiajs/vue3'; import { api } from '@Services/Api.js';
/** /**
* Controlador simple de las bandejas * Controlador simple de las bandejas
*/ */
class SearcherController class SearcherController
{ {
route = '';
params = {};
query = ref(''); query = ref('');
constructor(route, params) { constructor({ route, model, params = {} }) {
this.route = route; this.route = route;
this.model = ref(model);
this.params = params; this.params = params;
} }
@ -18,44 +21,64 @@ class SearcherController
*/ */
search = (q = '', params) => { search = (q = '', params) => {
this.query.value = q; this.query.value = q;
router.get(this._getRoute(), { api.get(this._getRoute(), {
q, params: {
...params q: this.query.value,
}, {preserveState: true}); ...params
},
onSuccess: (r) => {
this.model.value = r.users;
}
});
}; };
/** /**
* Paginación simple * Paginación simple
*/ */
withPagination = (page, params) => { withPagination = (page, params) => {
router.get(this._getRoute(), { api.get(this._getRoute(), {
page, params: {
...params page,
}, {preserveState: true}); ...params
},
onSuccess: (r) => {
this.model.value = r.users;
}
});
} }
/** /**
* Búsqueda con paginación en tablas * Búsqueda con Paginación en tablas
*/ */
searchWithPagination = (page, params) => { searchWithPagination = (page, params) => {
router.get(page, { api.get(page, {
q: this.query.value, params: {
...params q: this.query.value,
}, {preserveState: true}); ...params
},
onSuccess: (r) => {
this.model.value = r.users;
}
});
} }
/** /**
* Búsqueda con páginación en bandejas * Búsqueda con Paginación en bandejas
*/ */
searchWithInboxPagination = (page, params) => { searchWithInboxPagination = (page, params) => {
router.get(page, { api.get(page, {
q: this.query.value, params: {
...params q: this.query.value,
}, {preserveState: true}); ...params
},
onSuccess: (r) => {
this.model.value = r.users;
}
});
} }
/** /**
* Obtiene la ruta segun los parametros * Obtiene la ruta según los parámetros
*/ */
_getRoute = () => { _getRoute = () => {
return (this.params) return (this.params)

94
src/css/app.css Normal file
View File

@ -0,0 +1,94 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.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-md font-medium border border-transparent text-xs text-white uppercase tracking-widest hover:opacity-90 focus:outline-none active:saturate-150 disabled:opacity-25 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-md 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;
}
.input-primary {
@apply w-full p-2 border rounded-md outline-0 bg-transparent
}
.nav-item {
@apply p-1
}
.table-item {
@apply px-2 border text-sm;
}
.table-actions {
@apply flex justify-center items-center space-x-2;
}
nav a.router-link-active {
@apply bg-secondary/30 dark:bg-secondary-d/30 border-secondary dark:border-secondary-d
}
table {
@apply w-full;
}
table>thead {
@apply bg-primary text-white;
}
table>thead>tr {
@apply text-base font-semibold tracking-wide text-left text-white bg-primary dark:bg-primary-d uppercase border-b divide-indigo-50 divide-x;
}
table>thead>tr>th {
@apply px-2 border border-white text-sm;
}
.with-transition {
@apply transition-all duration-300
}
.with-color-transition {
@apply transition-colors duration-500
}
/**
* Switch
*/
.toggle-checkbox:checked {
@apply right-0 border-slate-500
}
.toggle-checkbox:checked + .toggle-label {
@apply bg-slate-500
}

View File

@ -1,3 +1,5 @@
@tailwind base; @import './icons.css';
@tailwind components; @import './notifications.css';
@tailwind utilities; @import "vue-multiselect/dist/vue-multiselect.css";
@import './multiselect.css';
@import './app.css';

92
src/css/icons.css Normal file
View File

@ -0,0 +1,92 @@
/*
* 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 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 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');
}
.material-symbols{
font-weight: normal;
font-style: normal;
font-size: 20spx;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}

57
src/css/multiselect.css Normal file
View File

@ -0,0 +1,57 @@
.multiselect {
@apply dark:text-white min-h-8;
}
.multiselect__input,
.multiselect__single {
@apply bg-white dark:bg-primary dark:text-white
}
.multiselect__input::placeholder {
@apply text-gray-300;
}
.multiselect__tag {
@apply dark:bg-green-800;
}
.multiselect__tag-icon::after {
@apply text-gray-300;
}
.multiselect__tag-icon:focus::after,
.multiselect__tag-icon:hover::after {
@apply text-white;
}
.multiselect__tags {
@apply dark:bg-primary min-h-8 border-0 border-b border-b-slate-400 pt-1 with-color-transition;
}
.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-gray-100 dark:bg-primary;
}
.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
View 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;
}
}

52
src/index.js Normal file
View File

@ -0,0 +1,52 @@
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 TailwindScreen from '@Plugins/TailwindScreen'
import { pagePlugin } from '@Services/Page';
import App from '@Layouts/AppLayout.vue'
import { view } from '@Services/Page';
// Configurar axios
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Crear instancias globales
window.axios = axios;
window.Lang = lang;
window.Notify = new Notify();
window.TwScreen = new TailwindScreen();
async function boot() {
try {
const { data } = await axios.get(import.meta.env.VITE_API_URL + '/api/routes');
// Iniciar rutas
window.Ziggy = data;
window.route = useRoute();
window.view = view;
} catch (error) {
console.error(error);
alert('Failed to load routes');
}
if(import.meta.env.VITE_REVERB_ACTIVE === 'true') {
await import('@Services/Broadcast')
}
createApp(App)
.use(createPinia())
.use(i18n)
.use(pagePlugin)
.use(router)
.use(ZiggyVue)
.mount('#app');
}
// Iniciar aplicación
boot();

View File

@ -300,6 +300,9 @@ export default {
search:'Buscar', search:'Buscar',
selected: 'Seleccionado', selected: 'Seleccionado',
select: 'Seleccionar', select: 'Seleccionar',
session: {
closed: 'Sesión cerrada',
},
setting: 'Configuración', setting: 'Configuración',
settings: { settings: {
assistances: { assistances: {
@ -393,6 +396,7 @@ export default {
title:'Usuarios', title:'Usuarios',
}, },
version:'Versión', version:'Versión',
welcome: '<b>Bienvenido</b> {name}.',
workstation: 'Puesto de trabajo', workstation: 'Puesto de trabajo',
workstations: { workstations: {
create: { create: {

View File

@ -18,6 +18,11 @@ const i18n = createI18n({
messages messages
}); });
const lang = (text) => i18n.global.t(text); function lang(text) {
return i18n.global.t(text);
}
export {i18n, lang}; export {
i18n,
lang
};

View File

@ -1,17 +1,11 @@
<script setup> <script setup>
import { hasPermission } from '@Plugins/RolePermission.js'; import Layout from '@Holos/Layout/App.vue';
import Layout from '@Holos/Layout/AppLayout.vue';
import Link from '@Holos/Skeleton/Sidebar/Link.vue'; import Link from '@Holos/Skeleton/Sidebar/Link.vue';
import Section from '@Holos/Skeleton/Sidebar/Section.vue'; import Section from '@Holos/Skeleton/Sidebar/Section.vue';
/** Propiedades */ /** Propiedades */
defineProps({ defineProps({
title: String, title: String,
titlePage: {
default: true,
type: Boolean
}
}); });
</script> </script>
@ -19,40 +13,31 @@ defineProps({
<template> <template>
<Layout <Layout
:title="title" :title="title"
:titlePage="titlePage"
> >
<template #leftSidebar> <template #leftSidebar>
<Section name="Principal"> <Section name="Principal">
<Link <Link
icon="monitoring" icon="monitoring"
name="dashboard" name="dashboard"
to="dashboard.index" to="index"
/> />
</Section> <Link
<Section :name="$t('account.title')"> icon="person"
<Link name="profile"
icon="manage_accounts"
name="profile"
to="profile.show" to="profile.show"
/> />
<Link
icon="notifications"
:name="$t('notifications.title')"
to="notifications.index"
/>
</Section> </Section>
<Section :name="$t('admin.title')"> <Section :name="$t('admin.title')">
<Link <Link
v-if="hasPermission('users.index')"
icon="people" icon="people"
name="users.title" name="users.title"
to="admin.users.index" to="admin.users.index"
/> />
</Section> </Section>
</template> </template>
<!-- Contenido --> <!-- Contenido -->
<slot /> <RouterView />
<!-- Fin contenido --> <!-- Fin contenido -->
</Layout> </Layout>
</template> </template>

View File

@ -1,22 +1,20 @@
<script setup> <script setup>
import { Link, useForm } from '@inertiajs/vue3'; import { onMounted, ref } from 'vue';
import { goTo, transl } from './Module'; import { useRouter } from 'vue-router';
import { api, useForm } from '@Services/Api';
import { viewTo } from './Module';
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '@Holos/Button/Icon.vue'
import Input from '@Holos/Form/Input.vue'; import Input from '@Holos/Form/Input.vue';
import Selectable from '@Holos/Form/Selectable.vue'; import Selectable from '@Holos/Form/Selectable.vue';
import PageHeader from '@Holos/PageHeader.vue'; import PageHeader from '@Holos/PageHeader.vue';
import DashboardLayout from '@Layouts/AppLayout.vue';
import Form from './Form.vue' import Form from './Form.vue'
/** Definidores */
const router = useRouter();
/** Propiedades */ /** Propiedades */
defineProps({
roles: Object
});
const form = useForm({ const form = useForm({
_id: null,
name: '', name: '',
paternal: '', paternal: '',
maternal: '', maternal: '',
@ -26,30 +24,41 @@ const form = useForm({
roles: [] roles: []
}); });
const roles = ref([]);
/** Métodos */ /** Métodos */
function submit() { function submit() {
form.transform(data => ({ form.transform(data => ({
...data, ...data,
roles: data.roles.map(role => role.id) roles: data.roles.map(role => role.id)
})).post(route(goTo('store')), { })).post(apiTo('store'), {
onSuccess: () => Notify.success(lang('register.create.onSuccess')), onSuccess: () => {
onError: () => Notify.error(lang('register.create.onError')), Notify.success(Lang('register.create.onSuccess'))
onFinish: () => form.reset('password') router.push(viewTo({ name: 'index' }));
}
}) })
} }
/** Ciclos */
onMounted(() => {
api.get(route('system.roles'), {
onSuccess: (r) => {
roles.value = r.roles;
}
});
})
</script> </script>
<template> <template>
<DashboardLayout :title="transl('create.title')">
<PageHeader> <PageHeader>
<Link :href="route(goTo('index'))"> <RouterLink :to="viewTo({ name: 'index' })">
<IconButton <IconButton
class="text-white" class="text-white"
icon="arrow_back" icon="arrow_back"
:title="$t('return')" :title="$t('return')"
filled filled
/> />
</Link> </RouterLink>
</PageHeader> </PageHeader>
<Form <Form
action="create" action="create"
@ -72,5 +81,4 @@ function submit() {
multiple multiple
/> />
</Form> </Form>
</DashboardLayout>
</template> </template>

View File

@ -1,52 +1,58 @@
<script setup> <script setup>
import { Link, useForm } from '@inertiajs/vue3'; import { onMounted } from 'vue';
import { goTo, transl } from './Module'; import { RouterLink, useRoute, useRouter } from 'vue-router';
import { api, useForm } from '@Services/Api';
import { viewTo, apiTo } from './Module';
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '@Holos/Button/Icon.vue'
import PageHeader from '@Holos/PageHeader.vue'; import PageHeader from '@Holos/PageHeader.vue';
import Layout from '@/Layouts/AppLayout.vue';
import Form from './Form.vue' import Form from './Form.vue'
/** Propiedades */ /** Definiciones */
const props = defineProps({ const vroute = useRoute();
model: Object, const router = useRouter();
});
/** Propiedades */ /** Propiedades */
const form = useForm({ const form = useForm({
name: props.model.name, id: null,
paternal: props.model.paternal, name: '',
maternal: props.model.maternal, paternal: '',
email: props.model.email, maternal: '',
phone: props.model.phone, email: '',
phone: '',
}); });
/** Métodos */ /** Métodos */
function submit() { function submit() {
form.put(route(goTo('update'), {user:props.model.id}), { form.put(apiTo('update', { user: form.id }), {
onSuccess: () => Notify.success(lang('register.edit.onSuccess')), onSuccess: () => {
onError: () => Notify.error(lang('register.edit.onError')), Notify.success(Lang('register.edit.onSuccess'))
onFinish: () => form.reset('password') router.push(viewTo({ name: 'index' }));
},
}) })
} }
onMounted(() => {
api.get(apiTo('show', { user: vroute.params.id }), {
onSuccess: (r) => form.fill(r.user)
});
})
</script> </script>
<template> <template>
<Layout :title="transl('edit.title')">
<PageHeader> <PageHeader>
<Link :href="route(goTo('index'))"> <RouterLink :to="viewTo({ name: 'index' })">
<IconButton <IconButton
class="text-white" class="text-white"
icon="arrow_back" icon="arrow_back"
:title="$t('return')" :title="$t('return')"
filled filled
/> />
</Link> </RouterLink>
</PageHeader> </PageHeader>
<Form <Form
action="update" action="update"
:form="form" :form="form"
@submit="submit" @submit="submit"
/> />
</Layout>
</template> </template>

View File

@ -68,9 +68,9 @@ function submit() {
<slot /> <slot />
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4"> <div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
<PrimaryButton <PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
v-text="$t(action)" v-text="$t(action)"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
/> />
</div> </div>
</form> </form>

View File

@ -1,40 +1,53 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { Link } from '@inertiajs/vue3'; import { can, apiTo, viewTo } from './Module'
import { transl, can, goTo } from './Module' import { api } from '@Services/Api';
import ModalController from '@/Controllers/ModalController.js'; import ModalController from '@Controllers/ModalController.js';
import SearcherController from '@/Controllers/SearcherController.js'; import SearcherController from '@Controllers/SearcherController.js';
import IconButton from '@Holos/Button/Icon.vue' import IconButton from '@Holos/Button/Icon.vue'
import DestroyView from '@Holos/Modal/Template/Destroy.vue'; import DestroyView from '@Holos/Modal/Template/Destroy.vue';
import SearcherHead from '@Holos/Searcher.vue'; import SearcherHead from '@Holos/Searcher.vue';
import Table from '@Holos/Table.vue'; import Table from '@Holos/Table.vue';
import DashboardLayout from '@Layouts/AppLayout.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue';
import ShowView from './Modals/Show.vue'; import ShowView from './Modals/Show.vue';
/** Eventos */
const props = defineProps({
models: Object
});
/** Controladores */ /** Controladores */
const Modal = new ModalController(); const Modal = new ModalController();
const Searcher = new SearcherController(goTo('index'));
/** Propiedades */ /** Propiedades */
const destroyModal = ref(Modal.destroyModal); const destroyModal = ref(Modal.destroyModal);
const showModal = ref(Modal.showModal); const showModal = ref(Modal.showModal);
const modelModal = ref(Modal.modelModal); const modelModal = ref(Modal.modelModal);
const models = ref([]);
const Searcher = new SearcherController({
route: 'users.index',
model: models
});
/** Métodos */
function load() {
api.get(apiTo('index'), {
onSuccess: (r) => models.value = r.users
});
}
/** Ciclos */
onMounted(() => load());
</script> </script>
<template> <template>
<DashboardLayout :title="transl('system')"> <div>
<SearcherHead @search="Searcher.search"> <SearcherHead
<Link :title="$t('users.title')"
v-if="can('create')" @search="Searcher.search"
:href="route(goTo('create'))" >
<RouterLink
v-if="can('create')"
:to="viewTo({ name: 'create' })"
> >
<IconButton <IconButton
class="text-white" class="text-white"
@ -42,7 +55,7 @@ const modelModal = ref(Modal.modelModal);
:title="$t('crud.create')" :title="$t('crud.create')"
filled filled
/> />
</Link> </RouterLink>
</SearcherHead> </SearcherHead>
<div class="pt-2 w-full"> <div class="pt-2 w-full">
<Table <Table
@ -92,10 +105,10 @@ const modelModal = ref(Modal.modelModal);
@click="Modal.switchShowModal(model)" @click="Modal.switchShowModal(model)"
outline outline
/> />
<Link <RouterLink
v-if="can('edit')" v-if="can('edit')"
class="h-fit" class="h-fit"
:href="route(goTo('edit'), model.id)" :to="viewTo({ name: 'edit', params: { id: model.id } })"
> >
<GoogleIcon <GoogleIcon
class="btn-icon" class="btn-icon"
@ -103,7 +116,7 @@ const modelModal = ref(Modal.modelModal);
:title="$t('crud.edit')" :title="$t('crud.edit')"
outline outline
/> />
</Link> </RouterLink>
<GoogleIcon <GoogleIcon
v-if="can('destroy')" v-if="can('destroy')"
class="btn-icon" class="btn-icon"
@ -112,17 +125,17 @@ const modelModal = ref(Modal.modelModal);
@click="Modal.switchDestroyModal(model)" @click="Modal.switchDestroyModal(model)"
outline outline
/> />
<Link <RouterLink
v-if="can('settings')" v-if="can('settings')"
class="h-fit" class="h-fit"
:href="route('admin.users.settings', model.id)" :to="viewTo({ name: 'settings', params: { id: model.id } })"
> >
<GoogleIcon <GoogleIcon
class="btn-icon" class="btn-icon"
name="settings" name="settings"
:title="$t('setting')" :title="$t('setting')"
/> />
</Link> </RouterLink>
</div> </div>
</td> </td>
</tr> </tr>
@ -151,9 +164,10 @@ const modelModal = ref(Modal.modelModal);
v-if="can('destroy')" v-if="can('destroy')"
:model="modelModal" :model="modelModal"
:show="destroyModal" :show="destroyModal"
:to="(user) => route(goTo('destroy'), {user})" :to="(user) => apiTo('destroy', { user })"
@close="Modal.switchDestroyModal" @close="Modal.switchDestroyModal"
@update="load"
/> />
</DashboardLayout> </div>
</template> </template>

View File

@ -1,15 +1,21 @@
import { lang } from '@/Lang/i18n'; import { lang } from '@Lang/i18n';
import { hasPermission } from '@Plugins/RolePermission.js'; import { hasPermission } from '@Plugins/RolePermission.js';
// Obtener ruta // Ruta API
const goTo = (route) => `admin.users.${route}` const apiTo = (name, params = {}) => route(`users.${name}`, params)
// Ruta visual
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.users.${name}`, params, query })
// Obtener traducción del componente // Obtener traducción del componente
const transl = (str) => lang(`users.${str}`) const transl = (str) => lang(`users.${str}`)
// Determina si un usuario puede hacer algo no en base a los permisos // Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`users.${permission}`) const can = (permission) => hasPermission(`users.${permission}`)
export { export {
can, can,
goTo, viewTo,
apiTo,
transl transl
} }

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useForm } from '@inertiajs/vue3'; import { api, useForm } from '@Services/Api';
import { goTo, transl } from './Module'; import { transl } from './Module';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import FormSection from '@Holos/FormSection.vue'; import FormSection from '@Holos/FormSection.vue';
@ -9,25 +9,38 @@ import Selectable from '@Holos/Form/Selectable.vue';
/** Propiedades */ /** Propiedades */
const props = defineProps({ const props = defineProps({
role: Object, userId: String
roles: Object,
user: Object
}); });
const form = useForm({ const form = useForm({
roles: props.role roles: []
}); });
const roles = ref([]);
/** Métodos */ /** Métodos */
function updateProfileInformation() { function updateProfileInformation() {
form.transform(data => ({ form.transform(data => ({
roles: data.roles.map(role => role.id) roles: data.roles.map(role => role.id)
})).post(route(goTo('sync-roles'), {user:props.user.id}), { })).put(apiTo('roles', { user: props.userId }), {
preserveScroll: true, onSuccess: () => Notify.success(Lang('roles.edit.onSuccess')),
onSuccess: () => Notify.success(lang('roles.edit.onSuccess')), onError: () => Notify.error(Lang('roles.edit.onError'))
onError: () => Notify.error(lang('roles.edit.onError'))
}); });
}; };
/** Ciclos */
onMounted(() => {
api.get(route('system.roles'), {
onSuccess: (r) => roles.value = r.roles
});
api.get(apiTo('roles', { user: props.userId }), {
onSuccess: (r) => {
console.log(r);
form.roles = r.roles
}
});
});
</script> </script>
<template> <template>
@ -43,7 +56,7 @@ function updateProfileInformation() {
<Selectable <Selectable
v-model="form.roles" v-model="form.roles"
label="description" label="description"
title="Roles" title="roles.title"
:options="roles" :options="roles"
multiple multiple
/> />

View File

@ -1,58 +1,59 @@
<script setup> <script setup>
import { goTo, transl } from './Module'; import { onMounted, ref } from 'vue';
import { Link } from '@inertiajs/vue3'; import { RouterLink, useRoute } from 'vue-router';
import { api } from '@Services/Api';
import { viewTo, to } from './Module';
import IconButton from '@Holos/Button/Icon.vue'; import IconButton from '@Holos/Button/Icon.vue';
import PageHeader from '@Holos/PageHeader.vue'; import PageHeader from '@Holos/PageHeader.vue';
import SectionBorder from '@Holos/SectionBorder.vue'; import SectionBorder from '@Holos/SectionBorder.vue';
import DashboardLayout from '@Layouts/AppLayout.vue';
import Roles from './Roles.vue'; import Roles from './Roles.vue';
import UpdatePassword from './UpdatePassword.vue'; import UpdatePassword from './UpdatePassword.vue';
/** /** Definiciones */
* Propiedades const vroute = useRoute();
*/
defineProps({ /** Propiedades */
role: Object, const user = ref();
roles: Object,
user: Object /** Ciclos */
}); onMounted(() => {
api.get(apiTo('show', { user: vroute.params.id }), {
onSuccess: (r) => user.value = r.user
});
})
</script> </script>
<template> <template>
<DashboardLayout :title="transl('settings')">
<PageHeader> <PageHeader>
<Link :href="route(goTo('index'))"> <RouterLink :to="viewTo({ name: 'index' })">
<IconButton <IconButton
class="text-white" class="text-white"
icon="arrow_back" icon="arrow_back"
:title="$t('return')" :title="$t('return')"
outline outline
/> />
</Link> </RouterLink>
</PageHeader> </PageHeader>
<div class="flex w-full pt-2"> <div class="flex w-full pt-2">
<div class="w-full text-center p-2 bg-primary dark:bg-primary-d border-b rounded-lg"> <div class="w-full text-center p-2 bg-primary dark:bg-primary-d border-b rounded-lg">
<p class="pt-2 text-lg font-bold text-gray-50"> <p class="pt-2 text-lg font-bold text-gray-50">
{{ user.name }} {{ user?.name }}
</p> </p>
<p class="text-sm text-gray-100"> <p class="text-sm text-gray-100">
{{ user.full_last_name }} {{ user?.full_last_name }}
</p> </p>
</div> </div>
</div> </div>
<div class="w-full mt-12 space-y-4"> <div class="w-full mt-12 space-y-4">
<UpdatePassword <UpdatePassword
:user="user" :userId="vroute.params.id"
/> />
<SectionBorder /> <SectionBorder />
<Roles <Roles
:role="role" :userId="vroute.params.id"
:roles="roles"
:user="user"
/> />
<SectionBorder /> <SectionBorder />
</div> </div>
</DashboardLayout>
</template> </template>

View File

@ -1,30 +1,29 @@
<script setup> <script setup>
import { goTo, transl } from './Module'; import { apiTo, transl } from './Module';
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import Input from '@Holos/Form/Input.vue'; import Input from '@Holos/Form/Input.vue';
import FormSection from '@Holos/FormSection.vue'; import FormSection from '@Holos/FormSection.vue';
/** Propiedades */
const props = defineProps({ const props = defineProps({
user: Object userId: String
}); });
const form = useForm({ const form = useForm({
_method: 'POST',
password: '', password: '',
password_confirmation: '', password_confirmation: '',
}); });
/** Métodos */
const updateProfileInformation = () => { const updateProfileInformation = () => {
form.post(route(goTo('password'), props.user.id), { form.put(apiTo('password', { user: props.userId }), {
errorBag: 'updateProfileInformation',
preserveScroll: true,
onSuccess: () => { onSuccess: () => {
Notify.success(lang('account.password.updated')); Notify.success(Lang('account.password.updated'));
form.reset(); form.reset();
}, },
onError: () => Notify.error(lang('updateFail')) onError: () => Notify.error(Lang('updateFail'))
}); });
}; };
</script> </script>
@ -40,19 +39,19 @@ const updateProfileInformation = () => {
<template #form> <template #form>
<div class="col-span-6 sm:col-span-4 space-y-4"> <div class="col-span-6 sm:col-span-4 space-y-4">
<Input <Input
v-model="form.password"
id="password" id="password"
title="account.password.new" title="account.password.new"
type="password" type="password"
v-model="form.password"
:onError="form.errors.password" :onError="form.errors.password"
autocomplete="off" autocomplete="off"
required required
/> />
<Input <Input
v-model="form.password_confirmation"
icon="password" icon="password"
id="passwordConfirmation" id="passwordConfirmation"
type="password" type="password"
v-model="form.password_confirmation"
:onError="form.errors.password_confirmation" :onError="form.errors.password_confirmation"
required required
/> />

View File

@ -1,10 +1,13 @@
<script setup> <script setup>
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api';
import { useRouter } from 'vue-router';
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
import PrimaryButton from '@Holos/Button/Primary.vue' import PrimaryButton from '@Holos/Button/Primary.vue'
import Layout from '@Holos/Layout/AuthLayout.vue'
/** Definidores */
const router = useRouter()
/** Propiedades */
defineProps({ defineProps({
status: String, status: String,
}); });
@ -13,41 +16,46 @@ const form = useForm({
email: '', email: '',
}); });
/** Métodos */
const submit = () => { const submit = () => {
form.post(route('password.email')); form.post(route('password.email'));
}; };
</script> </script>
<template> <template>
<Layout :title="$t('auth.forgotPassword.title')"> <div class="mb-4 text-sm text-justify">
<div class="mb-4 text-sm text-justify"> {{ $t('auth.forgotPassword.description') }}
{{ $t('auth.forgotPassword.description') }} </div>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
{{ status }}
</div>
<form @submit.prevent="submit">
<Input
icon="mail"
id="email"
type="email"
v-model="form.email"
:onError="form.errors.email"
:placeholder="$t('email.title')"
/>
<div class="flex flex-col gap-2 items-center justify-end mt-4">
<PrimaryButton
class="!w-full"
:class="{ 'opacity-25': form.processing }"
type="submit"
:disabled="form.processing"
>
{{ $t('auth.forgotPassword.sendLink') }}
</PrimaryButton>
<PrimaryButton
class="!w-full"
type="button"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="router.push($view({ name: 'index' }))"
>
{{ $t('auth.login') }}
</PrimaryButton>
</div> </div>
</form>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
{{ status }}
</div>
<form @submit.prevent="submit">
<Input
icon="mail"
id="email"
type="email"
v-model="form.email"
:onError="form.errors.email"
:placeholder="$t('email.title')"
/>
<div class="flex items-center justify-end mt-4">
<PrimaryButton
class="!w-full"
:class="{ 'opacity-25': form.processing }"
type="submit"
:disabled="form.processing"
>
{{ $t('auth.forgotPassword.sendLink') }}
</PrimaryButton>
</div>
</form>
</Layout>
</template> </template>

View File

@ -1,9 +1,10 @@
<script setup> <script setup>
import { Link, useForm } from '@inertiajs/vue3'; import { onMounted } from 'vue';
import { defineApiToken, defineCsrfToken, hasToken, useForm } from '@Services/Api.js'
import { defineUser } from '@Services/Page';
import PrimaryButton from '@Holos/Button/Primary.vue' import PrimaryButton from '@Holos/Button/Primary.vue'
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
import Layout from '@Holos/Layout/AuthLayout.vue'
/** Propiedades */ /** Propiedades */
defineProps({ defineProps({
@ -13,52 +14,58 @@ defineProps({
const form = useForm({ const form = useForm({
email: '', email: '',
password: '', password: ''
remember: false,
}); });
/** Métodos */ /** Métodos */
const login = () => { const login = () => {
form.transform(data => ({ form.post(route('auth.login'), {
...data, onSuccess: (res) => {
remember: form.remember ? 'on' : '', defineApiToken(res.token)
})).post(route('login'), { defineUser(res.user)
onFinish: () => form.reset('password'), defineCsrfToken(res.csrf)
location.replace('/')
}
}); });
}; };
/** Ciclos */
onMounted(() => {
if (hasToken()) {
location.replace('/')
}
})
</script> </script>
<template> <template>
<Layout :title="$t('auth.login')"> <form @submit.prevent="login">
<form @submit.prevent="login"> <Input
<Input icon="mail"
icon="mail" id="email"
id="email" type="email"
type="email" v-model="form.email"
v-model="form.email" :onError="form.errors.email"
:onError="form.errors.email" :placeholder="$t('email.title')"
:placeholder="$t('email.title')" />
/> <Input
<Input v-model="form.password"
v-model="form.password" icon="password"
icon="password" id="password"
id="password" type="password"
type="password" :onError="form.errors.password"
:onError="form.errors.password" :placeholder="$t('password')"
:placeholder="$t('password')" />
/> <PrimaryButton class="!w-full">
<PrimaryButton class="!w-full"> {{ $t('auth.login') }}
{{ $t('auth.login') }} </PrimaryButton>
</PrimaryButton> <div class="flex justify-end mt-4">
<div class="flex justify-end mt-4"> <RouterLink
<Link class="text-sm ml-2 hover:text-blue-200 cursor-pointer hover:-translate-y-1 duration-500 transition-all"
v-if="canResetPassword" :to="$view({ name: 'forgot-password' })"
class="text-sm ml-2 hover:text-blue-200 cursor-pointer hover:-translate-y-1 duration-500 transition-all" >
:href="route('password.request')" {{ $t('auth.forgotPassword.ask') }}
> </RouterLink>
{{ $t('auth.forgotPassword.ask') }} </div>
</Link> </form>
</div>
</form>
</Layout>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { Head, Link, useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api.js'
import Checkbox from '@Holos/Checkbox.vue'; import Checkbox from '@Holos/Checkbox.vue';
import InputLabel from '@Holos/InputLabel.vue'; import InputLabel from '@Holos/InputLabel.vue';
@ -8,17 +8,19 @@ import PrimaryButton from '@Holos/Button/Primary.vue';
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
import Layout from '@Holos/Layout/AuthLayout.vue' import Layout from '@Holos/Layout/AuthLayout.vue'
/** Propiedades */
const form = useForm({ const form = useForm({
name: '', name: '',
paternal: '', paternal: '',
maternal: '', maternal: '',
phone: '', phone: '',
email: '', email: '',
password: '', password: '',
password_confirmation: '', password_confirmation: '',
terms: false, terms: false,
}); });
/** Métodos */
const submit = () => { const submit = () => {
form.post(route('register'), { form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'), onFinish: () => form.reset('password', 'password_confirmation'),
@ -87,7 +89,7 @@ const submit = () => {
/> />
<div v-if="$page.props.jetstream.hasTermsAndPrivacyPolicyFeature" class="mt-4"> <div class="mt-4">
<InputLabel for="terms"> <InputLabel for="terms">
<div class="flex items-center"> <div class="flex items-center">
<Checkbox id="terms" v-model:checked="form.terms" name="terms" required /> <Checkbox id="terms" v-model:checked="form.terms" name="terms" required />

View File

@ -1,10 +1,10 @@
<script setup> <script setup>
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api.js'
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import Input from '@Holos/Form/InputWithIcon.vue' import Input from '@Holos/Form/InputWithIcon.vue'
import Layout from '@Holos/Layout/AuthLayout.vue'
/** Propiedades */
const props = defineProps({ const props = defineProps({
email: String, email: String,
token: String, token: String,
@ -17,10 +17,11 @@ const form = useForm({
password_confirmation: '', password_confirmation: '',
}); });
/** Métodos */
const submit = () => { const submit = () => {
form.post(route('password.update'), { form.post(route('password.update'), {
onSuccess: () => { onSuccess: () => {
Notify.success(lang('auth.reset.success')); Notify.success(Lang('auth.reset.success'));
}, },
onFinish: () => form.reset('password', 'password_confirmation'), onFinish: () => form.reset('password', 'password_confirmation'),
}); });
@ -28,38 +29,35 @@ const submit = () => {
</script> </script>
<template> <template>
<Layout :title="$t('auth.reset.title')"> <form @submit.prevent="submit">
<form @submit.prevent="submit"> <Input
<Input icon="mail"
icon="mail" id="email"
id="email" type="email"
type="email" v-model="form.email"
v-model="form.email" :onError="form.errors.email"
:onError="form.errors.email" :placeholder="$t('email.title')"
:placeholder="$t('email.title')" />
/> <Input
<Input icon="password"
icon="password" id="password"
id="password" type="password"
type="password" v-model="form.password"
v-model="form.password" :onError="form.errors.password"
:onError="form.errors.password" :placeholder="$t('password')"
:placeholder="$t('password')" />
/> <Input
<Input icon="password"
icon="password" id="passwordConfirmation"
id="passwordConfirmation" type="password"
type="password" v-model="form.password_confirmation"
v-model="form.password_confirmation" :onError="form.errors.password_confirmation"
:onError="form.errors.password_confirmation" :placeholder="$t('passwordConfirmation')"
:placeholder="$t('passwordConfirmation')" />
/> <div class="flex items-center justify-end mt-4">
<PrimaryButton class="!w-full" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<div class="flex items-center justify-end mt-4"> {{ $t('account.password.update') }}
<PrimaryButton class="!w-full" :class="{ 'opacity-25': form.processing }" :disabled="form.processing"> </PrimaryButton>
{{ $t('account.password.update') }} </div>
</PrimaryButton> </form>
</div>
</form>
</Layout>
</template> </template>

View File

@ -5,17 +5,21 @@ import { Link, useForm } from '@inertiajs/vue3';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import Layout from '@Holos/Layout/AuthLayout.vue'; import Layout from '@Holos/Layout/AuthLayout.vue';
/** Propiedades */
const props = defineProps({ const props = defineProps({
status: String, status: String,
}); });
const form = useForm({}); const form = useForm({});
/** Propiedades computadas */
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
/** Métodos */
const submit = () => { const submit = () => {
form.post(route('verification.send')); form.post(route('verification.send'));
}; };
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
</script> </script>
<template> <template>

View File

@ -1,14 +1,9 @@
<script setup> <script setup>
import AppLayout from '@Layouts/AppLayout.vue'; import PageHeader from '@Holos/PageHeader.vue';
</script> </script>
<template> <template>
<AppLayout title="Dashboard"> <PageHeader title="Dashboard" />
<template #header> <p v-html="$t('welcome', { name: $page.user.name })"></p>
<h2 class="font-semibold text-xl text-gray-800 leading-tight"> </template>
{{ $t('dashboard') }}
</h2>
</template>
</AppLayout>
</template>

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { Link } from '@inertiajs/vue3'; import { Link } from '@inertiajs/vue3';
import { transl, can, goTo } from './Module' import { transl, can, viewTo } from './Module'
import ModalController from '@Controllers/ModalController.js'; import ModalController from '@Controllers/ModalController.js';
import SearcherController from '@Controllers/SearcherController.js'; import SearcherController from '@Controllers/SearcherController.js';
@ -18,7 +18,7 @@ import GoogleIcon from '@/Components/Shared/GoogleIcon.vue';
/** Definidores */ /** Definidores */
const inboxCtl = new InboxController(); const inboxCtl = new InboxController();
const searcherCtl = new SearcherController(goTo('index')); const searcherCtl = new SearcherController(viewTo('index'));
/** Eventos */ /** Eventos */
const props = defineProps({ const props = defineProps({

View File

@ -2,7 +2,7 @@ import { lang } from '@/Lang/i18n';
import { hasPermission } from '@Plugins/RolePermission.js'; import { hasPermission } from '@Plugins/RolePermission.js';
// Obtener ruta // Obtener ruta
const goTo = (route) => `admin.users.${route}` const viewTo = (route) => `admin.users.${route}`
// Obtener traducción del componente // Obtener traducción del componente
const transl = (str) => lang(`notifications.${str}`) const transl = (str) => lang(`notifications.${str}`)
// Determina si un usuario puede hacer algo no en base a los permisos // Determina si un usuario puede hacer algo no en base a los permisos
@ -10,6 +10,6 @@ const can = (permission) => hasPermission(`users.${permission}`)
export { export {
can, can,
goTo, viewTo,
transl transl
} }

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api';
import { logout } from '@Services/Page';
import ActionSection from '@Holos/ActionSection.vue'; import ActionSection from '@Holos/ActionSection.vue';
import DangerButton from '@Holos/Button/Danger.vue'; import DangerButton from '@Holos/Button/Danger.vue';
@ -22,9 +23,12 @@ const confirmUserDeletion = () => {
}; };
const deleteUser = () => { const deleteUser = () => {
form.delete(route('current-user.destroy'), { form.delete(route('user.destroy'), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => closeModal(), onSuccess: () => {
closeModal();
logout();
},
onError: () => passwordInput.value.focus(), onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(), onFinish: () => form.reset(),
}); });

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api';
import ActionSection from '@Holos/ActionSection.vue'; import ActionSection from '@Holos/ActionSection.vue';
import DialogModal from '@Holos/DialogModal.vue'; import DialogModal from '@Holos/DialogModal.vue';
@ -9,7 +9,10 @@ import SecondaryButton from '@Holos/Button/Secondary.vue';
import Input from '@Holos/Form/Input.vue'; import Input from '@Holos/Form/Input.vue';
defineProps({ defineProps({
sessions: Array, sessions: {
type: Array,
default: () => [],
},
}); });
const confirmingLogout = ref(false); const confirmingLogout = ref(false);
@ -30,7 +33,7 @@ const logoutOtherBrowserSessions = () => {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
form.reset(); form.reset();
Notify.success(lang('account.sessions.done')); Notify.success(Lang('account.sessions.done'));
}, },
onError: () => passwordInput.value.focus(), onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(), onFinish: () => form.reset(),

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { router, useForm, usePage } from '@inertiajs/vue3'; import { api, useForm } from '@Services/Api';
import ActionSection from '@Holos/ActionSection.vue'; import ActionSection from '@Holos/ActionSection.vue';
import ConfirmsPassword from '@Holos/ConfirmsPassword.vue'; import ConfirmsPassword from '@Holos/ConfirmsPassword.vue';
@ -10,37 +10,40 @@ import SecondaryButton from '@Holos/Button/Secondary.vue';
import Input from '@Holos/Form/Input.vue'; import Input from '@Holos/Form/Input.vue';
const props = defineProps({ const props = defineProps({
requiresConfirmation: Boolean, requiresConfirmation: {
type: Boolean,
default: false,
},
}); });
const page = usePage();
const enabling = ref(false); const enabling = ref(false);
const confirming = ref(false); const confirming = ref(false);
const disabling = ref(false); const disabling = ref(false);
const qrCode = ref(null); const qrCode = ref(null);
const setupKey = ref(null); const setupKey = ref(null);
const recoveryCodes = ref([]); const recoveryCodes = ref([]);
const user = ref(null);
const confirmationForm = useForm({ const confirmationForm = useForm({
code: '', code: '',
}); });
const twoFactorEnabled = computed( const twoFactorEnabled = computed(
() => ! enabling.value && page.props.auth.user?.two_factor_enabled, () => ! enabling.value && user.value?.two_factor_secret,
); );
watch(twoFactorEnabled, () => { watch(twoFactorEnabled, () => {
if (! twoFactorEnabled.value) { if (! twoFactorEnabled.value) {
confirmationForm.reset(); confirmationForm.reset();
confirmationForm.clearErrors();
} }
}); });
const enableTwoFactorAuthentication = () => { const enableTwoFactorAuthentication = () => {
enabling.value = true; enabling.value = true;
router.post(route('two-factor.enable'), {}, { console.log('enabling ...');
preserveScroll: true,
api.post(route('two-factor.enable'), {
onSuccess: () => Promise.all([ onSuccess: () => Promise.all([
showQrCode(), showQrCode(),
showSetupKey(), showSetupKey(),
@ -93,7 +96,7 @@ const regenerateRecoveryCodes = () => {
const disableTwoFactorAuthentication = () => { const disableTwoFactorAuthentication = () => {
disabling.value = true; disabling.value = true;
router.delete(route('two-factor.disable'), { api.delete(route('two-factor.disable'), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
disabling.value = false; disabling.value = false;
@ -101,6 +104,14 @@ const disableTwoFactorAuthentication = () => {
}, },
}); });
}; };
onMounted(() => {
api.get(route('user.show'), {
onSuccess: (r) => {
user.value = r.user;
}
});
});
</script> </script>
<template> <template>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@Services/Api';
import FormSection from '@Holos/FormSection.vue'; import FormSection from '@Holos/FormSection.vue';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
@ -16,14 +16,13 @@ const form = useForm({
}); });
const updatePassword = () => { const updatePassword = () => {
form.put(route('user-password.update'), { form.put(route('user.password'), {
errorBag: 'updatePassword',
preserveScroll: true,
onSuccess: () => { onSuccess: () => {
form.reset(); form.reset();
Notify.success(lang('account.password.updated')); Notify.success(Lang('account.password.updated'));
}, },
onError: () => { onError: (e) => {
console.log(e);
if (form.errors.password) { if (form.errors.password) {
form.reset('password', 'password_confirmation'); form.reset('password', 'password_confirmation');
passwordInput.value.focus(); passwordInput.value.focus();

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { Link, router, useForm } from '@inertiajs/vue3'; import { api, useForm } from '@Services/Api';
import { reloadUser } from '@Services/Page';
import FormSection from '@Holos/FormSection.vue'; import FormSection from '@Holos/FormSection.vue';
import Input from '@Holos/Form/Input.vue'; import Input from '@Holos/Form/Input.vue';
import Error from '@Holos/Form/Elements/Error.vue'; import Error from '@Holos/Form/Elements/Error.vue';
@ -9,42 +9,34 @@ import Label from '@Holos/Form/Elements/Label.vue';
import PrimaryButton from '@Holos/Button/Primary.vue'; import PrimaryButton from '@Holos/Button/Primary.vue';
import SecondaryButton from '@Holos/Button/Secondary.vue'; import SecondaryButton from '@Holos/Button/Secondary.vue';
const props = defineProps({ /** Propiedades */
user: Object,
});
const form = useForm({ const form = useForm({
_method: 'PUT', _method: 'PUT',
name: props.user.name, name: '',
paternal: props.user.paternal, paternal: '',
maternal: props.user.maternal, maternal: '',
phone: props.user.phone, phone: '',
name: props.user.name, email: '',
email: props.user.email,
photo: null, photo: null,
}); });
const verificationLinkSent = ref(null); const photoInput = ref(null);
const photoPreview = ref(null); const photoPreview = ref(null);
const photoInput = ref(null);
/** Métodos */
const updateProfileInformation = () => { const updateProfileInformation = () => {
if (photoInput.value) { if (photoInput.value) {
form.photo = photoInput.value.files[0]; form.photo = photoInput.value.files[0];
} }
form.post(route('user-profile-information.update'), { form.post(route('user.update'), {
errorBag: 'updateProfileInformation', onFinish: () => {
preserveScroll: true, reloadUser();
onSuccess: () => {
clearPhotoFileInput(); clearPhotoFileInput();
Notify.success(lang('account.profile.updated')); }
},
}); });
};
const sendEmailVerification = () => { Notify.success(Lang('account.profile.updated'));
verificationLinkSent.value = true;
}; };
const selectNewPhoto = () => { const selectNewPhoto = () => {
@ -66,12 +58,12 @@ const updatePhotoPreview = () => {
}; };
const deletePhoto = () => { const deletePhoto = () => {
router.delete(route('current-user-photo.destroy'), { api.delete(route('user.photo'), {
preserveScroll: true, onFinish: () => {
onSuccess: () => {
photoPreview.value = null; photoPreview.value = null;
reloadUser();
clearPhotoFileInput(); clearPhotoFileInput();
}, }
}); });
}; };
@ -80,6 +72,14 @@ const clearPhotoFileInput = () => {
photoInput.value.value = null; photoInput.value.value = null;
} }
}; };
onMounted(() => {
api.get(route('user.show'), {
onSuccess: (r) => {
form.fill(r.user);
}
});
});
</script> </script>
<template> <template>
@ -94,7 +94,7 @@ const clearPhotoFileInput = () => {
<template #form> <template #form>
<!-- Profile Photo --> <!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="col-span-6 sm:col-span-4"> <div v-if="$page.user" class="col-span-6 sm:col-span-4">
<!-- Profile Photo File Input --> <!-- Profile Photo File Input -->
<input <input
id="photo" id="photo"
@ -111,7 +111,7 @@ const clearPhotoFileInput = () => {
<!-- Current Profile Photo --> <!-- Current Profile Photo -->
<div v-show="! photoPreview" class="mt-2"> <div v-show="! photoPreview" class="mt-2">
<img :src="user.profile_photo_url" :alt="user.name" class="rounded-full h-20 w-20 object-cover"> <img :src="$page.user.profile_photo_url" :alt="$page.user.name" class="rounded-full h-20 w-20 object-cover">
</div> </div>
<!-- New Profile Photo Preview --> <!-- New Profile Photo Preview -->
@ -127,7 +127,7 @@ const clearPhotoFileInput = () => {
</SecondaryButton> </SecondaryButton>
<SecondaryButton <SecondaryButton
v-if="user.profile_photo_path" v-if="$page.user.profile_photo_path"
type="button" type="button"
class="mt-2" class="mt-2"
@click.prevent="deletePhoto" @click.prevent="deletePhoto"
@ -180,26 +180,6 @@ const clearPhotoFileInput = () => {
:onError="form.errors.email" :onError="form.errors.email"
required required
/> />
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null">
<p class="text-sm mt-2">
{{ $t('account.email.unverify') }}
<Link
:href="route('verification.send')"
method="post"
as="button"
class="underline text-sm text-page-t/50 hover:text-page-t dark:text-page-dt/50 dark:hover:text-page-dt rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click.prevent="sendEmailVerification"
>
{{ $t('account.email.sendVerification') }}
</Link>
</p>
<div v-show="verificationLinkSent" class="mt-2 font-medium text-sm text-green-600">
{{ $t('account.email.notifySendVerification') }}
</div>
</div>
</div> </div>
</template> </template>

View File

@ -1,11 +1,11 @@
<script setup> <script setup>
import SectionBorder from '@Holos/SectionBorder.vue'; import SectionBorder from '@Holos/SectionBorder.vue';
import AppLayout from '@Layouts/AppLayout.vue';
import DeleteUserForm from '@Pages/Profile/Partials/DeleteUserForm.vue'; import DeleteUserForm from '@Pages/Profile/Partials/DeleteUserForm.vue';
import LogoutOtherBrowserSessionsForm from '@Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue';
import TwoFactorAuthenticationForm from '@Pages/Profile/Partials/TwoFactorAuthenticationForm.vue'; import TwoFactorAuthenticationForm from '@Pages/Profile/Partials/TwoFactorAuthenticationForm.vue';
import UpdatePasswordForm from '@Pages/Profile/Partials/UpdatePasswordForm.vue'; import UpdatePasswordForm from '@Pages/Profile/Partials/UpdatePasswordForm.vue';
import UpdateProfileInformationForm from '@Pages/Profile/Partials/UpdateProfileInformationForm.vue'; import UpdateProfileInformationForm from '@Pages/Profile/Partials/UpdateProfileInformationForm.vue';
import LogoutOtherBrowserSessionsForm from '@Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue';
import PageHeader from '@Holos/PageHeader.vue';
defineProps({ defineProps({
confirmsTwoFactorAuthentication: Boolean, confirmsTwoFactorAuthentication: Boolean,
@ -14,44 +14,25 @@ defineProps({
</script> </script>
<template> <template>
<AppLayout title="Profile"> <PageHeader :title="$t('profile')" />
<template #header> <div>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Profile
</h2>
</template>
<div> <div>
<div class="py-10 sm:px-6 lg:px-8"> <UpdateProfileInformationForm :user="$page.user" />
<div v-if="$page.props.jetstream.canUpdateProfileInformation"> <SectionBorder />
<UpdateProfileInformationForm :user="$page.props.auth.user" />
<SectionBorder />
</div>
<div v-if="$page.props.jetstream.canUpdatePassword">
<UpdatePasswordForm class="mt-10 sm:mt-0" />
<SectionBorder />
</div>
<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
<TwoFactorAuthenticationForm
:requires-confirmation="confirmsTwoFactorAuthentication"
class="mt-10 sm:mt-0"
/>
<SectionBorder />
</div>
<LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" />
<template v-if="$page.props.jetstream.hasAccountDeletionFeatures">
<SectionBorder />
<DeleteUserForm class="mt-10 sm:mt-0" />
</template>
</div>
</div> </div>
</AppLayout> <div>
<UpdatePasswordForm class="mt-10 sm:mt-0" />
<SectionBorder />
</div>
<!-- <div>
<TwoFactorAuthenticationForm
:requires-confirmation="confirmsTwoFactorAuthentication"
class="mt-10 sm:mt-0"
/>
<SectionBorder />
</div> -->
<!-- <LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" /> -->
<SectionBorder />
<DeleteUserForm class="mt-10 sm:mt-0" />
</div>
</template> </template>

View File

@ -4,7 +4,7 @@ import toastr from 'toastr';
class Notify { class Notify {
constructor() {} constructor() {}
flash({message = 'Successful registration', type = 'success', timeout = 5, title= lang('notification')}) { flash({message = 'Successful registration', type = 'success', timeout = 5, title= Lang('notification')}) {
toastr.options = { toastr.options = {
"closeButton": true, "closeButton": true,

View File

@ -1,4 +1,5 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { api } from '@Services/Api';
const permissionsInit = ref(false) const permissionsInit = ref(false)
const allPermissions = ref([]) const allPermissions = ref([])
@ -22,10 +23,13 @@ const hasPermission = (can) => {
const bootPermissions = () => { const bootPermissions = () => {
if (!permissionsInit.value) { if (!permissionsInit.value) {
axios.get(route('system.permissions')).then((res) => { api.get(route('user.permissions'), {
loadPermissions(res.data.data.permissions) onSuccess: (res) => {
loadPermissions(res.permissions)
permissionsInit.value = true; },
onFinish: () => {
permissionsInit.value = true;
}
}) })
} }
} }

24
src/router/Auth.js Normal file
View File

@ -0,0 +1,24 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: () => import('@Pages/Auth/Login.vue')
},
{
path: '/forgot-password',
name: 'forgot-password',
component: () => import('@Pages/Auth/ForgotPassword.vue')
},
{
path: '/reset-password',
name: 'reset-password',
component: () => import('@Pages/Auth/ResetPassword.vue')
}
]
})
export default router

45
src/router/Index.js Normal file
View File

@ -0,0 +1,45 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: () => import('@Pages/Dashboard/Index.vue')
}, {
path: '/profile',
name: 'profile.show',
component: () => import('@Pages/Profile/Show.vue')
}, {
path: '/admin',
children: [
{
path: 'users',
children: [
{
path: '',
name: 'admin.users.index',
component: () => import('@Pages/Admin/Users/Index.vue')
},
{
path: 'create',
name: 'admin.users.create',
component: () => import('@Pages/Admin/Users/Create.vue')
}, {
path: ':id/edit',
name: 'admin.users.edit',
component: () => import('@Pages/Admin/Users/Edit.vue')
}, {
path: ':id/settings',
name: 'admin.users.settings',
component: () => import('@Pages/Admin/Users/Settings.vue')
}
]
},
]
}
]
})
export default router

View File

@ -7,7 +7,6 @@
import axios from 'axios'; import axios from 'axios';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { lang } from '@Lang/i18n';
axios.defaults.withXSRFToken = true; axios.defaults.withXSRFToken = true;
// axios.defaults.withCredentials = true; // axios.defaults.withCredentials = true;
@ -18,20 +17,14 @@ axios.defaults.withXSRFToken = true;
const failCodes = [ const failCodes = [
400, 400,
409, 409,
422
]; ];
/** /**
* Servidor a utilizar * Servidor a utilizar
*/ */
const server = ref(''); const token = ref(localStorage.token);
const token = ref(localStorage.token); const csrfToken = ref(localStorage.csrfToken);
/**
* Define el servidor de la api
*/
const defineApiServer = (x) => {
server.value = x;
}
/** /**
* Define el token de la api * Define el token de la api
@ -42,21 +35,13 @@ const defineApiToken = (x) => {
} }
/** /**
* Ruta base del servidor * Define CSRF token
*/ */
const apiBaseUrl = (url) => { const defineCsrfToken = (x) => {
return `${server.value}/${url}` csrfToken.value = x;
localStorage.csrfToken = x;
} }
/**
* Ruta api del servidor
*/
const apiUrl = (url) => {
return apiBaseUrl(`api/${url}`)
}
/** /**
* Define el token de la api * Define el token de la api
*/ */
@ -65,6 +50,14 @@ const resetApiToken = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
} }
/**
* Reset CSRF token
*/
const resetCsrfToken = () => {
csrfToken.value = undefined;
localStorage.removeItem('csrfToken');
}
/** /**
* Determina si el token tiene algo o no * Determina si el token tiene algo o no
*/ */
@ -75,9 +68,13 @@ const hasToken = () => {
/** /**
* Fuerza el cierre de la sesión * Fuerza el cierre de la sesión
*/ */
const logout = () => { const closeSession = () => {
localStorage.removeItem('token'); resetApiToken()
token.value = undefined; resetCsrfToken()
Notify.info(Lang('session.closed'))
location.replace('auth.html')
} }
/** /**
@ -138,6 +135,8 @@ const api = {
if(options.hasOwnProperty('onFail')) { if(options.hasOwnProperty('onFail')) {
options.onFail(data.data); options.onFail(data.data);
} }
console.log(data.data);
} }
if(options.hasOwnProperty('onFinish')) { if(options.hasOwnProperty('onFinish')) {
@ -152,9 +151,9 @@ const api = {
// Código de sesión invalida // Código de sesión invalida
if(response.status === 401 && response.data?.message == 'Unauthenticated.') { if(response.status === 401 && response.data?.message == 'Unauthenticated.') {
Notify.error(lang('session.expired')); Notify.error(Lang('session.expired'));
logout(); closeSession();
return return
} }
@ -182,35 +181,35 @@ const api = {
get(url, options) { get(url, options) {
this.load({ this.load({
method: 'get', method: 'get',
url: apiUrl(url), url,
options options
}) })
}, },
post(url, options) { post(url, options) {
this.load({ this.load({
method: 'post', method: 'post',
url: apiUrl(url), url,
options options
}) })
}, },
put(url, options) { put(url, options) {
this.load({ this.load({
method: 'put', method: 'put',
url: apiUrl(url), url,
options options
}) })
}, },
patch(url, options) { patch(url, options) {
this.load('patch', { this.load('patch', {
method: 'patch', method: 'patch',
url: apiUrl(url), url,
options options
}) })
}, },
delete(url, options) { delete(url, options) {
this.load({ this.load({
method: 'delete', method: 'delete',
url: apiUrl(url), url,
options options
}) })
}, },
@ -219,7 +218,6 @@ const api = {
...options, ...options,
data: resources data: resources
}) })
console.log(api.resource)
}, },
download(url, file, params = {}) { download(url, file, params = {}) {
axios({ axios({
@ -310,6 +308,14 @@ const useForm = (form = {}) => {
processing: false, processing: false,
wasSuccessful: false, wasSuccessful: false,
_inputs: Object.keys(form), _inputs: Object.keys(form),
_original: {
...form
},
reset() {
for(let i in this._original) {
this[i] = this._original[i]
}
},
data() { data() {
let data = {}; let data = {};
@ -354,7 +360,8 @@ const useForm = (form = {}) => {
? 'multipart/form-data boundary=' ? 'multipart/form-data boundary='
: 'application/json', : 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}` 'Authorization': `Bearer ${apiToken}`,
'X-CSRF-TOKEN': csrfToken.value
} }
}); });
@ -397,45 +404,43 @@ const useForm = (form = {}) => {
}, },
fill(model) { fill(model) {
this._inputs.forEach(element => { this._inputs.forEach(element => {
if (element == 'is_active') { this[element] = (element == 'is_active')
this[element] = (model[element] == 1) ? (model[element] == 1)
} else { : model[element] ?? this[element]
this[element] = model[element] ?? this[element]
}
}); });
}, },
get(url, options) { get(url, options) {
this.load({ this.load({
method: 'get', method: 'get',
url: apiUrl(url), url,
options options
}) })
}, },
post(url, options) { post(url, options) {
this.load({ this.load({
method: 'post', method: 'post',
url: apiUrl(url), url,
options options
}) })
}, },
put(url, options) { put(url, options) {
this.load({ this.load({
method: 'put', method: 'put',
url: apiUrl(url), url,
options options
}) })
}, },
patch(url, options) { patch(url, options) {
this.load('patch', { this.load('patch', {
method: 'patch', method: 'patch',
url: apiUrl(url), url,
options options
}) })
}, },
delete(url, options) { delete(url, options) {
this.load({ this.load({
method: 'delete', method: 'delete',
url: apiUrl(url), url,
options options
}) })
}, },
@ -507,8 +512,8 @@ const useSearcher = (options = {
// Código de sesión invalida // Código de sesión invalida
if(response.status === 401 && response.data.message == 'Unauthenticated.') { if(response.status === 401 && response.data.message == 'Unauthenticated.') {
Notify.error(lang('session.expired')); Notify.error(Lang('session.expired'));
logout(); closeSession();
return return
} }
@ -535,13 +540,13 @@ const useSearcher = (options = {
search(q, filters = {}) { search(q, filters = {}) {
this.query = q this.query = q
this.load({ this.load({
url: apiUrl(options.url), url,
filters filters
}) })
}, },
refresh(filters = {}) { refresh(filters = {}) {
this.load({ this.load({
url: apiUrl(options.url), url,
filters filters
}) })
}, },
@ -550,12 +555,11 @@ const useSearcher = (options = {
export { export {
api, api,
token, token,
apiBaseUrl, closeSession,
apiUrl,
hasToken, hasToken,
useForm, useForm,
useSearcher, useSearcher,
defineApiServer,
defineApiToken, defineApiToken,
defineCsrfToken,
resetApiToken resetApiToken
} }

40
src/services/Broadcast.js Normal file
View File

@ -0,0 +1,40 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
import { token } from '@Services/Api';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
authorizer: (channel, options) => {
return {
authorize: async (socketId, callback) => {
try {
let { data } = await axios({
method: 'post',
url: import.meta.env.VITE_REVERB_SCHEME + '://' + import.meta.env.VITE_REVERB_HOST + '/broadcasting/auth',
data: {
socket_id: socketId,
channel_name: channel.name,
},
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token.value}`
}
});
callback(null, data);
} catch (err) {
callback(err);
}
}
};
},
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

116
src/services/Page.js Normal file
View File

@ -0,0 +1,116 @@
import { reactive } from "vue";
import { api, closeSession } from '@Services/Api';
import { resetPermissions } from '@Plugins/RolePermission';
/**
* Cache
*
* Permite cargar los datos una vez para la sesión actual. Se mantienen mientras
* no se recargue la página.
*/
const page = reactive({
lang: 'es',
user: {
id: 0,
name: 'public'
}
})
/**
* Recarga datos en cache
*/
const reloadApp = () => {
const user = localStorage.user
if(user) {
page.user = JSON.parse(user);
}
}
/**
* Limpiar sesión de usuario
*/
const resetPage = () => {
localStorage.removeItem('user');
}
/**
* Permite buscar una opcionalmente
*/
const view = ({
name = '',
params = {},
query = {},
}) => ({
name: name,
params,
query,
})
/**
* Almacenar datos usuario
*/
const defineUser = (user) => {
localStorage.user = JSON.stringify({
id: user.id,
name: user.name,
lastname: `${user.paternal} ${user?.maternal ?? ''}`,
email: user.email,
phone: user.phone,
profile_photo_url: user.profile_photo_url,
profile_photo_path: user.profile_photo_path,
});
}
/**
* Instalar el componente de forma nativa
*/
const pagePlugin = {
install: (app, options) => {
app.config.globalProperties.$page = page;
app.config.globalProperties.$view = view;
}
}
/**
* Reload user
*/
const reloadUser = () => {
console.log('reloadUser')
return api.get(route('user.show'), {
onSuccess: (r) => {
defineUser(r.user)
reloadApp()
return r.user;
}
});
}
/**
* Cerrar sesión
*/
const logout = () => {
resetPermissions()
resetPage()
api.post(route('auth.logout'), {
onSuccess: (r) => {
if(r.is_revoked === true) {
closeSession()
}
}
});
};
export {
pagePlugin,
page,
reloadApp,
reloadUser,
resetPage,
defineUser,
logout,
view
}

Some files were not shown because too many files have changed in this diff Show More