-
-
One
+
+
+
+
diff --git a/src/components/CardBox.vue b/src/components/CardBox.vue
index 9daa7172..9c2dc477 100644
--- a/src/components/CardBox.vue
+++ b/src/components/CardBox.vue
@@ -29,7 +29,7 @@ const componentClass = computed(() => {
const base = [
props.rounded,
props.flex,
- props.isModal ? 'dark:bg-slate-900' : 'dark:bg-slate-900/70'
+ props.isModal ? 'dark:bg-slate-900' : 'dark:bg-slate-900/70',
]
if (props.isHoverable) {
diff --git a/src/components/CardBoxModal.vue b/src/components/CardBoxModal.vue
index 1be60dbf..6d7222a1 100644
--- a/src/components/CardBoxModal.vue
+++ b/src/components/CardBoxModal.vue
@@ -18,27 +18,37 @@ const props = defineProps({
},
buttonLabel: {
type: String,
- default: 'Done'
+ default: 'Aceptar'
},
hasCancel: Boolean,
modelValue: {
type: [String, Number, Boolean],
default: null
+ },
+ modalSize: {
+ type: String,
+ default: 'xl'
+ },
+ noButton: {
+ type: Boolean,
+ default: false
}
})
-const emit = defineEmits(['update:modelValue', 'cancel', 'confirm'])
+const emit = defineEmits(['update:modelValue'])
const value = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
+
const confirmCancel = (mode) => {
value.value = false
emit(mode)
}
+
const confirm = () => confirmCancel('confirm')
const cancel = () => confirmCancel('cancel')
@@ -48,13 +58,18 @@ window.addEventListener('keydown', (e) => {
cancel()
}
})
+
+defineExpose({
+ confirm,
+ cancel
+})
-
@@ -68,14 +83,14 @@ window.addEventListener('keydown', (e) => {
/>
-
-
+
+
-
-
+
+
diff --git a/src/components/CardBoxModalForm.vue b/src/components/CardBoxModalForm.vue
new file mode 100644
index 00000000..20ba2ff0
--- /dev/null
+++ b/src/components/CardBoxModalForm.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CardBoxModalOperario.vue b/src/components/CardBoxModalOperario.vue
new file mode 100644
index 00000000..f1967a55
--- /dev/null
+++ b/src/components/CardBoxModalOperario.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ExportExcel.vue b/src/components/ExportExcel.vue
new file mode 100644
index 00000000..44fa025a
--- /dev/null
+++ b/src/components/ExportExcel.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/FlagIcons.vue b/src/components/FlagIcons.vue
new file mode 100644
index 00000000..2a2527d7
--- /dev/null
+++ b/src/components/FlagIcons.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/FooterBar.vue b/src/components/FooterBar.vue
index 3f20198e..d3b76f84 100644
--- a/src/components/FooterBar.vue
+++ b/src/components/FooterBar.vue
@@ -1,7 +1,8 @@
@@ -10,12 +11,12 @@ const year = new Date().getFullYear()
-
©{{ year }}, JustBoil.me .
+
©{{ year }}, AQLARA.com .
diff --git a/src/components/FormAnalitica.vue b/src/components/FormAnalitica.vue
new file mode 100644
index 00000000..e0c516db
--- /dev/null
+++ b/src/components/FormAnalitica.vue
@@ -0,0 +1,337 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Caracteristicas Organolépticas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/FormInfraestructura.vue b/src/components/FormInfraestructura.vue
new file mode 100644
index 00000000..2adbc511
--- /dev/null
+++ b/src/components/FormInfraestructura.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/FormOperario.vue b/src/components/FormOperario.vue
new file mode 100644
index 00000000..56003333
--- /dev/null
+++ b/src/components/FormOperario.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/FormPuntoMuestreo.vue b/src/components/FormPuntoMuestreo.vue
new file mode 100644
index 00000000..5af77501
--- /dev/null
+++ b/src/components/FormPuntoMuestreo.vue
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/FormUO.vue b/src/components/FormUO.vue
new file mode 100644
index 00000000..221e06ff
--- /dev/null
+++ b/src/components/FormUO.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/FormZona.vue b/src/components/FormZona.vue
new file mode 100644
index 00000000..8919caed
--- /dev/null
+++ b/src/components/FormZona.vue
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/InfraestructurasTable.vue b/src/components/InfraestructurasTable.vue
new file mode 100644
index 00000000..3ce069af
--- /dev/null
+++ b/src/components/InfraestructurasTable.vue
@@ -0,0 +1,336 @@
+
+
+
+
+
+
+
+
+
+ Esta seguro que desea eliminar la Infraestructura
+ {{ dataToEdit?.name }} ?
+
+ Esta operación no se puede deshacer.
+
+
+
+
+
Filtros de Búsqueda
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mostrando {{ infraestructuras.length }} infraestructura(s)
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Nombre
+ Tipo
+ Operador
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ infraestructura.id }}
+
+
+ {{ infraestructura.name }}
+
+
+ {{ getTypeById(infraestructura.type) }}
+
+
+ {{ infraestructura.operador }}
+
+
+
+
+
+
+
+
+
+
+ Puntos de Muestreo
+
+
+
+
+ No hay puntos de muestreo asignados
+
+
+
+
+
+ {{ puntoMuestreo.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/NavBarItem.vue b/src/components/NavBarItem.vue
index 06384498..0b7e9760 100644
--- a/src/components/NavBarItem.vue
+++ b/src/components/NavBarItem.vue
@@ -2,12 +2,15 @@
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import { RouterLink } from 'vue-router'
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
-import { useMainStore } from '@/stores/main.js'
+// import { useMainStore } from '@/stores/main.js'
+import { useLoginStore } from '@/stores/login'
import BaseIcon from '@/components/BaseIcon.vue'
import UserAvatarCurrentUser from '@/components/UserAvatarCurrentUser.vue'
import NavBarMenuList from '@/components/NavBarMenuList.vue'
import BaseDivider from '@/components/BaseDivider.vue'
+
+
const props = defineProps({
item: {
type: Object,
@@ -17,6 +20,18 @@ const props = defineProps({
const emit = defineEmits(['menu-click'])
+const loginStore = useLoginStore()
+
+// Computed que determina si se muestra el ítem basado en la propiedad maxRole
+const isVisible = computed(() => {
+ // Si el ítem tiene propiedad maxRole, se verifica que el role del usuario sea menor
+ if (props.item.minRole !== undefined) {
+ const currentRole = Number(loginStore.userLogged?.role)
+ return currentRole >= props.item.minRole
+ }
+ return true
+})
+
const is = computed(() => {
if (props.item.href) {
return 'a'
@@ -45,7 +60,7 @@ const componentClass = computed(() => {
})
const itemLabel = computed(() =>
- props.item.isCurrentUser ? useMainStore().userName : props.item.label
+ props.item.isCurrentUser ? useLoginStore().userName : props.item.label
)
const isDropdownActive = ref(false)
@@ -87,7 +102,7 @@ onBeforeUnmount(() => {
{
item.menu
}"
>
-
+
- {{ itemLabel }} {
+import { ref, computed, watch, onMounted } from 'vue'
+import { usePlantasStore } from '@/stores/plantas'
+import BaseButtons from './BaseButtons.vue'
+import BaseButton from './BaseButton.vue'
+import {
+ mdiAlertCircleOutline,
+ mdiChevronDown,
+ mdiChevronLeft,
+ mdiMapMarker,
+ mdiPencil,
+ mdiTrashCan,
+ mdiFilterRemove
+} from '@mdi/js'
+import BaseIcon from './BaseIcon.vue'
+import CardBoxModal from './CardBoxModal.vue'
+import CardBoxModalForm from './CardBoxModalForm.vue'
+import FormControl from './FormControl.vue'
+import FormField from './FormField.vue'
+import BaseLevel from './BaseLevel.vue'
+// import { anularUO as anularZona, createUO, updateUO } from '@/services/uo'
+import FormZona from './FormZona.vue'
+import { createZona, anularZona, updateZona } from '@/services/zonas'
+import {
+ createInfraestructura,
+ anularInfraestructura,
+ updateInfraestructura
+} from '@/services/infraestructuras'
+import FormInfraestructura from './FormInfraestructura.vue'
+import FormPuntoMuestreo from './FormPuntoMuestreo.vue'
+import { anularPuntoMuestreo, createPuntoMuestreo, updatePuntoMuestreo } from '@/services/puntosMuestreo'
+
+defineProps({
+ checkable: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const plantaStore = usePlantasStore()
+
+// Variables para filtros
+const filters = ref({
+ nombre: '',
+ zona: '',
+ infraestructura: ''
+})
+
+// Funciones para limpiar filtros
+const clearFilters = () => {
+ filters.value.nombre = ''
+ filters.value.zona = ''
+ filters.value.infraestructura = ''
+}
+
+
+// Computed para filtrar puntos de muestreo
+const puntosMuestreoFiltrados = computed(() => {
+ let puntos = plantaStore.getPuntosMuestreo.filter((punto) => punto.activo)
+
+ // Filtrar por nombre
+ if (filters.value.nombre) {
+ puntos = puntos.filter(punto =>
+ punto.name.toLowerCase().includes(filters.value.nombre.toLowerCase())
+ )
+ }
+
+ // Filtrar por zona
+ if (filters.value.zona) {
+ const zonaId = (typeof filters.value.zona === 'object' && filters.value.zona.value !== undefined)
+ ? filters.value.zona.value
+ : filters.value.zona
+
+ if (zonaId !== '' && zonaId !== null) {
+ puntos = puntos.filter(punto => punto.zona_fk == zonaId)
+ }
+ }
+
+ // Filtrar por infraestructura
+ if (filters.value.infraestructura) {
+ const infraId = (typeof filters.value.infraestructura === 'object' && filters.value.infraestructura.value !== undefined)
+ ? filters.value.infraestructura.value
+ : filters.value.infraestructura
+
+ if (infraId !== '' && infraId !== null) {
+ puntos = puntos.filter(punto => punto.infraestructura_fk == infraId)
+ }
+ }
+
+ return puntos
+})
+
+// Opciones para los filtros
+const zonasOptions = computed(() => {
+ const options = [{ value: '', label: 'Todas las zonas' }]
+
+ // Crear un Set para evitar duplicados basándose en zona_fk de los puntos
+ const zonasEnUso = new Set()
+ plantaStore.getPuntosMuestreo.forEach(punto => {
+ if (punto.zona_fk) zonasEnUso.add(punto.zona_fk)
+ })
+
+ // Agregar solo las zonas que tienen puntos asociados
+ Array.from(zonasEnUso).forEach(zonaId => {
+ const zona = plantaStore.getZonas.find(z => z.id === zonaId)
+ if (zona) {
+ options.push({ value: zonaId, label: zona.name })
+ }
+ })
+
+ return options
+})
+
+const infraestructurasOptions = computed(() => {
+ const options = [{ value: '', label: 'Todas las infraestructuras' }]
+
+ // Crear un Set para evitar duplicados basándose en infraestructura_fk de los puntos
+ const infrasEnUso = new Set()
+ plantaStore.getPuntosMuestreo.forEach(punto => {
+ if (punto.infraestructura_fk) infrasEnUso.add(punto.infraestructura_fk)
+ })
+
+ // Agregar solo las infraestructuras que tienen puntos asociados
+ Array.from(infrasEnUso).forEach(infraId => {
+ const infra = plantaStore.getInfraestructuras.find(i => i.id === infraId)
+ if (infra) {
+ options.push({ value: infraId, label: infra.name })
+ }
+ })
+
+ return options
+})
+
+const puntosMuestreo = computed(() => puntosMuestreoFiltrados.value)
+const isLoading = ref(true)
+
+const isModalDangerActive = ref(false)
+const dataToEdit = ref(null)
+const isModalOpen = ref(false)
+
+const expandedRows = ref([])
+
+const getInfraestructuraById = (infrId) => {
+ return (
+ plantaStore.getInfraestructuras.find((infraestructura) => infraestructura.id === infrId)
+ ?.name ?? '-'
+ )
+}
+
+const getZonaById = (zonaId) => {
+ return plantaStore.getZonas.find((zona) => zona.id === zonaId)?.name ?? '-'
+}
+
+const puntosMuestreoPorInfraestructura = (idInfraestructura) => {
+ return plantaStore.getPuntosMuestreo
+ .filter((pMuestreo) => pMuestreo.infraestructura_fk === idInfraestructura)
+ .map((punto) => {
+ return {
+ name: punto.name,
+ id: punto.id
+ }
+ })
+}
+
+const toggleExpand = (id) => {
+ if (expandedRows.value.includes(id)) {
+ expandedRows.value = expandedRows.value.filter((rowId) => rowId !== id)
+ } else {
+ expandedRows.value.push(id)
+ }
+}
+
+const openModal = (zona) => {
+ dataToEdit.value = zona
+ console.log('openModal:', dataToEdit.value)
+ isModalOpen.value = true
+}
+
+const closeModal = () => {
+ isModalOpen.value = false
+ dataToEdit.value = null
+}
+
+const anularPuntoMuestreoSeleccionado = (zona) => {
+ dataToEdit.value = zona
+ isModalDangerActive.value = true
+}
+
+const handleDeleteData = async () => {
+ try {
+ const id = dataToEdit.value.id
+ await anularPuntoMuestreo(id)
+ await plantaStore.loadPuntosMuestreo()
+ isModalDangerActive.value = false
+ dataToEdit.value = null
+ alert('Punto de Muestreo eliminado correctamente')
+ } catch (error) {
+ console.log('error al borrar el Punto de Muestreo: ', error)
+ alert('error al borrar el Punto de Muestreo: ', error)
+ }
+}
+
+const saveForm = async (form) => {
+ // console.log("ESCRITO Y HECHO", form);
+ if (form.esNuevo) {
+ console.log('Formulario Nuevo:', form.id)
+ await createPuntoMuestreo(form)
+ // form.value=null
+ } else {
+ console.log('Formulario Editado:', form)
+ await updatePuntoMuestreo(form)
+ }
+ await plantaStore.loadPuntosMuestreo()
+ closeModal()
+ alert('Punto de Muestreo guardado correctamente')
+}
+
+// Cargar datos iniciales
+onMounted(async () => {
+ try {
+ await plantaStore.loadPuntosMuestreo()
+ isLoading.value = false
+ } catch (error) {
+ console.error('Error al cargar puntos de muestreo:', error)
+ }
+})
+
+watch(puntosMuestreo, (newValue) => {
+ console.log('Puntos de Muestreo WATCH:', newValue)
+})
+
+defineExpose({
+ openModal
+})
+
+
+
+
+ Cargando datos...
+
+
+
+
+
Filtros de Búsqueda
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mostrando {{ puntosMuestreo.length }} punto(s) de muestreo
+
+
+
+
+
+
+
+
+
+
+
+
+ Esta seguro que desea eliminar la Infraestructura
+ {{ dataToEdit?.name }} ?
+
+ Esta operación no se puede deshacer.
+
+
+
+
+
+
+
+ ID
+ Nombre
+ Infraestructura
+ Zona
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ punto.id }}
+
+
+ {{ punto.name }}
+
+
+ {{ getInfraestructuraById(punto.infraestructura_fk) }}
+
+
+ {{ getZonaById(punto.zona_fk) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SectionFullScreen.vue b/src/components/SectionFullScreen.vue
index 87d6dca7..f09b548a 100644
--- a/src/components/SectionFullScreen.vue
+++ b/src/components/SectionFullScreen.vue
@@ -1,7 +1,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/TablaOperarios.vue b/src/components/TablaOperarios.vue
new file mode 100644
index 00000000..7aba2790
--- /dev/null
+++ b/src/components/TablaOperarios.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+
+ Esta seguro que desea eliminar el operario {{ operarioSeleccionado?.name }} ?
+
+ Esta operación no se puede deshacer.
+
+
+
+
+
+
+
+ Nombre
+ Tipo
+ e-mail
+ UO
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ client.name }}
+
+
+
+
+
+ {{ muestraTipoOperario(client.type) }}
+
+
+ {{ client.email }}
+
+
+ {{ nombreUO(client.ud_operativa_fk) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Page {{ currentPageHuman }} of {{ numPages }}
+
+
+
diff --git a/src/components/TablaOperarios_PrimeVue.vue b/src/components/TablaOperarios_PrimeVue.vue
new file mode 100644
index 00000000..72e8f26b
--- /dev/null
+++ b/src/components/TablaOperarios_PrimeVue.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+ Esta seguro que desea eliminar el operario {{ operarioSeleccionado?.name }} ?
+
+ Esta operación no se puede deshacer.
+
+
+
+
+
+
+
Operarios
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/UnAutorizedComponent.vue b/src/components/UnAutorizedComponent.vue
new file mode 100644
index 00000000..8e43ef6a
--- /dev/null
+++ b/src/components/UnAutorizedComponent.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
401
+ No Autorizado
+
+
+
Lo sentimos, no tienes permiso para acceder a esta página.
+
+
+
+ Volver al inicio
+
+
+
+
+
+
+
diff --git a/src/components/UserAvatar.vue b/src/components/UserAvatar.vue
index bfabb294..249d6dd5 100644
--- a/src/components/UserAvatar.vue
+++ b/src/components/UserAvatar.vue
@@ -12,19 +12,24 @@ const props = defineProps({
},
api: {
type: String,
- default: 'avataaars'
+ // default: 'avataaars'
+ default:'big-smile'
}
})
-const avatar = computed(
- () =>
- props.avatar ??
- `https://api.dicebear.com/7.x/${props.api}/svg?seed=${props.username.replace(
- /[^a-z0-9]+/gi,
- '-'
- )}.svg`
-)
+// const avatar = computed(
+// () =>
+// props.avatar ??
+// `https://api.dicebear.com/9.x/${props.api}/svg?seed=${props.username.replace(
+// /[^a-z0-9]+/gi,
+// '-'
+// )}.svg`
+// )
+const avatar = computed(
+ () =>props.avatar ??
+ `https://ui-avatars.com/api/?name=${props.username}&background=random&font-size=0.75&bold=true&color=fff&rounded=false&size=512`
+ )
const username = computed(() => props.username)
@@ -33,7 +38,7 @@ const username = computed(() => props.username)
diff --git a/src/components/UserAvatarCurrentUser.vue b/src/components/UserAvatarCurrentUser.vue
index 47c4df78..d0864441 100644
--- a/src/components/UserAvatarCurrentUser.vue
+++ b/src/components/UserAvatarCurrentUser.vue
@@ -1,12 +1,15 @@
-
+
+
diff --git a/src/components/UserCard.vue b/src/components/UserCard.vue
index df290248..2ac31add 100644
--- a/src/components/UserCard.vue
+++ b/src/components/UserCard.vue
@@ -1,16 +1,20 @@
@@ -25,15 +29,15 @@ const userSwitchVal = ref(false)
v-model="userSwitchVal"
name="notifications-switch"
type="switch"
- label="Notifications"
+ label="Activo"
:input-value="true"
/>
- Howdy, {{ userName }} {{ loginStore.userName }}!
- Last login 12 mins ago from 127.0.0.1
+
diff --git a/src/components/ZonasTable.vue b/src/components/ZonasTable.vue
new file mode 100644
index 00000000..98da1217
--- /dev/null
+++ b/src/components/ZonasTable.vue
@@ -0,0 +1,373 @@
+
+
+
+
+
+
+
+
+
+ Esta seguro que desea eliminar la Zona de Abastecimiento
+ {{ dataToEdit?.name }} ?
+
+ Esta operación no se puede deshacer.
+
+
+
+
+
Filtros de Búsqueda
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mostrando {{ zonasAbastecimiento.length }} zona(s) de abastecimiento
+
+
+
+
+
+
+
+
+
+
+ ID
+ Nombre
+ Comunidad Autonoma
+ Unidad Operativa
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ zona.id }}
+
+
+ {{ zona.name }}
+
+
+
+
+ {{ comunidadPorId(zona.com_autonoma_fk) }}
+
+
+
+ {{ unidadOperativaPorId(zona.unidades_operativas_fk) }}
+
+
+
+
+
+
+
+
+
+
+ Puntos de Muestreo
+
+
+
+
+ No hay puntos de muestreo asignados
+
+
+
+
+
+ {{ puntoMuestreo.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useAnalyticsLoader.js b/src/composables/useAnalyticsLoader.js
new file mode 100644
index 00000000..6b3bc3e5
--- /dev/null
+++ b/src/composables/useAnalyticsLoader.js
@@ -0,0 +1,98 @@
+import { ref, computed } from 'vue'
+import { usePlantasStore } from '@/stores/plantas'
+
+/**
+ * Composable para manejar la carga inteligente de analíticas
+ * Evita cargas innecesarias y optimiza el rendimiento
+ */
+export function useAnalyticsLoader() {
+ const plantasStore = usePlantasStore()
+ const loading = ref(false)
+ const error = ref(null)
+
+ // Estado de las analíticas
+ const isLoaded = computed(() => plantasStore.isAnalyticasLoaded)
+ const count = computed(() => plantasStore.analyticsCount)
+ const data = computed(() => plantasStore.getAnaliticas)
+
+ /**
+ * Carga analíticas solo si no están ya cargadas
+ * @param {boolean} force - Forzar recarga incluso si ya están cargadas
+ */
+ const loadAnalytics = async (force = false) => {
+ if (!force && isLoaded.value) {
+ console.log(`✅ Analíticas ya cargadas (${count.value} registros)`)
+ return data.value
+ }
+
+ loading.value = true
+ error.value = null
+
+ try {
+ console.log('🔄 Cargando analíticas...')
+ const result = await plantasStore.loadAnaliticas()
+ console.log(`✅ ${count.value} analíticas cargadas`)
+ return result
+ } catch (err) {
+ error.value = err
+ console.error('❌ Error cargando analíticas:', err)
+ throw err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ /**
+ * Carga analíticas para exportación
+ * Muestra advertencias apropiadas
+ */
+ const loadForExport = async () => {
+ if (!isLoaded.value) {
+ console.warn('⚠️ Cargando todas las analíticas para exportación...')
+ console.warn('💡 Considera usar exportación de seleccionados para mejor rendimiento')
+ await loadAnalytics()
+ }
+ return data.value
+ }
+
+ /**
+ * Limpia las analíticas del store
+ * Útil para liberar memoria cuando no se necesiten
+ */
+ const clearAnalytics = () => {
+ plantasStore.analiticas = []
+ console.log('🧹 Analíticas limpiadas del store')
+ }
+
+ /**
+ * Verifica si las analíticas necesitan ser cargadas para una operación específica
+ */
+ const needsLoading = (operationType = 'general') => {
+ if (isLoaded.value) return false
+
+ const messages = {
+ 'export': '📊 Se necesita cargar analíticas para exportación',
+ 'table': '📋 Se necesita cargar analíticas para mostrar tabla',
+ 'filter': '🔍 Se necesita cargar analíticas para filtrado',
+ 'general': '📈 Se necesita cargar analíticas'
+ }
+
+ console.info(messages[operationType] || messages.general)
+ return true
+ }
+
+ return {
+ // Estado
+ loading,
+ error,
+ isLoaded,
+ count,
+ data,
+
+ // Métodos
+ loadAnalytics,
+ loadForExport,
+ clearAnalytics,
+ needsLoading
+ }
+}
\ No newline at end of file
diff --git a/src/composables/useAuth.js b/src/composables/useAuth.js
new file mode 100644
index 00000000..b4c76c7a
--- /dev/null
+++ b/src/composables/useAuth.js
@@ -0,0 +1,81 @@
+// composables/useAuth.js
+import { ref, computed } from 'vue';
+import { usePermissions } from './usePermissions';
+import { supabase } from '@/services/supabase';
+import { loginWithMicrosoft, getUserProfile } from '@/services/msalConfig';
+
+export function useAuth() {
+ const user = ref(null);
+ const isLoading = ref(false);
+ const error = ref(null);
+
+ const {
+ loadUserPermissions,
+ hasPermission,
+ hasRole
+ } = usePermissions();
+
+ const isAuthenticated = computed(() => !!user.value);
+
+ // Login con Microsoft y carga de permisos
+ const login = async () => {
+ isLoading.value = true;
+ error.value = null;
+
+ try {
+ // Login con Microsoft
+ const msalResult = await loginWithMicrosoft();
+ const profile = await getUserProfile();
+
+ // Obtener roles y permisos de Supabase
+ const { data: userData, error: dbError } = await supabase
+ .from('usuarios')
+ .select(`
+ *,
+ usuario_roles (
+ roles (
+ nombre,
+ permisos
+ )
+ )
+ `)
+ .eq('email', profile.email)
+ .single();
+
+ if (dbError) throw dbError;
+
+ // Extraer roles del usuario
+ const userRoles = userData.usuario_roles.map(ur => ur.roles.nombre);
+
+ // Cargar permisos basados en los roles7Q71Do
+ loadUserPermissions(userRoles);
+
+ // Guardar datos del usuario
+ user.value = {
+ ...profile,
+ roles: userRoles
+ };
+
+ } catch (e) {
+ error.value = e.message;
+ throw e;
+ } finally {
+ isLoading.value = false;
+ }
+ };
+
+ const logout = async () => {
+ // Lógica de logout
+ };
+
+ return {
+ user,
+ isLoading,
+ error,
+ isAuthenticated,
+ hasPermission,
+ hasRole,
+ login,
+ logout
+ };
+}
\ No newline at end of file
diff --git a/src/composables/useExtractData.js b/src/composables/useExtractData.js
new file mode 100644
index 00000000..1f807e82
--- /dev/null
+++ b/src/composables/useExtractData.js
@@ -0,0 +1,250 @@
+import {
+ formatDateToSpanish,
+ excelDateToJSDate,
+ fechaFinal,
+ formatDatos,
+ codMuestraLab,
+} from "../helpers/index";
+import { codParametro, codAnalisis, codParametroOperacional } from "../helpers/data";
+import useStore from "../stores/index";
+
+export default function useExtractdata() {
+ const store = useStore();
+
+ //* Extrae los datos de cada una de las hojas
+ const datosBoletin = (sheetsData) => {
+ const sheets = Object.values(sheetsData);
+ console.log("Hojas: ", sheets);
+
+ //* (Filas) Extraemos las filas necesarias de cada una de las hojas
+ const dataVector = [];
+
+ sheets.forEach((sheet, sheetIndex) => {
+ let fecha = "";
+ try {
+ // Validar que sheet[3] existe y tiene la propiedad esperada
+ if (!sheet[3] || sheet[3].__EMPTY_1 === undefined) {
+ console.error(`Hoja ${sheetIndex + 1}: No se encontró la fecha en la celda esperada (sheet[3].__EMPTY_1)`);
+ console.log("Estructura de sheet[3]:", sheet[3]);
+ return; // Saltar esta hoja
+ }
+
+ const excelSerialDate = sheet[3].__EMPTY_1;
+ console.log(`Hoja ${sheetIndex + 1}: Valor de fecha Excel:`, excelSerialDate);
+
+ fecha = formatDateToSpanish(excelDateToJSDate(excelSerialDate));
+ console.log("FECHA convertida: ", fecha);
+ } catch (error) {
+ console.error(`Error al convertir la fecha en la hoja ${sheetIndex + 1}:`, error.message);
+ console.error("Datos de la fila 3:", sheet[3]);
+ return; // Saltar esta hoja si hay error en la fecha
+ }
+ //en el caso que sea un analisis de RUTINA
+ if (sheet[3].__EMPTY_10) {
+ // console.log("rutina", sheet[3].__EMPTY_10);
+
+ const codPuntoExtraccion1 = sheet[4].__EMPTY_6;
+ const codPuntoExtraccion2 = sheet[4].__EMPTY_12;
+
+ const rows = sheet.slice(8, 24);
+ rows.forEach((row, index) => {
+ const codAnalisis = "29";
+ //* (Filas/2) Extraemos los vectores con los datos necesarios con la fecha a principio de vector, código de análisis y código de punto de extracción
+ const data1 = [
+ fechaFinal(row.__EMPTY, fecha),
+ codAnalisis,
+ codPuntoExtraccion1,
+ formatDatos(row.__EMPTY_1),
+ row.__EMPTY_2 === 0 || row.__EMPTY_2 === 1 ? row.__EMPTY_2 : 1,
+ row.__EMPTY_3 === 0 || row.__EMPTY_3 === 1 ? row.__EMPTY_3 : 1,
+ row.__EMPTY_4 === 0 || row.__EMPTY_4 === 1 ? row.__EMPTY_4 : 1,
+ formatDatos(row.__EMPTY_5),
+ formatDatos(row.__EMPTY_6),
+ ];
+ const data2 = [
+ fechaFinal(row.__EMPTY, fecha),
+ codAnalisis,
+ codPuntoExtraccion2,
+ formatDatos(row.__EMPTY_7),
+ row.__EMPTY_8 === 0 && row.__EMPTY_8 === 1 ? row.__EMPTY_8 : 1,
+ row.__EMPTY_9 === 0 && row.__EMPTY_9 === 1 ? row.__EMPTY_9 : 1,
+ row.__EMPTY_10 === 0 && row.__EMPTY_10 === 1 ? row.__EMPTY_10 : 1,
+ formatDatos(row.__EMPTY_11),
+ formatDatos(row.__EMPTY_12),
+ ];
+
+ const insertData = (data) => {
+ if ([data[3], data[7], data[8]].some((value) => value !== "")) {
+ // console.log("DATA: ", data);
+ dataVector.push(data);
+ }
+ };
+ insertData(data1);
+ insertData(data2);
+ });
+
+ // En el caso que sea un analisis de OPERACIONAL
+ } else if (sheet[3].__EMPTY_7) {
+ const codPuntoExtraccion1 = sheet[4].__EMPTY_3;
+ const codPuntoExtraccion2 = sheet[4].__EMPTY_6;
+ const codPuntoExtraccion3 = sheet[4].__EMPTY_9;
+
+ const codAnalisis = "28";
+ const rows = sheet.slice(7, 23);
+ rows.forEach((row, index) => {
+ const data1 = [
+ fechaFinal(row.__EMPTY, fecha),
+ codAnalisis,
+ codPuntoExtraccion1,
+ formatDatos(row.__EMPTY_1),
+ formatDatos(row.__EMPTY_2),
+ formatDatos(row.__EMPTY_3),
+ ];
+ const data2 = [
+ fechaFinal(row.__EMPTY, fecha),
+ codAnalisis,
+ codPuntoExtraccion2,
+ formatDatos(row.__EMPTY_4),
+ formatDatos(row.__EMPTY_5),
+ formatDatos(row.__EMPTY_6),
+ ];
+ const data3 = [
+ fechaFinal(row.__EMPTY, fecha),
+ codAnalisis,
+ codPuntoExtraccion3,
+ formatDatos(row.__EMPTY_7),
+ formatDatos(row.__EMPTY_8),
+ formatDatos(row.__EMPTY_9),
+ ];
+
+ const data_object = {
+ fechaFinal: fechaFinal(row.__EMPTY, fecha),
+ codAnalisis: codAnalisis,
+ codPuntoExtraccion: codPuntoExtraccion1,
+ cod1: row.__EMPTY_1,
+ cod2: row.__EMPTY_2,
+ cod3: row.__EMPTY_3,
+ };
+ // console.log("DATA_OBJECT:", data_object);
+
+ const insertData = (data) => {
+ if (
+ (data[2],
+ [data[3], data[4]].some((value) => value !== "" && !isNaN(value)))
+ ) {
+ dataVector.push(data);
+ }
+ };
+ insertData(data1);
+ insertData(data2);
+ insertData(data3);
+ });
+ } else {
+ console.log("no hay nada");
+ }
+ });
+ // console.log("DATAVECTOR: ", dataVector);
+ store.dataSamples = dataVector;
+ };
+
+
+ //* Funcion de busqueda del codigo de analisis y el parametro (devuelve el objeto completo)
+ const buscaCodigoParametro = (indice, tipo) => {
+ if (tipo === "29") {
+ const parametro = codParametro.find(
+ (parametro) => parametro.indice === indice
+ );
+ return parametro
+ ? parametro
+ : { codigo: "no hay codigo", codigoAnalisis: "no hay codigo" };
+ } else if (tipo === "28") {
+ const parametro = codParametroOperacional.find(
+ (parametro) => parametro.indice === indice
+ );
+ return parametro
+ ? parametro
+ : { codigo: "no hay codigo", codigoAnalisis: "no hay codigo" };
+ }
+ };
+
+ const exportToXML = (data) => {
+ const xmlDataBoletin = data
+ .map((row) => {
+ const fechaLlegada = `${row[0]}`;
+ const idLaboratorio = 4053;
+ // const codMuestraLab = "LAB002";
+ const fechainforme = `${row[0]}`;
+ const idTipoAnalisis = `${row[1]}`;
+ const idPuntoMuestra = `${row[2]}`;
+
+ const xmlData = row
+ .slice(3)
+ .map((element, index) => {
+ if (element === "") {
+ return null;
+ } else {
+ const idParametro = codParametro[index]
+ ? buscaCodigoParametro(index, idTipoAnalisis)?.codigo
+ : "no hay codigo";
+
+ const idMetAnalisis = codAnalisis[index]
+ ? buscaCodigoParametro(index, idTipoAnalisis)?.codigoAnalisis
+ : "no hay codigo";
+ const valorCuantif = element;
+ const codAnalista = "ANAL001";
+
+ return `
+ ${idParametro}
+ ${idMetAnalisis}
+ ${
+ idParametro === "056" || idParametro === "057" || idParametro === "058"
+ ? `${valorCuantif} `
+ : `${valorCuantif} `
+ }
+ ${codAnalista}
+ `;
+ }
+ })
+ .join("\n");
+
+ return `
+
+ ${idTipoAnalisis}
+ ${idPuntoMuestra}
+ ${fechaLlegada}
+
+
+ ${fechaLlegada}
+ ${idLaboratorio}
+ ${codMuestraLab(
+ idPuntoMuestra,
+ fechaLlegada
+ )}
+ ${fechainforme}
+
+ ${xmlData}
+
+
+
+ `;
+ })
+ .join("\n");
+
+ // const xmlContent = `
+ //
+ // ${xmlDataBoletin}
+ // `;
+ const xmlContent = `\n\n${xmlDataBoletin}\n `;
+
+ const blob = new Blob([xmlContent], { type: "application/xml" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.download = `Analitica_Rutina_Operacional.xml`;
+ link.click();
+ };
+
+ return {
+ datosBoletin,
+ exportToXML,
+ };
+}
diff --git a/src/composables/useFormSelectData.js b/src/composables/useFormSelectData.js
new file mode 100644
index 00000000..5c37bd86
--- /dev/null
+++ b/src/composables/useFormSelectData.js
@@ -0,0 +1,196 @@
+import { computed, reactive, watch } from 'vue'
+import { usePlantasStore } from '@/stores/plantas'
+import { useLoginStore } from '@/stores/login'
+
+export default function useFormSelectData() {
+ const loginStore = useLoginStore()
+ const plantasStore = usePlantasStore()
+
+ const findOperarioByUser = (usuarioMail) => {
+ // Verificar que existe el store y los datos
+ if (!plantasStore?.getOperarios) return 'HOLA'
+
+ const operario = plantasStore.getOperarios.find((user) => user.email === usuarioMail)
+
+ return operario ? operario.id : null
+ }
+
+ // })
+ const operarioLogueado = computed(() => {
+ // Verificar si existe userEmail y plantasStore
+ if (!loginStore?.userEmail || !plantasStore?.getOperarios) {
+ console.warn('No hay email de usuario o no hay operarios')
+ return null
+ }
+
+ const operario = plantasStore.getOperarios.find((op) => {
+ // Validar que op.email existe
+ if (!op?.email) return false
+ return op.email.toLowerCase() === loginStore.userEmail.toLowerCase()
+ })
+
+ return operario || null
+ })
+
+ // Obtener fecha actual en formato AAAA-MM-DD
+ const today = new Date().toISOString().split('T')[0]
+
+ const form = reactive({
+ uo: '',
+ zona: null,
+ punto_muestreo_fk: null,
+ fecha: today,
+ color: 1,
+ olor: 1,
+ sabor: 1,
+ cloro: '',
+ type: '',
+ observaciones: '',
+ ph: null,
+ turbidez: null,
+ operario: '',
+ infraestructura: null,
+ fecha_inicio: null,
+ fecha_final: null
+ })
+
+ watch(
+ () => plantasStore.getOperarios,
+ () => {
+ if (operarioLogueado.value) {
+ // form.operario = operarioLogueado.value.id
+ // form.uo = operarioLogueado.value.ud_operativa_fk
+ }
+ },
+ { immediate: true }
+ )
+
+ const resetForm = () => {
+ Object.keys(form).forEach((key) => {
+ form[key] = null
+ })
+ }
+
+
+ const selectUO = computed(() => {
+ return plantasStore.getUnidadesOperativas.map((uo) => {
+ return { value: uo.id, label: uo.name }
+ })
+ })
+ const selectZona = computed(() => {
+ if (!form.uo)
+ return plantasStore.getZonas.map((zona) => {
+ return { value: zona.id, label: zona.name }
+ })
+ return plantasStore.getZonas
+ .filter((zona) => zona.unidades_operativas_fk === form.uo)
+ .map((zona) => {
+ return { value: zona.id, label: zona.name }
+ })
+ })
+
+ const selectInfraestructura = computed(() => {
+ if (!form.zona)
+ return plantasStore.getPuntosMuestreo.map((infraestructura) => {
+ return { value: infraestructura.id, label: infraestructura.name }
+ })
+ const infraestructuras = plantasStore.getZonasInfraestructuras
+ .filter((infraestructura) => infraestructura.zonas_fk === form.zona)
+ .map((infraestructura) => {
+ // console.log(':Infraestructura: ',infraestructura)
+ return {
+ value: infraestructura.infraestructuras_fk,
+ label: buscaInfraestructuraPorId(infraestructura.infraestructuras_fk)
+ }
+ })
+ return infraestructuras
+ })
+
+ const buscaInfraestructuraPorId = (id) => {
+ const infraestructura = plantasStore.getInfraestructuras.find(
+ (infraestructura) => infraestructura.id === id
+ )
+ if (infraestructura) {
+ return infraestructura.name
+ } else {
+ return ''
+ }
+ }
+
+const selectPuntosMuestra = computed(() => {
+ if (loginStore.userRole === 99) {
+ if (!form.infraestructura)
+ return plantasStore.getPuntosMuestreo.map((punto) => {
+ return { value: punto.id, label: punto.name }
+ })
+
+ return plantasStore.getPuntosMuestreo
+ .filter((punto) => punto.infraestructura_fk === form.infraestructura)
+ .map((punto) => {
+ return { value: punto.id, label: punto.name }
+ })
+ }
+
+ // Para otros roles, filtrar por las zonas del operario
+ const operarioActual = plantasStore.getOperarios.find(
+ (op) => op.email?.toLowerCase() === loginStore.userEmail?.toLowerCase()
+ )
+
+ // Si no se encuentra el operario, devolver lista vacía o lista completa
+ if (!operarioActual || !operarioActual.zonas) {
+ console.warn('Operario no encontrado o sin zonas asignadas')
+ return []
+ }
+
+ // Obtener IDs de zonas asignadas al operario
+ const zonasIds = operarioActual.zonas.map(zona =>
+ typeof zona === 'object' ? zona.id : zona
+ )
+ console.log('Zonas asignadas al operario:', zonasIds)
+
+ // if (!form.infraestructura)
+ // return plantasStore.getPuntosMuestreo.map((punto) => {
+ // return { value: punto.id, label: punto.name }
+ // })
+
+ if (!form.infraestructura)
+ return plantasStore.getPuntosMuestreo
+ .filter((punto) => (punto.activo && zonasIds.includes(punto.zona_fk)))
+ .map((punto) => {
+ return { value: punto.id, label: punto.name }
+ })
+
+ return plantasStore.getPuntosMuestreo
+ .filter((punto) => (punto.activo && punto.infraestructura_fk === form.infraestructura && zonasIds.includes(punto.zona_fk)))
+ .map((punto) => {
+ return { value: punto.id, label: punto.name }
+ })
+})
+
+
+ const operarioPorZona = computed(() => {
+ if (!form.uo)
+ return plantasStore.getOperarios.map((operario) => {
+ return { value: operario.id, label: operario.name }
+ })
+
+ return plantasStore.getOperarios
+ .filter((operario) => operario.ud_operativa_fk === form.uo)
+ .map((operario) => {
+ return { value: operario.id, label: operario.name }
+ })
+ })
+
+
+ return {
+ resetForm,
+ form,
+ selectUO,
+ selectZona,
+ selectInfraestructura,
+ selectPuntosMuestra,
+ operarioPorZona,
+ findOperarioByUser,
+ operarioLogueado
+ }
+}
diff --git a/src/composables/usePermissions.js b/src/composables/usePermissions.js
new file mode 100644
index 00000000..f0f481a4
--- /dev/null
+++ b/src/composables/usePermissions.js
@@ -0,0 +1,53 @@
+// composables/usePermissions.js
+import { ref, computed } from 'vue';
+import { PERMISSIONS, ROLES } from '@/constants/permissions';
+
+export function usePermissions() {
+ const userPermissions = ref([]);
+ const userRoles = ref([]);
+
+ // Verificar si tiene un permiso específico
+ const hasPermission = (permission) => {
+ return userPermissions.value.includes(permission);
+ };
+
+ // Verificar si tiene un rol específico
+ const hasRole = (role) => {
+ return userRoles.value.includes(role);
+ };
+
+ // Verificar si tiene alguno de los permisos
+ const hasAnyPermission = (permissions) => {
+ return permissions.some(permission => hasPermission(permission));
+ };
+
+ // Verificar si tiene todos los permisos
+ const hasAllPermissions = (permissions) => {
+ return permissions.every(permission => hasPermission(permission));
+ };
+
+ // Cargar permisos del usuario
+ const loadUserPermissions = (roles) => {
+ userRoles.value = roles;
+ const permissions = new Set();
+
+ roles.forEach(roleName => {
+ const role = ROLES[roleName.toUpperCase()];
+ if (role && role.permisos) {
+ role.permisos.forEach(permission => permissions.add(permission));
+ }
+ });
+
+ userPermissions.value = Array.from(permissions);
+ };
+
+ return {
+ userPermissions,
+ userRoles,
+ hasPermission,
+ hasRole,
+ hasAnyPermission,
+ hasAllPermissions,
+ loadUserPermissions
+ };
+}
\ No newline at end of file
diff --git a/src/composables/useServerPagination.js b/src/composables/useServerPagination.js
new file mode 100644
index 00000000..5717acc1
--- /dev/null
+++ b/src/composables/useServerPagination.js
@@ -0,0 +1,175 @@
+import { ref, reactive, computed, watch } from 'vue'
+import { getAnaliticasPaginated } from '@/services/analiticas'
+
+export function useServerPagination(initialOptions = {}) {
+ const loading = ref(false)
+ const error = ref(null)
+
+ // Estado de la paginación
+ const pagination = reactive({
+ page: 1,
+ pageSize: 20,
+ totalItems: 0,
+ totalPages: 0,
+ hasNextPage: false,
+ hasPreviousPage: false
+ })
+
+ // Estado del ordenamiento
+ const sorting = reactive({
+ sortBy: 'fecha',
+ sortOrder: 'desc'
+ })
+
+ // Filtros
+ const filters = reactive({
+ fecha_inicio: null,
+ fecha_final: null,
+ punto_muestreo_fk: null,
+ personal_fk: null,
+ type: null,
+ zona_fk: null
+ })
+
+ // Búsqueda
+ const searchText = ref('')
+
+ // Datos
+ const data = ref([])
+
+ // Computed para información de paginación
+ const paginationInfo = computed(() => ({
+ from: (pagination.page - 1) * pagination.pageSize + 1,
+ to: Math.min(pagination.page * pagination.pageSize, pagination.totalItems),
+ total: pagination.totalItems,
+ currentPage: pagination.page,
+ totalPages: pagination.totalPages
+ }))
+
+ // Función para cargar datos
+ const loadData = async () => {
+ loading.value = true
+ error.value = null
+
+ try {
+ const options = {
+ page: pagination.page,
+ pageSize: pagination.pageSize,
+ sortBy: sorting.sortBy,
+ sortOrder: sorting.sortOrder,
+ filters: { ...filters },
+ searchText: searchText.value
+ }
+
+ const result = await getAnaliticasPaginated(options)
+
+ data.value = result.data
+ pagination.totalItems = result.count
+ pagination.totalPages = result.totalPages
+ pagination.hasNextPage = result.hasNextPage
+ pagination.hasPreviousPage = result.hasPreviousPage
+
+ } catch (err) {
+ console.error('Error loading paginated data:', err)
+ error.value = err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ // Funciones de navegación
+ const goToPage = (page) => {
+ if (page >= 1 && page <= pagination.totalPages) {
+ pagination.page = page
+ }
+ }
+
+ const nextPage = () => {
+ if (pagination.hasNextPage) {
+ pagination.page++
+ }
+ }
+
+ const previousPage = () => {
+ if (pagination.hasPreviousPage) {
+ pagination.page--
+ }
+ }
+
+ const changePageSize = (newPageSize) => {
+ pagination.pageSize = newPageSize
+ pagination.page = 1 // Reset a primera página
+ }
+
+ // Funciones de ordenamiento
+ const toggleSort = (column) => {
+ if (sorting.sortBy === column) {
+ sorting.sortOrder = sorting.sortOrder === 'asc' ? 'desc' : 'asc'
+ } else {
+ sorting.sortBy = column
+ sorting.sortOrder = 'asc'
+ }
+ pagination.page = 1 // Reset a primera página
+ }
+
+ // Función para aplicar filtros
+ const applyFilters = (newFilters) => {
+ Object.assign(filters, newFilters)
+ pagination.page = 1 // Reset a primera página
+ }
+
+ // Función para limpiar filtros
+ const clearFilters = () => {
+ Object.keys(filters).forEach(key => {
+ filters[key] = null
+ })
+ searchText.value = ''
+ pagination.page = 1
+ }
+
+ // Función para buscar
+ const search = (text) => {
+ searchText.value = text
+ pagination.page = 1 // Reset a primera página
+ }
+
+ // Función de refresh
+ const refresh = () => {
+ loadData()
+ }
+
+ // Watch para recargar datos cuando cambian los parámetros
+ watch(
+ [() => pagination.page, () => pagination.pageSize, sorting, filters, searchText],
+ () => {
+ loadData()
+ },
+ { deep: true }
+ )
+
+ return {
+ // Estado
+ loading,
+ error,
+ data,
+ pagination,
+ sorting,
+ filters,
+ searchText,
+
+ // Computed
+ paginationInfo,
+
+ // Métodos
+ loadData,
+ goToPage,
+ nextPage,
+ previousPage,
+ changePageSize,
+ toggleSort,
+ applyFilters,
+ clearFilters,
+ search,
+ refresh
+ }
+}
\ No newline at end of file
diff --git a/src/composables/useSessionSecurity.js b/src/composables/useSessionSecurity.js
new file mode 100644
index 00000000..1c25d610
--- /dev/null
+++ b/src/composables/useSessionSecurity.js
@@ -0,0 +1,69 @@
+import { onMounted, onUnmounted } from 'vue'
+import { useRouter } from 'vue-router'
+import useLoginStore from '@/stores/login'
+
+export function useSessionSecurity() {
+ const loginStore = useLoginStore()
+ const router = useRouter()
+
+ const ACTIVITY_EVENTS = [
+ 'mousedown',
+ 'mousemove',
+ 'keypress',
+ 'scroll',
+ 'touchstart',
+ 'click'
+ ]
+
+ // Función para manejar actividad del usuario
+ const handleUserActivity = () => {
+ if (loginStore.isAuthenticated) {
+ loginStore.renewSession()
+ }
+ }
+
+ // Función para verificar sesión al cargar la página
+ const checkInitialSession = () => {
+ if (loginStore.isAuthenticated && !loginStore.checkSessionExpiry()) {
+ console.warn('Sesión expirada detectada al cargar la página')
+ router.push('/login')
+ }
+ }
+
+ // Configurar listeners de actividad
+ const setupActivityListeners = () => {
+ ACTIVITY_EVENTS.forEach(event => {
+ document.addEventListener(event, handleUserActivity, true)
+ })
+ }
+
+ // Remover listeners de actividad
+ const removeActivityListeners = () => {
+ ACTIVITY_EVENTS.forEach(event => {
+ document.removeEventListener(event, handleUserActivity, true)
+ })
+ }
+
+ // Función para manejar el evento beforeunload (cerrar pestaña/navegador)
+ const handleBeforeUnload = () => {
+ loginStore.clearSessionTimeout()
+ }
+
+ onMounted(() => {
+ checkInitialSession()
+ setupActivityListeners()
+ window.addEventListener('beforeunload', handleBeforeUnload)
+ })
+
+ onUnmounted(() => {
+ removeActivityListeners()
+ window.removeEventListener('beforeunload', handleBeforeUnload)
+ loginStore.clearSessionTimeout()
+ })
+
+ return {
+ checkInitialSession,
+ setupActivityListeners,
+ removeActivityListeners
+ }
+}
\ No newline at end of file
diff --git a/src/composables/useUploadFile.js b/src/composables/useUploadFile.js
new file mode 100644
index 00000000..d5894d86
--- /dev/null
+++ b/src/composables/useUploadFile.js
@@ -0,0 +1,83 @@
+import useStore from "../stores";
+import { ref } from "vue";
+import * as XLSX from "xlsx";
+import useExtractdata from "./useExtractData";
+
+export default function useUploadFile() {
+ const { datosBoletin} = useExtractdata();
+
+
+ const store = useStore();
+
+ const sheetsData = ref({});
+ const errorMessage = ref("");
+
+ //* CARGA DEL ARCHIVO EXCEL
+ const handleFileUpload = (event) => {
+ const file = event.target.files[0];
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ try {
+ const workbook = XLSX.read(e.target.result, { type: "binary" });
+
+ //* Obtener las hojas
+ // const sheetNames = workbook.SheetNames.slice(0, 3);
+ const sheetNames = workbook.SheetNames;
+
+ //* Convertir las hojas seleccionadas a JSON
+ sheetsData.value = sheetNames.reduce((acc, sheetName) => {
+ const worksheet = workbook.Sheets[sheetName];
+
+ // Usar sheet_to_json con opciones para manejar hojas vacías
+ acc[sheetName] = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
+ return acc;
+ }, {});
+
+ datosBoletin(sheetsData.value);
+ // console.log("SHEETDATA: ", sheetsData);
+
+ // Añadir mensaje de depuración
+ // console.log("Hojas leídas:", Object.keys(sheetsData.value));
+ // console.log("Object Value:", Object.values(sheetsData.value));
+ // console.log("Datos de las hojas:", sheetsData.value);
+ } catch (error) {
+ errorMessage.value = `Error al leer el archivo: ${error.message}`;
+ console.error(error);
+ }
+ };
+
+ reader.onerror = (error) => {
+ errorMessage.value = `Error al leer el archivo: ${error}`;
+ console.error(error);
+ };
+
+ reader.readAsBinaryString(file);
+ };
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ return {
+ handleFileUpload,
+ sheetsData,
+ errorMessage
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/composables/useUploadFormData.js b/src/composables/useUploadFormData.js
new file mode 100644
index 00000000..2cd36142
--- /dev/null
+++ b/src/composables/useUploadFormData.js
@@ -0,0 +1,135 @@
+
+import {
+ formatDateToSpanish,
+ excelDateToJSDate,
+ fechaFinal,
+ formatDatos,
+ codMuestraLab,
+} from "../helpers/index";
+
+import { formatDate } from "@/helpers"
+
+export default function useExtractdata() {
+ // const store = useStore();
+
+
+
+ const exportXMLData = (analiticas) => {
+ const xmlBoletines = analiticas.map(analitica => {
+ const determinaciones = []
+ const COD_ANALISTA = "ANAL001"
+ const ID_LABORATORIO = '4053'
+ const fechaFormateada= formatDate(analitica.fecha)
+
+ // Añadir determinaciones según los valores presentes
+ if (analitica.cloro !== null) {
+ determinaciones.push(`
+
+ 045
+ 50680
+ ${formatDatos(analitica.cloro)}
+ ${COD_ANALISTA}
+
+ `)
+ }
+
+ if (analitica.ph !== null) {
+ determinaciones.push(`
+
+ 051
+ 50678
+ ${formatDatos(analitica.ph)}
+ ${COD_ANALISTA}
+
+ `)
+ }
+
+ if (analitica.turbidez !== null) {
+ determinaciones.push(`
+
+ 054
+ 50679
+ ${formatDatos(analitica.turbidez)}
+ ${COD_ANALISTA}
+
+ `)
+ }
+
+ // Valores cualitativos
+ if (analitica.olor && analitica.type === 29) {
+ determinaciones.push(`
+
+ 057
+ 50960
+ ${analitica.olor}
+ ${COD_ANALISTA}
+
+ `)
+ }
+
+ if (analitica.color && analitica.type === 29) {
+ determinaciones.push(`
+
+ 056
+ 50962
+ ${analitica.color}
+ ${COD_ANALISTA}
+
+ `)
+ }
+
+ if (analitica.sabor && analitica.type === 29) {
+ determinaciones.push(`
+
+ 058
+ 50961
+ ${analitica.sabor}
+ ${COD_ANALISTA}
+
+ `)
+ }
+
+ return `
+
+ ${analitica.type}
+ ${analitica.punto_muestreo_fk}
+ ${fechaFormateada}
+
+
+ ${fechaFormateada}
+ ${ID_LABORATORIO}
+ ${codMuestraLab(
+ analitica.punto_muestreo_fk,
+ analitica.fecha.slice(8,10)
+ )}
+ ${fechaFormateada}
+
+ ${determinaciones.join('\n')}
+
+
+
+
+ `
+ }).join('\n')
+
+ // return `
+ //
+ // ${xmlBoletines}
+ // `
+
+ return`\n\n${xmlBoletines}\n `;
+ }
+
+
+
+
+
+
+
+
+
+
+ return {
+ exportXMLData
+ };
+}
diff --git a/src/constants/flags.js b/src/constants/flags.js
new file mode 100644
index 00000000..3aa1f73f
--- /dev/null
+++ b/src/constants/flags.js
@@ -0,0 +1,22 @@
+export const COMUNIDADES_FLAGS = {
+ 1: 'https://upload.wikimedia.org/wikipedia/commons/1/16/Flag_of_the_Valencian_Community_%282x3%29.svg',
+ 2: 'https://upload.wikimedia.org/wikipedia/commons/2/20/Flag_of_Andaluc%C3%ADa.svg',
+ 3: 'https://upload.wikimedia.org/wikipedia/commons/1/18/Flag_of_Aragon.svg',
+ 4: 'https://upload.wikimedia.org/wikipedia/commons/3/3e/Flag_of_Asturias.svg',
+ 5: 'https://upload.wikimedia.org/wikipedia/commons/7/7b/Flag_of_the_Balearic_Islands.svg',
+ 6: 'https://upload.wikimedia.org/wikipedia/commons/b/b0/Flag_of_the_Canary_Islands.svg',
+ 7: 'https://upload.wikimedia.org/wikipedia/commons/0/0f/Flag_of_Cantabria_%28Official%29.svg',
+ 8: 'https://upload.wikimedia.org/wikipedia/commons/1/13/Flag_of_Castile_and_Le%C3%B3n.svg',
+ 9: 'https://upload.wikimedia.org/wikipedia/commons/d/d4/Bandera_Castilla-La_Mancha.svg',
+ 10: 'https://upload.wikimedia.org/wikipedia/commons/c/ce/Flag_of_Catalonia.svg',
+ 11: 'https://upload.wikimedia.org/wikipedia/commons/1/13/Flag_of_Extremadura%2C_Spain_%28with_coat_of_arms%29.svg',
+ 12: 'https://upload.wikimedia.org/wikipedia/commons/6/64/Flag_of_Galicia.svg',
+ 13: 'https://upload.wikimedia.org/wikipedia/commons/9/9c/Flag_of_the_Community_of_Madrid.svg',
+ 14: 'https://upload.wikimedia.org/wikipedia/commons/a/a5/Flag_of_the_Region_of_Murcia.svg',
+ 15: 'https://upload.wikimedia.org/wikipedia/commons/f/f1/Bandera_de_Reino_de_Navarra.svg',
+ 16: 'https://upload.wikimedia.org/wikipedia/commons/2/2d/Flag_of_the_Basque_Country.svg',
+ 17: 'https://upload.wikimedia.org/wikipedia/commons/d/db/Flag_of_La_Rioja_%28with_coat_of_arms%29.svg',
+ 18: 'https://upload.wikimedia.org/wikipedia/commons/d/d3/Flag_of_Ceuta.svg',
+ 19: 'https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Melilla.svg',
+ };
+
\ No newline at end of file
diff --git a/src/constants/permissions.js b/src/constants/permissions.js
new file mode 100644
index 00000000..903cfd1a
--- /dev/null
+++ b/src/constants/permissions.js
@@ -0,0 +1,147 @@
+// constants/permissions.js
+
+// Categorías de Permisos
+export const PERMISSION_CATEGORIES = {
+ ANALITICAS: 'ANALITICAS',
+ INFRAESTRUCTURAS: 'INFRAESTRUCTURAS',
+ PUNTOS_MUESTREO: 'PUNTOS_MUESTREO',
+ USUARIOS: 'USUARIOS'
+ };
+
+ // Permisos detallados por categoría
+ export const PERMISSIONS = {
+ ANALITICAS: {
+ VER: 'analiticas:ver',
+ CREAR: 'analiticas:crear',
+ EDITAR: 'analiticas:editar',
+ ELIMINAR: 'analiticas:eliminar',
+ APROBAR: 'analiticas:aprobar',
+ EXPORTAR: 'analiticas:exportar'
+ },
+
+ INFRAESTRUCTURAS: {
+ VER: 'infraestructuras:ver',
+ CREAR: 'infraestructuras:crear',
+ EDITAR: 'infraestructuras:editar',
+ ELIMINAR: 'infraestructuras:eliminar'
+ },
+
+ PUNTOS_MUESTREO: {
+ VER: 'puntos_muestreo:ver',
+ CREAR: 'puntos_muestreo:crear',
+ EDITAR: 'puntos_muestreo:editar',
+ ELIMINAR: 'puntos_muestreo:eliminar'
+ },
+
+ USUARIOS: {
+ VER: 'usuarios:ver',
+ CREAR: 'usuarios:crear',
+ EDITAR: 'usuarios:editar',
+ ELIMINAR: 'usuarios:eliminar',
+ GESTIONAR_ROLES: 'roles:gestionar'
+ }
+ };
+
+ // Roles predefinidos con sus permisos
+ export const ROLES = {
+ ADMIN: {
+ nombre: 'admin',
+ descripcion: 'Administrador con acceso total al sistema',
+ permisos: [
+ // Todos los permisos de ANALITICAS
+ PERMISSIONS.ANALITICAS.VER,
+ PERMISSIONS.ANALITICAS.CREAR,
+ PERMISSIONS.ANALITICAS.EDITAR,
+ PERMISSIONS.ANALITICAS.ELIMINAR,
+ PERMISSIONS.ANALITICAS.APROBAR,
+ PERMISSIONS.ANALITICAS.EXPORTAR,
+ // Todos los permisos de INFRAESTRUCTURAS
+ PERMISSIONS.INFRAESTRUCTURAS.VER,
+ PERMISSIONS.INFRAESTRUCTURAS.CREAR,
+ PERMISSIONS.INFRAESTRUCTURAS.EDITAR,
+ PERMISSIONS.INFRAESTRUCTURAS.ELIMINAR,
+ // Todos los permisos de PUNTOS_MUESTREO
+ PERMISSIONS.PUNTOS_MUESTREO.VER,
+ PERMISSIONS.PUNTOS_MUESTREO.CREAR,
+ PERMISSIONS.PUNTOS_MUESTREO.EDITAR,
+ PERMISSIONS.PUNTOS_MUESTREO.ELIMINAR,
+ // Todos los permisos de USUARIOS
+ PERMISSIONS.USUARIOS.VER,
+ PERMISSIONS.USUARIOS.CREAR,
+ PERMISSIONS.USUARIOS.EDITAR,
+ PERMISSIONS.USUARIOS.ELIMINAR,
+ PERMISSIONS.USUARIOS.GESTIONAR_ROLES
+ ]
+ },
+
+ SUPERVISOR: {
+ nombre: 'supervisor',
+ descripcion: 'Supervisor con capacidad de gestión y aprobación',
+ permisos: [
+ // Permisos de ANALITICAS
+ PERMISSIONS.ANALITICAS.VER,
+ PERMISSIONS.ANALITICAS.CREAR,
+ PERMISSIONS.ANALITICAS.EDITAR,
+ PERMISSIONS.ANALITICAS.APROBAR,
+ PERMISSIONS.ANALITICAS.EXPORTAR,
+ // Permisos de INFRAESTRUCTURAS
+ PERMISSIONS.INFRAESTRUCTURAS.VER,
+ PERMISSIONS.INFRAESTRUCTURAS.EDITAR,
+ // Permisos de PUNTOS_MUESTREO
+ PERMISSIONS.PUNTOS_MUESTREO.VER,
+ PERMISSIONS.PUNTOS_MUESTREO.EDITAR,
+ // Permisos de USUARIOS
+ PERMISSIONS.USUARIOS.VER
+ ]
+ },
+
+ OPERARIO: {
+ nombre: 'operario',
+ descripcion: 'Operario con acceso básico a análisis',
+ permisos: [
+ // Permisos de ANALITICAS
+ PERMISSIONS.ANALITICAS.VER,
+ PERMISSIONS.ANALITICAS.CREAR,
+ PERMISSIONS.ANALITICAS.EDITAR,
+ // Permisos de INFRAESTRUCTURAS
+ PERMISSIONS.INFRAESTRUCTURAS.VER,
+ // Permisos de PUNTOS_MUESTREO
+ PERMISSIONS.PUNTOS_MUESTREO.VER
+ ]
+ },
+
+ CONSULTOR: {
+ nombre: 'consultor',
+ descripcion: 'Usuario con acceso solo de lectura',
+ permisos: [
+ // Permisos de solo lectura
+ PERMISSIONS.ANALITICAS.VER,
+ PERMISSIONS.ANALITICAS.EXPORTAR,
+ PERMISSIONS.INFRAESTRUCTURAS.VER,
+ PERMISSIONS.PUNTOS_MUESTREO.VER
+ ]
+ }
+ };
+
+ // Funciones auxiliares para verificar permisos
+ export const hasPermission = (userPermissions, requiredPermission) => {
+ return userPermissions.includes(requiredPermission);
+ };
+
+ export const hasAnyPermission = (userPermissions, requiredPermissions) => {
+ return requiredPermissions.some(permission => userPermissions.includes(permission));
+ };
+
+ export const hasAllPermissions = (userPermissions, requiredPermissions) => {
+ return requiredPermissions.every(permission => userPermissions.includes(permission));
+ };
+
+ // Función para obtener todos los permisos de una categoría
+ export const getPermissionsByCategory = (category) => {
+ return Object.values(PERMISSIONS[category]);
+ };
+
+ // Función para verificar si un permiso pertenece a una categoría
+ export const isPermissionInCategory = (permission, category) => {
+ return Object.values(PERMISSIONS[category]).includes(permission);
+ };
\ No newline at end of file
diff --git a/src/css/main.css b/src/css/main.css
index ec48ad25..3f00aeaa 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -1,5 +1,6 @@
@import 'tailwind/_base.css';
@import 'tailwind/_components.css';
+/* @import '../assets/primevue/tailwind.css'; */
@import 'tailwind/_utilities.css';
@import '_checkbox-radio-switch.css';
diff --git a/src/css/styles/_basic-copy.css b/src/css/styles/_basic-copy.css
new file mode 100644
index 00000000..9f922687
--- /dev/null
+++ b/src/css/styles/_basic-copy.css
@@ -0,0 +1,31 @@
+@layer components {
+ .style-basic:not(.dark) {
+ .aside {
+ @apply bg-gray-800;
+ }
+ .aside-scrollbars {
+ @apply aside-scrollbars-gray;
+ }
+ .aside-brand {
+ @apply bg-gray-900 text-white;
+ }
+ .aside-menu-item {
+ @apply text-gray-300 hover:text-white;
+ }
+ .aside-menu-item-active {
+ @apply text-white;
+ }
+ .aside-menu-dropdown {
+ @apply bg-gray-700/50;
+ }
+ .navbar-item-label {
+ @apply text-black hover:text-blue-500;
+ }
+ .navbar-item-label-active {
+ @apply text-blue-600;
+ }
+ .overlay {
+ @apply from-gray-700 via-gray-900 to-gray-700;
+ }
+ }
+}
diff --git a/src/css/styles/_basic.css b/src/css/styles/_basic.css
index 9f922687..c2aebfa8 100644
--- a/src/css/styles/_basic.css
+++ b/src/css/styles/_basic.css
@@ -1,31 +1,34 @@
@layer components {
.style-basic:not(.dark) {
.aside {
- @apply bg-gray-800;
+ @apply bg-white;
}
.aside-scrollbars {
- @apply aside-scrollbars-gray;
- }
- .aside-brand {
- @apply bg-gray-900 text-white;
+ @apply aside-scrollbars-light;
}
.aside-menu-item {
- @apply text-gray-300 hover:text-white;
+ @apply text-blue-600 hover:text-black;
}
.aside-menu-item-active {
- @apply text-white;
+ @apply text-black;
}
.aside-menu-dropdown {
- @apply bg-gray-700/50;
+ @apply bg-gray-100/75;
}
.navbar-item-label {
- @apply text-black hover:text-blue-500;
+ @apply text-blue-600;
+ }
+ .navbar-item-label-hover {
+ @apply hover:text-black;
}
.navbar-item-label-active {
- @apply text-blue-600;
+ @apply text-black;
}
.overlay {
- @apply from-gray-700 via-gray-900 to-gray-700;
+ @apply from-white via-gray-100 to-white;
}
}
}
+
+
+
\ No newline at end of file
diff --git a/src/css/tailwind/_components.css b/src/css/tailwind/_components.css
index 020aabaf..bd92ffe2 100644
--- a/src/css/tailwind/_components.css
+++ b/src/css/tailwind/_components.css
@@ -1 +1,2 @@
@tailwind components;
+
diff --git a/src/helpers/cors.js b/src/helpers/cors.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/helpers/data.js b/src/helpers/data.js
new file mode 100644
index 00000000..22c745fc
--- /dev/null
+++ b/src/helpers/data.js
@@ -0,0 +1,22 @@
+export const codParametro = [
+ { indice: 0, codigo: "045", codigoAnalisis: "50680", nombre: "Cloro Libre Residual (CLR)"},
+ { indice: 1, codigo: "057", codigoAnalisis: "50960", nombre: "Olor" },
+ { indice: 2, codigo: "056", codigoAnalisis: "50962", nombre: "Color" },
+ { indice: 3, codigo: "058", codigoAnalisis: "50961", nombre: "Sabor" },
+ { indice: 4, codigo: "051", codigoAnalisis: "50678", nombre: "pH" },
+ { indice: 5, codigo: "054", codigoAnalisis: "50679", nombre: "Turbidez" },
+];
+export const codParametroOperacional = [
+ { indice: 0, codigo: "045", codigoAnalisis: "50680" ,nombre: "Cloro Libre Residual (CLR)" },
+ { indice: 1, codigo: "051", codigoAnalisis: "50678",nombre: "pH" },
+ { indice: 2, codigo: "054", codigoAnalisis: "50679",nombre: "Turbidez" },
+];
+
+export const codAnalisis = [
+ { indice: 0, codigo: "50680", nombre: "Cloro Libre Residual (CLR)" },
+ { indice: 1, codigo: "50960", nombre: "Olor" },
+ { indice: 2, codigo: "50962", nombre: "Color" },
+ { indice: 3, codigo: "50961", nombre: "Sabor" },
+ { indice: 4, codigo: "50678", nombre: "pH" },
+ { indice: 5, codigo: "50679", nombre: "Turbidez" },
+];
diff --git a/src/helpers/index.js b/src/helpers/index.js
new file mode 100644
index 00000000..12d02965
--- /dev/null
+++ b/src/helpers/index.js
@@ -0,0 +1,90 @@
+import { format } from "date-fns";
+import { es } from "date-fns/locale";
+
+export const formatDateToSpanish = (date) => {
+ if (!date) {
+ throw new Error('Date is required');
+ }
+
+ const dateObj = new Date(date);
+
+ if (isNaN(dateObj.getTime())) {
+ throw new Error(`Invalid date value: ${date}`);
+ }
+
+ return format(dateObj, "/MM/yyyy", { locale: es });
+};
+
+export const excelDateToJSDate = (serial) => {
+ if (serial === null || serial === undefined) {
+ throw new Error('Excel serial date is null or undefined');
+ }
+
+ if (typeof serial !== 'number' || isNaN(serial)) {
+ throw new Error(`Invalid Excel serial date: ${serial}`);
+ }
+
+ // Excel dates typically range from 1 (1900-01-01) to ~60000 (year 2064)
+ if (serial < 1 || serial > 100000) {
+ throw new Error(`Excel serial date out of valid range: ${serial}`);
+ }
+
+ const utc_days = Math.floor(serial - 25569);
+ const utc_value = utc_days * 86400;
+ const date_info = new Date(utc_value * 1000);
+
+ const fractional_day = serial - Math.floor(serial) + 0.0000001;
+
+ let total_seconds = Math.floor(86400 * fractional_day);
+
+ const seconds = total_seconds % 60;
+
+ total_seconds -= seconds;
+
+ const hours = Math.floor(total_seconds / (60 * 60));
+ const minutes = Math.floor(total_seconds / 60) % 60;
+
+ return new Date(
+ date_info.getFullYear(),
+ date_info.getMonth(),
+ date_info.getDate(),
+ hours,
+ minutes,
+ seconds
+ );
+};
+
+export const formatDate = (dateString) => {
+ const date = new Date(dateString);
+ const day = date.getDate().toString().padStart(2, '0');
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const year = date.getFullYear();
+ return `${day}/${month}/${year}`;
+};
+
+export const fechaFinal = (dia, fecha) => {
+ return `${dia.toString().padStart(2, "0")}${fecha}`;
+};
+
+export const formatDatos = (data) => {
+ if (data === '') {
+ return '';
+ }
+ else {
+ return parseFloat(data).toFixed(1);
+ }
+ // return parseFloat(data).toFixed(1);
+
+};
+
+//*Genera el código de la muestra de laboratorio
+export const codMuestraLab = ((puntMuestra, fecha) => {
+const today = new Date();
+const year = today.getFullYear();
+const month = (today.getMonth() + 1).toString().padStart(2, '0');
+ const day = today.getDate().toString().padStart(2, '0');
+ const random= Math.random().toString().slice(2, 6);//genera un numero aleatorio de 4 cifras
+
+return `LAB${year}${month}${day}${puntMuestra}_${fecha}${random}`
+.split('/')[0]
+ });
diff --git a/src/helpers/maps.js b/src/helpers/maps.js
new file mode 100644
index 00000000..d6a94787
--- /dev/null
+++ b/src/helpers/maps.js
@@ -0,0 +1,25 @@
+import { usePlantasStore } from "@/stores/plantas";
+const plantaStore = usePlantasStore()
+
+
+export const getTipoInfrastructuraByPunto = (infraId) => {
+ const infraestructura = plantaStore.getInfraestructuras.find((infra) => infra.id === infraId)?.type
+ return infraestructura
+ }
+
+export const getIconByInfraestructura = (infId) => {
+ switch (getTipoInfrastructuraByPunto(infId)) {
+ case 1:
+ return 'water'
+ // return 'faucet'
+ case 2:
+ return 'ring'
+ case 3:
+ return 'flask'
+ case 4:
+ return 'route'
+ default:
+ return 'faucet'
+ }
+}
+
diff --git a/src/layouts/LayoutAuthenticated.vue b/src/layouts/LayoutAuthenticated.vue
index b4884cc0..924a2638 100644
--- a/src/layouts/LayoutAuthenticated.vue
+++ b/src/layouts/LayoutAuthenticated.vue
@@ -11,6 +11,15 @@ import NavBar from '@/components/NavBar.vue'
import NavBarItemPlain from '@/components/NavBarItemPlain.vue'
import AsideMenu from '@/components/AsideMenu.vue'
import FooterBar from '@/components/FooterBar.vue'
+import useLoginStore from '@/stores/login.js'
+import UnAutorizedComponent from '@/components/UnAutorizedComponent.vue'
+import { useSessionSecurity } from '@/composables/useSessionSecurity.js'
+
+
+const loginStore = useLoginStore()
+
+// Inicializar seguridad de sesión
+useSessionSecurity()
const layoutAsidePadding = 'xl:pl-60'
@@ -31,14 +40,20 @@ const menuClick = (event, item) => {
darkModeStore.set()
}
- if (item.isLogout) {
- //
+ if (item.isLogout) {
+ console.log('logout')
+ loginStore.logout()
+ router.push('/login')
+
+
+
}
}
{
class="pt-14 min-h-screen w-screen transition-position lg:w-auto bg-gray-50 dark:bg-slate-800 dark:text-slate-100"
>
{
/>
- Get more with
- Premium version AQLARA
+ Ciclo Integral de Agua
+
+
+
+
diff --git a/src/main.js b/src/main.js
index e3f2645c..2ab09552 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,9 +1,17 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
+import { plugin, defaultConfig } from '@formkit/vue'
+import PrimeVue from 'primevue/config'
+
+import config from '../formkit.config'
+import Vueform from '@vueform/vueform'
+import vueformConfig from '../vueform.config'
+import '@formkit/themes/genesis'
import App from './App.vue'
import router from './router'
import { useMainStore } from '@/stores/main.js'
+// import "./assets/base.css"
import './css/main.css'
@@ -11,10 +19,17 @@ import './css/main.css'
const pinia = createPinia()
// Create Vue app
-createApp(App).use(router).use(pinia).mount('#app')
+createApp(App)
+ .use(pinia)
+ .use(router)
+ .use(PrimeVue, { theme: 'none' })
+ .use(plugin, defaultConfig(config))
+ .use(Vueform, vueformConfig)
+ .mount('#app')
// Init main store
const mainStore = useMainStore(pinia)
+// await supabase.initialize() // Uso de Top-level await
// Fetch sample data
mainStore.fetchSampleClients()
@@ -34,7 +49,7 @@ mainStore.fetchSampleHistory()
// }
// Default title tag
-const defaultDocumentTitle = 'Admin One Vue 3 Tailwind'
+const defaultDocumentTitle = 'AQLARA Admin Panel'
// Set document title from route meta
router.afterEach((to) => {
diff --git a/src/menuAside.js b/src/menuAside.js
index 3ee9ae00..3e6e4163 100644
--- a/src/menuAside.js
+++ b/src/menuAside.js
@@ -1,86 +1,45 @@
import {
- mdiAccountCircle,
- mdiMonitor,
- mdiGithub,
- mdiLock,
- mdiAlertCircle,
- mdiSquareEditOutline,
- mdiTable,
- mdiViewList,
- mdiTelevisionGuide,
- mdiResponsive,
- mdiPalette,
- mdiReact
+ mdiWaterCircle,
} from '@mdi/js'
export default [
{
- to: '/dashboard',
- icon: mdiMonitor,
- label: 'Dashboard'
- },
- {
- to: '/tables',
- label: 'Tables',
- icon: mdiTable
- },
- {
- to: '/forms',
- label: 'Forms',
- icon: mdiSquareEditOutline
- },
- {
- to: '/ui',
- label: 'UI',
- icon: mdiTelevisionGuide
- },
- {
- to: '/responsive',
- label: 'Responsive',
- icon: mdiResponsive
- },
- {
- to: '/',
- label: 'Styles',
- icon: mdiPalette
- },
- {
- to: '/profile',
- label: 'Profile',
- icon: mdiAccountCircle
- },
- {
- to: '/login',
- label: 'Login',
- icon: mdiLock
- },
- {
- to: '/error',
- label: 'Error',
- icon: mdiAlertCircle
- },
- {
- label: 'Dropdown',
- icon: mdiViewList,
+ label: 'SinAQ',
+ icon: mdiWaterCircle,
menu: [
{
- label: 'Item One'
+ label: 'Excel uploader',
+ to: '/sinaq',
+ maxRole: 1,
+ minRole: 90
+ // icon: mdiWaterCircle
+ },
+ {
+ label: 'Formulario Analítica',
+ to: {name: "forms"},
+ // icon: mdiSquareEditOutline
},
{
- label: 'Item Two'
- }
+ label: 'Analíticas',
+ to: { name: "tablaAnaliticas" },
+ // maxRole: 1,
+ // minRole: 90
+ // icon: mdiSquareEditOutline
+ },
+ {
+ label: 'Mapa',
+ to: {name: "mapa"},
+ },
+ // {
+ // label: 'Prime Table Example',
+ // to: {name: "prime-table"},
+ // icon: mdiSquareEditOutline
+ // }
]
},
- {
- href: 'https://github.com/justboil/admin-one-vue-tailwind',
- label: 'GitHub',
- icon: mdiGithub,
- target: '_blank'
- },
- {
- href: 'https://github.com/justboil/admin-one-react-tailwind',
- label: 'React version',
- icon: mdiReact,
- target: '_blank'
- }
+ // {
+ // label: 'Panel de control',
+ // to: {name:'settings'},
+ // icon: mdiCog,
+ // }
]
diff --git a/src/menuNavBar.js b/src/menuNavBar.js
index 63dc060a..d0bd20e6 100644
--- a/src/menuNavBar.js
+++ b/src/menuNavBar.js
@@ -1,37 +1,57 @@
import {
mdiMenu,
- mdiClockOutline,
- mdiCloud,
- mdiCrop,
mdiAccount,
mdiCogOutline,
mdiEmail,
mdiLogout,
mdiThemeLightDark,
- mdiGithub,
- mdiReact
+ mdiAccountCog,
+ mdiMapClock,
+ mdiTestTube,
+ mdiSetCenter,
+ mdiPipeDisconnected,
+
} from '@mdi/js'
export default [
{
icon: mdiMenu,
- label: 'Sample menu',
+ label: 'Administración',
+ maxRole: 0,
+ minRole: 90,
menu: [
{
- icon: mdiClockOutline,
- label: 'Item One'
+ icon: mdiAccountCog,
+ label: 'Personal',
+ to: {name:'operarios'}
},
{
- icon: mdiCloud,
- label: 'Item Two'
+ icon: mdiSetCenter,
+ label: 'Unidades Operativas',
+ to: {name:'unidadesOperativas'}
},
{
- isDivider: true
+ icon: mdiMapClock,
+ label: 'Zonas',
+ to: {name:'zonas'}
},
{
- icon: mdiCrop,
- label: 'Item Last'
- }
+ icon: mdiPipeDisconnected,
+ label: 'Infraestructuras',
+ to: {name:'infraestructuras'}
+ },
+ {
+ icon: mdiTestTube,
+ label: 'Puntos de Muestreo',
+ to: {name:'puntosMuestreo'}
+ },
+ {
+ isDivider: true
+ },
+ // {
+ // icon: mdiTestTube,
+ // label: 'analiticas'
+ // }
]
},
{
@@ -39,7 +59,7 @@ export default [
menu: [
{
icon: mdiAccount,
- label: 'My Profile',
+ label: 'Mi Perfil',
to: '/profile'
},
{
@@ -48,14 +68,14 @@ export default [
},
{
icon: mdiEmail,
- label: 'Messages'
+ label: 'Mensajes'
},
{
isDivider: true
},
{
icon: mdiLogout,
- label: 'Log Out',
+ label: 'Salir',
isLogout: true
}
]
@@ -66,20 +86,8 @@ export default [
isDesktopNoLabel: true,
isToggleLightDark: true
},
- {
- icon: mdiGithub,
- label: 'GitHub',
- isDesktopNoLabel: true,
- href: 'https://github.com/justboil/admin-one-vue-tailwind',
- target: '_blank'
- },
- {
- icon: mdiReact,
- label: 'React version',
- isDesktopNoLabel: true,
- href: 'https://github.com/justboil/admin-one-react-tailwind',
- target: '_blank'
- },
+
+
{
icon: mdiLogout,
label: 'Log out',
diff --git a/src/router/index.js b/src/router/index.js
index ca6cd4eb..3bbd65b6 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,26 +1,110 @@
-import { createRouter, createWebHashHistory } from 'vue-router'
-import Style from '@/views/StyleView.vue'
+import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '@/views/HomeView.vue'
+import useLoginStore from '@/stores/login';
+import LoginAqlaraView from '@/views/LoginAqlaraView.vue';
+import UnAutorizedComponent from '@/components/UnAutorizedComponent.vue';
+
+// const loginStore = useLoginStore();
const routes = [
{
meta: {
- title: 'Select style'
+ title: 'Aqlara Login'
+ },
+ path: '/login',
+ name: 'login',
+ component: LoginAqlaraView
+ },
+ {
+ path: '/unauthorized',
+ name: 'Unauthorized',
+ component: UnAutorizedComponent
+ },
+ {
+ meta: {
+ title: 'Aqlara Admin Panel',
+ requiresAuth: true,
+ requiredRole: 'admin'
+ },
+ path: '/admin',
+ name: 'admin',
+ children: [
+ {
+ title: 'Operarios',
+ path: '/admin/operarios',
+ name: 'operarios',
+ component: () => import('@/views/OperariosView.vue')
+ },
+ {
+ title: 'Unidades Operativas',
+ path: '/admin/unidades_operativas',
+ name: 'unidadesOperativas',
+ component: () => import('@/views/UOTableView.vue')
+ },
+ {
+ title: 'Zonas',
+ path: '/admin/zonas',
+ name: 'zonas',
+ component: () => import('@/views/ZonasAdminView.vue')
+ },
+ {
+ title: 'Infraestructuras',
+ path: '/admin/infraestructuras',
+ name: 'infraestructuras',
+ component: () => import('@/views/InfraestructurasAdminView.vue')
+ },
+ {
+ title: 'Puntos de Muestreo',
+ path: '/admin/puntos_muestreo',
+ name: 'puntosMuestreo',
+ component: () => import('@/views/PuntosMuestreoAdminView.vue')
+ },
+
+
+ ],
+ },
+ {
+ meta: {
+ title: 'Aqlara Home',
+ requiresAuth: true
},
path: '/',
- name: 'style',
- component: Style
+ name: 'home',
+ component: () => import('@/views/ExcelUploaderView.vue')
+ },
+ {
+ meta: {
+ title: 'Mapa de Puntos de Muestreo',
+ requiresAuth: true
+ },
+ path: '/mapa',
+ name: 'mapa',
+ component: () => import('@/views/MapaPuntoMuestrasView.vue')
},
{
// Document title tag
// We combine it with defaultDocumentTitle set in `src/main.js` on router.afterEach hook
meta: {
- title: 'Dashboard'
+ title: 'Dashboard',
+ requiresAuth: true
},
path: '/dashboard',
name: 'dashboard',
component: Home
},
+ {
+ // Document title tag
+ // We combine it with defaultDocumentTitle set in `src/main.js` on router.afterEach hook
+ meta: {
+ title: 'Panel de Control',
+ requiresAuth: true,
+ requiredRole: 'admin'
+ },
+ path: '/settings',
+ name: 'settings',
+ component: ()=>import('@/views/SettingsView.vue')
+ },
+
{
meta: {
title: 'Tables'
@@ -31,11 +115,21 @@ const routes = [
},
{
meta: {
- title: 'Forms'
+ title: 'Forms',
+ requiresAuth: true
},
path: '/forms',
name: 'forms',
- component: () => import('@/views/FormsView.vue')
+ component: () => import('@/views/FormAnaliticaView.vue')
+ },
+ {
+ meta: {
+ title: 'Tabla de Analiticas',
+ requiresAuth: true
+ },
+ path: '/tablaAnaliticas',
+ name: 'tablaAnaliticas',
+ component: () => import('@/views/AnaliticsTableView.vue')
},
{
meta: {
@@ -65,8 +159,8 @@ const routes = [
meta: {
title: 'Login'
},
- path: '/login',
- name: 'login',
+ path: '/login_original',
+ name: 'login_original',
component: () => import('@/views/LoginView.vue')
},
{
@@ -76,7 +170,32 @@ const routes = [
path: '/error',
name: 'error',
component: () => import('@/views/ErrorView.vue')
- }
+ },
+ {
+ meta: {
+ title: 'Sinaq'
+ },
+ path: '/sinaq',
+ name: 'sinaq',
+ component: () => import('@/views/ExcelUploaderView.vue')
+ },
+ {
+ meta: {
+ title: 'Style selector'
+ },
+ path: '/style',
+ name: 'style',
+ component: () => import('@/views/StyleView.vue')
+ },
+ // {
+ // meta: {
+ // title: 'PrimeVue Table Example',
+ // requiresAuth: true
+ // },
+ // path: '/prime-table',
+ // name: 'prime-table',
+ // component: () => import('@/views/PrimeTableExampleView.vue')
+ // }
]
const router = createRouter({
@@ -87,4 +206,59 @@ const router = createRouter({
}
})
+// Función para verificar permisos de usuario
+const checkUserPermission = (userRole, requiredRole) => {
+ // Normalizar roles de admin: '99', 99, y 'admin' son equivalentes
+ const adminRoles = ['admin', '99', 99]
+
+ if (requiredRole === 'admin') {
+ return adminRoles.includes(userRole)
+ }
+
+ // Para otros roles, comparación directa
+ return userRole === requiredRole
+}
+
+// Guard de navegacion mejorado con seguridad de sesión
+
+router.beforeEach((to, from, next) => {
+ const loginStore = useLoginStore();
+ const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
+
+ if (requiresAuth) {
+ // Verificar si está autenticado
+ if (!loginStore.isAuthenticated) {
+ console.warn('Usuario no autenticado, redirigiendo a login')
+ next({name: 'login'});
+ return;
+ }
+
+ // Verificar si la sesión ha expirado
+ if (!loginStore.checkSessionExpiry()) {
+ console.warn('Sesión expirada, redirigiendo a login')
+ next({name: 'login'});
+ return;
+ }
+
+ // Verificar autorización por roles si es requerido
+ const requiredRole = to.meta.requiredRole;
+ if (requiredRole) {
+ const hasPermission = checkUserPermission(loginStore.userRole, requiredRole);
+ if (!hasPermission) {
+ console.warn('Usuario sin permisos suficientes', {
+ userRole: loginStore.userRole,
+ requiredRole: requiredRole
+ });
+ next({name: 'Unauthorized'});
+ return;
+ }
+ }
+
+ // Renovar sesión en cada navegación autenticada
+ loginStore.renewSession();
+ }
+
+ next();
+})
+
export default router
diff --git a/src/services/analiticas.js b/src/services/analiticas.js
new file mode 100644
index 00000000..c460b183
--- /dev/null
+++ b/src/services/analiticas.js
@@ -0,0 +1,99 @@
+import { supabase } from './supabase'
+
+export const getAnaliticas = async() => {
+ const { data } = await supabase.from('analiticas').select('*')
+ return data
+ }
+
+export const getAnaliticasPaginated = async (options = {}) => {
+ const {
+ page = 1,
+ pageSize = 20,
+ sortBy = 'fecha',
+ sortOrder = 'desc',
+ filters = {},
+ searchText = ''
+ } = options
+
+ let query = supabase
+ .from('analiticas')
+ .select(`
+ *,
+ personal:personal_fk(id, name),
+ punto_muestreo:punto_muestreo_fk(id, name, zona_fk)
+ `, { count: 'exact' })
+
+ // Aplicar filtros
+ if (filters.fecha_inicio) {
+ query = query.gte('fecha', filters.fecha_inicio)
+ }
+ if (filters.fecha_final) {
+ query = query.lte('fecha', filters.fecha_final)
+ }
+ if (filters.punto_muestreo_fk) {
+ query = query.eq('punto_muestreo_fk', filters.punto_muestreo_fk)
+ }
+ if (filters.personal_fk) {
+ query = query.eq('personal_fk', filters.personal_fk)
+ }
+ if (filters.type) {
+ query = query.eq('type', filters.type)
+ }
+ if (filters.zona_fk) {
+ query = query.eq('punto_muestreo.zona_fk', filters.zona_fk)
+ }
+
+ // Búsqueda de texto en observaciones
+ if (searchText) {
+ query = query.ilike('observaciones', `%${searchText}%`)
+ }
+
+ // Ordenamiento
+ const orderDirection = sortOrder === 'desc' ? false : true
+ query = query.order(sortBy, { ascending: orderDirection })
+
+ // Paginación
+ const from = (page - 1) * pageSize
+ const to = from + pageSize - 1
+ query = query.range(from, to)
+
+ const { data, error, count } = await query
+
+ if (error) {
+ console.error('Error fetching paginated analiticas:', error)
+ throw error
+ }
+
+ return {
+ data,
+ count,
+ page,
+ pageSize,
+ totalPages: Math.ceil(count / pageSize),
+ hasNextPage: page * pageSize < count,
+ hasPreviousPage: page > 1
+ }
+}
+
+export const getAnaliticasFilteredCount = async (filters = {}) => {
+ let query = supabase
+ .from('analiticas')
+ .select('*', { count: 'exact', head: true })
+
+ // Aplicar los mismos filtros que en getAnaliticasPaginated
+ if (filters.fecha_inicio) query = query.gte('fecha', filters.fecha_inicio)
+ if (filters.fecha_final) query = query.lte('fecha', filters.fecha_final)
+ if (filters.punto_muestreo_fk) query = query.eq('punto_muestreo_fk', filters.punto_muestreo_fk)
+ if (filters.personal_fk) query = query.eq('personal_fk', filters.personal_fk)
+ if (filters.type) query = query.eq('type', filters.type)
+
+ const { count, error } = await query
+
+ if (error) throw error
+ return count
+}
+
+export const setAnaliticas = async(analitica) => {
+ const { data } = await supabase.from('analiticas').insert(analitica)
+ return data
+}
diff --git a/src/services/factorialConfig.js b/src/services/factorialConfig.js
new file mode 100644
index 00000000..7d1692f5
--- /dev/null
+++ b/src/services/factorialConfig.js
@@ -0,0 +1,5 @@
+export const FACTORIAL_CLIENT_ID = import.meta.env.VITE_FACTORIAL_CLIENT_ID;
+export const FACTORIAL_CLIENT_SECRET = import.meta.env.VITE_FACTORIAL_CLIENT_SECRET;
+export const FACTORIAL_REDIRECT_URI = 'http://localhost:5173/admin-one-vue-tailwind/';
+export const FACTORIAL_AUTH_URL = 'https://api.factorialhr.com/oauth/authorize';
+export const FACTORIAL_TOKEN_URL = 'https://api.factorialhr.com/oauth/token';
\ No newline at end of file
diff --git a/src/services/infraestructuras.js b/src/services/infraestructuras.js
new file mode 100644
index 00000000..6e03cd61
--- /dev/null
+++ b/src/services/infraestructuras.js
@@ -0,0 +1,59 @@
+import { supabase } from "./supabase";
+
+
+export const createInfraestructura = async (infraestructura) => {
+ try {
+ const { data } = await supabase
+ .from('infraestructuras')
+ .insert({
+ id: infraestructura.id,
+ name: infraestructura.name,
+ type: infraestructura.tipo_infraestructura_fk,
+ operador: infraestructura.operador
+ })
+ .select()
+ .single()
+
+ return data
+ } catch (error) {
+ console.error('Error en setZona:', error)
+ throw error
+ }
+}
+
+export const anularInfraestructura = async (id) => {
+ try {
+ const { data } = await supabase
+ .from('infraestructuras')
+ .update({ activo: false })
+ .eq('id', id)
+ .single()
+
+ return data
+} catch (error) {
+ console.error('Error en anularInfraestructura:', error)
+ throw error
+}
+
+}
+
+export const updateInfraestructura = async (id) => {
+ console.log(id);
+ try {
+ const { data } = await supabase
+ .from('infraestructuras')
+ .update({
+ name: id.name,
+ type: id.type,
+ operador: id.operador })
+ .eq('id', id.id)
+ .single()
+
+ return data
+
+ } catch (error) {
+ console.error('Error al Actualizar la Infraestructura:', error)
+ alert('Error al Actualizar la Infraestructura:', error)
+ throw error
+ }
+}
\ No newline at end of file
diff --git a/src/services/msalConfig.js b/src/services/msalConfig.js
new file mode 100644
index 00000000..f4661175
--- /dev/null
+++ b/src/services/msalConfig.js
@@ -0,0 +1,203 @@
+// filepath: /src/msalConfig.js
+import { PublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser';
+
+const MICROSOFT_CLIENT_ID= import.meta.env.VITE_MICROSOFT_CLIENT_ID;
+const MICROSOFT_TENANT_ID = import.meta.env.VITE_MICROSOFT_TENANT_ID;
+
+// Añadir scopes necesarios
+const graphScopes = {
+ scopes: [
+ 'User.Read',
+ 'profile',
+ 'openid',
+ 'email',
+ 'User.ReadBasic.All',
+ // 'user.read.all'
+ ]
+};
+
+
+const msalConfig = {
+ auth: {
+ clientId: MICROSOFT_CLIENT_ID,
+ authority: `https://login.microsoftonline.com/${MICROSOFT_TENANT_ID}`,
+ redirectUri: window.location.origin,
+ postLogoutRedirectUri: window.location.origin,
+ },
+ cache: {
+ cacheLocation: 'localStorage',
+ storeAuthStateInCookie: true
+ },
+ system: {
+ allowRedirectInIframe: true,
+ iframeHashTimeout: 6000,
+ redirectNavigationTimeout: 6000,
+ loggerOptions: {
+ logLevel: 'Error',
+ piiLoggingEnabled: false
+ }
+ }
+};
+
+// Función para obtener foto de perfil
+
+const msalInstance = new PublicClientApplication(msalConfig);
+
+// Crear proveedor de criptografía para PKCE
+// export const cryptoProvider = new cryptoProvider()
+
+await msalInstance.initialize();
+
+export const loginWithMicrosoft = async () => {
+ try {
+ // Detectar si es móvil
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+
+ if (isMobile) {
+ return await msalInstance.loginRedirect({
+ ...graphScopes,
+ prompt: 'select_account'
+ });
+ } else {
+ return await msalInstance.loginPopup({
+ ...graphScopes,
+ prompt: 'select_account'
+ });
+ }
+ } catch (error) {
+ console.error('Error en login:', error);
+ throw error;
+ }
+};
+
+
+
+
+
+export const getUserProfile = async () => {
+ try {
+ const account = msalInstance.getAllAccounts()[0];
+ if (!account) return null;
+
+ const tokenResponse = await msalInstance.acquireTokenSilent({
+ scopes: graphScopes.scopes,
+ account: account
+ }).catch(async (error) => {
+ if (error instanceof InteractionRequiredAuthError) {
+ return await msalInstance.acquireTokenPopup(graphScopes);
+ }
+ throw error;
+ });
+
+ // Obtener datos del perfil
+ const response = await fetch('https://graph.microsoft.com/v1.0/me', {
+ headers: {
+ 'Authorization': `Bearer ${tokenResponse.accessToken}`
+ }
+ });
+
+ // Obtener foto de perfil
+ const photoResponse = await fetch('https://graph.microsoft.com/v1.0/me/photo/$value', {
+ headers: {
+ 'Authorization': `Bearer ${tokenResponse.accessToken}`
+ }
+ });
+
+ if (response.ok) {
+ const userData = await response.json();
+ let photoUrl = null;
+
+ if (photoResponse.ok) {
+ const blob = await photoResponse.blob();
+ photoUrl = URL.createObjectURL(blob);
+ console.log('photoUrl: ', photoUrl);
+ console.log('BLOB: ', blob);
+ }
+
+ return {
+ userData: userData,
+ telephone:userData.businessPhones[0],
+ displayName: userData.displayName,
+ email: userData.mail || userData.userPrincipalName,
+ id: userData.id,
+ photoUrl: photoUrl,
+ jobTitle: userData.jobTitle,
+ mobilePhone: userData.mobilePhone,
+ officeLocation: userData.officeLocation,
+ preferredLanguage: userData.preferredLanguage,
+ department: userData.department
+ };
+ }
+ return null;
+ } catch (error) {
+ console.error('Error al obtener perfil:', error);
+ return null;
+ }
+};
+
+
+// export const getUserPhoto = async () => {
+// try {
+// const account = msalInstance.getAllAccounts()[0];
+// const tokenResponse = await msalInstance.acquireTokenSilent({
+// ...graphScopes,
+// account: account
+// });
+// console.log('TokenResponse: ',tokenResponse);
+
+// const response = await fetch('https://graph.microsoft.com/v1.0/me/photo/$value', {
+// headers: {
+// 'Authorization': `Bearer ${tokenResponse.accessToken}`
+// }
+// });
+
+// if (response.ok) {
+// const blob = await response.blob();
+// return URL.createObjectURL(blob);
+// }
+// return null;
+// } catch (error) {
+// console.error('Error getting user photo:', error);
+// return null;
+// }
+// };
+
+// export const getUserProfile = async () => {
+// try {
+// const account = msalInstance.getAllAccounts()[0];
+// const tokenResponse = await msalInstance.acquireTokenSilent({
+// ...graphScopes,
+// account: account
+// });
+
+// const response = await fetch('https://graph.microsoft.com/v1.0/me', {
+// headers: {
+// 'Authorization': `Bearer ${tokenResponse.accessToken}`
+// }
+// });
+
+// if (response.ok) {
+// const userData = await response.json();
+// const photoUrl = await getUserPhoto();
+
+// return {
+// ...userData,
+// photoUrl,
+// displayName: userData.displayName,
+// email: userData.mail || userData.userPrincipalName,
+// phone: userData.mobilePhone,
+// jobTitle: userData.jobTitle,
+// department: userData.department
+// };
+// }
+// return null;
+// } catch (error) {
+// console.error('Error getting user profile:', error);
+// return null;
+// }
+// };
+
+
+
+
+export default msalInstance;
\ No newline at end of file
diff --git a/src/services/operarios.js b/src/services/operarios.js
new file mode 100644
index 00000000..01a518d5
--- /dev/null
+++ b/src/services/operarios.js
@@ -0,0 +1,141 @@
+import { supabase } from './supabase'
+
+export const getOperarios = async () => {
+ const { data } = await supabase.from('personal').select('*')
+ return data
+}
+
+// export const setOperarios = async (operario) => {
+// const { data } = await supabase.from('personal').insert(operario)
+// return data
+// }
+
+export const setOperarios = async (operario) => {
+ try {
+ // 1. Insertar operario y obtener ID
+ const { data: newOperario, error: errorOperario } = await supabase
+ .from('personal')
+ .insert({
+ name: operario.name,
+ email: operario.email,
+ phone: operario.phone,
+ ud_operativa_fk: operario.ud_operativa_fk,
+ type: operario.type
+ })
+ .select() // Devuelve todos los campos, incluido el ID generado
+ .single() // Devuelve un único registro en lugar de un array
+
+ if (errorOperario) throw errorOperario
+
+ // 2. Preparar zonas con el ID generado
+ const zonasToInsert = operario.zonas.map(zona_fk => ({
+ personal_fk: newOperario.id,
+ zonas_fk: zona_fk
+ }))
+
+ // 3. Insertar zonas
+ const { data: insertedZonas, error: errorZonas } = await supabase
+ .from('zonas_personal')
+ .insert(zonasToInsert)
+ .select()
+
+ if (errorZonas) throw errorZonas
+
+ return {
+ operario: newOperario,
+ zonas: insertedZonas
+ }
+
+ } catch (error) {
+ console.error('Error en setOperarios:', error)
+ throw error
+ }
+}
+
+// export const deleteOperario = async (id) => {
+// const { error } = await supabase.from('personal').delete().eq('id', id)
+
+// if (error) throw error
+// }
+
+export const deleteOperario = async (id) => {
+ try {
+ // 1. Primero borrar registros en zonas_personal
+ const { error: errorZonas } = await supabase
+ .from('zonas_personal')
+ .delete()
+ .eq('personal_fk', id)
+
+ if (errorZonas) throw errorZonas
+
+ // 2. Luego borrar el operario
+ const { error: errorOperario } = await supabase
+ .from('personal')
+ .delete()
+ .eq('id', id)
+
+ if (errorOperario) throw errorOperario
+
+ return true
+
+ } catch (error) {
+ console.error('Error al eliminar operario:', error)
+ throw error
+ }
+}
+
+
+export const updateOperariobyId = async (data) => {
+ try {
+ // console.log('updateOperariobyId: ',id, data);
+ // 1. Limpiar datos del operario
+ const cleanDataOperario = {
+ // id: data.id,
+ name: data.name,
+ email: data.email,
+ phone: data.phone,
+ ud_operativa_fk: data.ud_operativa_fk,
+ type: data.type
+ }
+ // 2. Actualizar datos del operario
+ const { data: updateDataOperario, errorOperario } = await supabase
+ .from('personal')
+ .update(cleanDataOperario)
+ .eq('id', data.id)
+ .select()
+
+
+ if (errorOperario) throw errorOperario
+
+
+ // 3. Limpiar datos de las zonas del operario
+ const { error: errorDelete } = await supabase
+ .from('zonas_personal')
+ .delete()
+ .eq('personal_fk', data.id)
+
+ if (errorDelete) throw errorDelete
+
+ // 4. Preparar nuevas zonas
+ const zonasToInsert = data.zonas.map((zona_fk) => ({
+ personal_fk: data.id,
+ zonas_fk: zona_fk
+ }))
+
+ // 5. Insertar nuevas zonas
+ const { data: insertedZonas, error: errorInsert } = await supabase
+ .from('zonas_personal')
+ .insert(zonasToInsert)
+ .select()
+
+ if (errorInsert) throw errorInsert
+
+ return {
+ operario: updateDataOperario,
+ zonas: insertedZonas
+ }
+ } catch (error) {
+ console.error('Error en updateOperariobyId:', error)
+ throw error
+ }
+}
diff --git a/src/services/puntosMuestreo.js b/src/services/puntosMuestreo.js
new file mode 100644
index 00000000..b7bc8cfa
--- /dev/null
+++ b/src/services/puntosMuestreo.js
@@ -0,0 +1,46 @@
+import { supabase } from './supabase'
+
+export const createPuntoMuestreo = async (data) => {
+ const { data: puntoMuestreo, error } = await supabase
+ .from('puntos_muestreo')
+ .insert({
+ id:data.id,
+ name: data.name,
+ infraestructura_fk: data.infraestructura_fk,
+ zona_fk: data.zona_fk,
+ posicion: data.posicion,
+ activo: data.activo,
+ })
+ .select()
+ .single()
+ if (error) throw error
+ return puntoMuestreo
+}
+
+export const anularPuntoMuestreo = async (id) => {
+ const { data, error } = await supabase
+ .from('puntos_muestreo')
+ .update({ activo: false })
+ .eq('id', id)
+ .select()
+ .single()
+ if (error) throw error
+ return data
+}
+
+export const updatePuntoMuestreo = async (data) => {
+ const { data: puntoMuestreo, error } = await supabase
+ .from('puntos_muestreo')
+ .update({
+ name: data.name,
+ infraestructura_fk: data.infraestructura_fk,
+ zona_fk: data.zona_fk,
+ posicion: data.posicion,
+ activo: data.activo,
+ })
+ .eq('id', data.id)
+ .select()
+ .single()
+ if (error) throw error
+ return puntoMuestreo
+}
\ No newline at end of file
diff --git a/src/services/supabase.js b/src/services/supabase.js
new file mode 100644
index 00000000..2866e3e0
--- /dev/null
+++ b/src/services/supabase.js
@@ -0,0 +1,120 @@
+import { createClient } from "@supabase/supabase-js";
+// import {corsHeaders} from '../helpers/cors'
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
+// const isModalDeleteActive = ref(false)
+// const analiticaToDelete = ref(null)
+
+if (!supabaseUrl || !supabaseKey) {
+ throw new Error('Missing Supabase configuration. Please check your environment variables.');
+}
+
+export const supabase = createClient(supabaseUrl, supabaseKey, {
+ auth: {
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: true
+ },
+ global: {
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
+ 'Access-Control-Allow-Headers': 'X-Client-Info, X-Client-Proto, X-Client-Version, X-Requested-With, Content-Type, Accept, Authorization',
+ 'Content-Type': 'application/json',
+ 'Prefer': 'return=representation'
+ }
+ },
+ // db: {
+ // schema: {
+ // defaultHTTPMethod: 'PUT' // Force PUT instead of PATCH for updates
+ // }
+ // }
+});
+
+
+// Interceptor para manejar cambios en la autenticación
+supabase.auth.onAuthStateChange((event, session) => {
+ if (event === 'SIGNED_IN') {
+ console.log('Usuario autenticado:', session.user)
+ }
+ if (event === 'SIGNED_OUT') {
+ console.log('Usuario desconectado')
+ }
+ if (event === 'TOKEN_REFRESHED') {
+ console.log('Token actualizado')
+ }
+})
+
+
+
+export const searchOperarios = async () => {
+ const { data } = await supabase.from('personal').select('*')
+ return data
+}
+export const searchZonasOperarios = async (id) => {
+ const { data } = await supabase.from('personal').select(`id,zonas_personal(zonas_abastecimiento(*))`).eq('id',id)
+ return data
+}
+
+// export const deleteAnalitica = async (analitica) => {
+// analiticaToDelete.value = analitica
+// isModalDeleteActive.value = true
+// }
+
+
+
+
+export const deleteAnalitica = async (id) => {
+ const { error } = await supabase
+ .from('analiticas')
+ .delete()
+ .eq('id', id)
+
+ if (error) throw error
+}
+
+export const updateAnaliticabyId = async (id, data) => {
+ try {
+ console.log('updateAnaliticabyId: ',id, data);
+ // Limpiar datos antes de actualizar
+ const cleanData = {
+ id: data.id,
+ fecha: data.fecha,
+ personal_fk: data.personal_fk,
+ punto_muestreo_fk: data.punto_muestreo_fk,
+ type: data.type,
+ cloro: data.cloro,
+ color: data.color,
+ olor: data.olor,
+ sabor: data.sabor,
+ ph: data.ph,
+ turbidez: data.turbidez,
+ observaciones: data.observaciones,
+ registro: data.registro,
+ zona_fk: data.zona_fk
+ }
+ const { data: updateData, error } = await supabase
+ .from('analiticas')
+ .update(cleanData)
+ .eq('id', id)
+ .select()
+
+ if (error) throw error
+
+ return updateData
+ } catch (error) {
+ console.error('Error en updateAnaliticabyId:', error)
+ throw error
+ }
+
+
+}
+// export const deleteAnalitica = async (id) => {
+// const { data, error } = await supabase
+// .from('analiticas')
+// .delete()
+// .eq('id', id)
+// if (error) throw error
+// return data
+// }
diff --git a/src/services/uo.js b/src/services/uo.js
new file mode 100644
index 00000000..544e9c09
--- /dev/null
+++ b/src/services/uo.js
@@ -0,0 +1,89 @@
+import { supabase } from './supabase'
+
+
+export const getUO = async () => {
+ const { data } = await supabase.from('unidades_operativas').select('*')
+ return data
+}
+
+
+export const anularUO = async (id) => {
+ console.log(id)
+ try {
+ const { data, error: errorUO } = await supabase
+ .from('unidades_operativas')
+ .upsert({ id: id, activo: false }, { onConflict: ['id'] })
+ .select()
+
+ if (errorUO) {
+ console.error('Error SQL:', errorUO)
+ throw errorUO
+ }
+ return data
+ } catch (error) {
+ console.error('Error en anularUO:', error)
+ throw error
+ }
+}
+
+export const createUO = async (uo) => {
+ try {
+ const { data } = await supabase
+ .from('unidades_operativas')
+ .insert({
+ name: uo.name,
+ description: uo.description
+ })
+ .select()
+ return data
+ } catch (error) {
+ console.error('Error en createUO:', error)
+ throw error
+ }
+}
+
+
+
+
+
+export const updateUO = async (data) => {
+ const MAX_RETRIES = 3;
+ const RETRY_DELAY = 1000;
+
+ const tryUpdate = async (retryCount = 0) => {
+ try {
+ if (!data.id || !data.name || !data.description) {
+ throw new Error('Faltan campos requeridos')
+ }
+
+ const { data: updateData, error: errorUO } = await supabase
+ .from('unidades_operativas')
+ .update({
+ name: data.name,
+ description: data.description,
+ })
+ .eq('id', data.id)
+ .select()
+ .single()
+
+ if (errorUO) throw errorUO
+
+ return updateData
+
+ } catch (error) {
+ if (retryCount < MAX_RETRIES) {
+ console.log(`Reintento ${retryCount + 1} de ${MAX_RETRIES}`)
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
+ return tryUpdate(retryCount + 1)
+ }
+ throw error
+ }
+ }
+
+ try {
+ return await tryUpdate()
+ } catch (error) {
+ console.error('Error en updateUO:', error)
+ throw new Error(`Error al actualizar UO: ${error.message}`)
+ }
+}
diff --git a/src/services/zonas.js b/src/services/zonas.js
new file mode 100644
index 00000000..747c66f1
--- /dev/null
+++ b/src/services/zonas.js
@@ -0,0 +1,64 @@
+import { supabase } from './supabase'
+
+export const createZona = async (zona) => {
+ try {
+ const { data } = await supabase
+ .from('zonas_abastecimiento')
+ .insert({
+ id: zona.id,
+ name: zona.name,
+ com_autonoma_fk: zona.com_autonoma_fk,
+ unidades_operativas_fk: zona.unidades_operativas_fk
+ })
+ .select()
+ .single()
+
+ return data
+ } catch (error) {
+ console.error('Error en setZona:', error)
+ throw error
+ }
+}
+
+export const anularZona = async (id) => {
+ console.log('ID: ',id)
+ try {
+ const { data, error: errorZona } = await supabase
+ .from('zonas_abastecimiento')
+ .upsert({ id: id, activa: false }, { onConflict: ['id'] })
+ .select()
+
+ if (errorZona) {
+ console.error('Error SQL:', errorZona)
+ throw errorZona
+ }
+ return data
+ } catch (error) {
+ console.error('Error en anularUO:', error)
+ throw error
+ }
+}
+
+export const updateZona = async (data) => {
+ try {
+ const { data: updatedData, error: errorZona } = await supabase
+ .from('zonas_abastecimiento')
+ .update({
+ // id:data.id,
+ name: data.name,
+ com_autonoma_fk: data.com_autonoma_fk,
+ unidades_operativas_fk: data.unidades_operativas_fk
+ })
+ .eq('id', data.id)
+ .select()
+ .single()
+
+ if (errorZona) {
+ console.error('Error SQL:', errorZona)
+ throw errorZona
+ }
+ return updatedData
+ } catch(error) {
+ console.log('Error en updateZona:', error)
+ }
+}
diff --git a/src/stores/auth.js b/src/stores/auth.js
new file mode 100644
index 00000000..3cb537ad
--- /dev/null
+++ b/src/stores/auth.js
@@ -0,0 +1,57 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { supabase } from '@/services/supabase'
+
+export const useAuthStore = defineStore('auth', () => {
+ const user = ref(null)
+ const session = ref(null)
+ const roles = ref([])
+
+ const isAuthenticated = computed(() => !!session.value)
+
+ const hasRole = (roleToCheck) => roles.value.includes(roleToCheck)
+
+ const login = async (accountData) => {
+ // Aquí puedes guardar cualquier dato recibido de MSAL
+ // y, si usas supabase, podrías hacer un registro o conexión con el usuario.
+ session.value = { account: accountData }
+ await loadUserProfile(accountData)
+ }
+
+ const loadUserProfile = async (accountData) => {
+ try {
+ // Ejemplo: usar el email del usuario para buscar sus roles en tu tabla de 'personal'
+ const { data, error } = await supabase
+ .from('personal')
+ .select('*, roles_fk(*)')
+ .eq('email', accountData.username)
+ .single()
+
+ if (error) throw error
+
+ user.value = data
+ // Suponiendo que 'roles' es un arreglo de objetos con propiedad name:
+ roles.value = data.roles?.map(r => r.name) || []
+ } catch (error) {
+ console.error('Error cargando perfil:', error)
+ }
+ }
+
+ const logout = async () => {
+ session.value = null
+ user.value = null
+ roles.value = []
+ // Limpia también la sesión de supabase si aplica.
+ await supabase.auth.signOut()
+ }
+
+ return {
+ user,
+ session,
+ roles,
+ isAuthenticated,
+ hasRole,
+ login,
+ logout
+ }
+})
\ No newline at end of file
diff --git a/src/stores/index.js b/src/stores/index.js
new file mode 100644
index 00000000..0122cf9e
--- /dev/null
+++ b/src/stores/index.js
@@ -0,0 +1,26 @@
+import { defineStore } from "pinia";
+import { ref } from "vue";
+
+export const useStore = defineStore("dataStore", () => {
+
+ const dataSamples = ref([]);
+ const user= ref(null);
+
+
+
+
+
+ return {
+ dataSamples,
+ user,
+
+
+
+
+
+
+ };
+});
+
+export default useStore;
+
diff --git a/src/stores/login.js b/src/stores/login.js
new file mode 100644
index 00000000..04a7702e
--- /dev/null
+++ b/src/stores/login.js
@@ -0,0 +1,203 @@
+import { defineStore } from 'pinia'
+import { computed, ref, watch } from 'vue'
+import { getUserProfile } from '@/services/msalConfig'
+
+
+export const useLoginStore = defineStore('loginStore', () => {
+ const user = ref(JSON.parse(sessionStorage.getItem('user')) || null)
+ const isAuthenticated = ref(sessionStorage.getItem('isAuthenticated') === 'true')
+ const userName = ref(sessionStorage.getItem('userName') || '')
+ const userEmail = ref(sessionStorage.getItem('userEmail') || '')
+ // const userAvatar = ref(sessionStorage.getItem('userAvatar') || '')
+ const userLogged = ref(JSON.parse(sessionStorage.getItem('userLogged')) || {})
+ const profilePhoto = ref(sessionStorage.getItem('profilePhoto') || '')
+ const userAutenticated = ref(sessionStorage.getItem('userAutenticated') || '')
+ const userRole=ref(sessionStorage.getItem('userRole') || '')
+ const userId=ref(sessionStorage.getItem('userId') || '')
+
+ // Variables para manejo de sesión
+ const sessionTimeout = ref(null)
+ const SESSION_DURATION = 30 * 60 * 1000 // 30 minutos en ms
+
+ const userAvatar = computed(
+ () =>
+ `https://ui-avatars.com/api/?name=${userName.value}&background=random&font-size=0.75&bold=true&color=fff`
+ )
+ // Watchers para persistencia segura
+ watch([isAuthenticated, user, userName, userEmail, userAvatar, userLogged, profilePhoto, userAutenticated, userRole,userId], ([newAuth, newUser, newName, newEmail, newAvatar,newUserLogged, newProfilePhoto, newUserAutenticated, newUserRole, newUserId]) => {
+ sessionStorage.setItem('isAuthenticated', newAuth)
+ if (newUser) sessionStorage.setItem('user', JSON.stringify(newUser))
+ if (newName) sessionStorage.setItem('userName', newName)
+ if (newEmail) sessionStorage.setItem('userEmail', newEmail)
+ if (newAvatar) sessionStorage.setItem('userAvatar', newAvatar)
+ if (newUserLogged) sessionStorage.setItem('userLogged', JSON.stringify(newUserLogged))
+ if (newProfilePhoto) sessionStorage.setItem('profilePhoto', newProfilePhoto)
+ if (newUserAutenticated) sessionStorage.setItem('userAutenticated', newUserAutenticated)
+ if (newUserRole) sessionStorage.setItem('userRole', newUserRole)
+ if (newUserId) sessionStorage.setItem('userId', newUserId)
+ })
+
+
+
+
+ // Función para limpiar timeout de sesión
+ const clearSessionTimeout = () => {
+ if (sessionTimeout.value) {
+ clearTimeout(sessionTimeout.value)
+ sessionTimeout.value = null
+ }
+ }
+
+ // Función para iniciar timeout de sesión
+ const startSessionTimeout = () => {
+ clearSessionTimeout()
+
+ sessionTimeout.value = setTimeout(() => {
+ console.warn('Sesión expirada por inactividad')
+ logout()
+ // Opcional: mostrar mensaje al usuario
+ alert('Tu sesión ha expirado por inactividad. Por favor, inicia sesión nuevamente.')
+ }, SESSION_DURATION)
+ }
+
+ // Función para renovar sesión (reiniciar timeout)
+ const renewSession = () => {
+ if (isAuthenticated.value) {
+ const lastActivity = Date.now()
+ sessionStorage.setItem('lastActivity', lastActivity.toString())
+ startSessionTimeout()
+ }
+ }
+
+ // Verificar si la sesión ha expirado
+ const checkSessionExpiry = () => {
+ const lastActivity = sessionStorage.getItem('lastActivity')
+ if (lastActivity) {
+ const timeDiff = Date.now() - parseInt(lastActivity)
+ if (timeDiff > SESSION_DURATION) {
+ logout()
+ return false
+ }
+ }
+ return true
+ }
+
+ const login = async (userData) => {
+ try {
+ isAuthenticated.value = true
+ user.value = userData
+
+ // Inicializar timestamp de sesión
+ const loginTime = Date.now()
+ sessionStorage.setItem('loginTime', loginTime.toString())
+ sessionStorage.setItem('lastActivity', loginTime.toString())
+
+ // Iniciar timeout de sesión
+ startSessionTimeout()
+
+ // Obtener perfil completo
+ const userProfile = await getUserProfile()
+ if (userProfile) {
+ console.log('USERPROFILE:', userProfile);
+ userAutenticated.value=userProfile
+ userName.value = userProfile.displayName
+ userEmail.value = userProfile.email
+ // userAvatar.value = userProfile.photoUrl
+ userLogged.value = { ...userLogged.value, ...userProfile }
+ }
+ } catch (error) {
+ console.error('Error en login:', error)
+ throw error
+ }
+ }
+
+ const logout = async () => {
+ // Limpiar timeout de sesión
+ clearSessionTimeout()
+
+ user.value = null
+ isAuthenticated.value = false
+ userName.value = ''
+ userEmail.value = ''
+ // sessionStorage.removeItem('isAuthenticated')
+ // sessionStorage.removeItem('user')
+ sessionStorage.clear()
+ }
+
+ const getUserLogged=computed(() => {
+ return userLogged.value
+ })
+
+ const setUser=(payload)=> {
+ if (payload.name) {
+ userName.value = payload.name
+ sessionStorage.setItem('userName', payload.name)
+ }
+ if (payload.username) {
+ userEmail.value = payload.username
+ sessionStorage.setItem('userEmail', payload.username)
+ }
+ }
+
+ const setUserRole = (role) => {
+ // Si userLogged.value no es un objeto, lo inicializamos
+ if (typeof userLogged.value !== 'object' || userLogged.value === null) {
+ userLogged.value = {}
+ }
+ userLogged.value = {
+ ...userLogged.value,
+ role: role
+ }
+ userRole.value = role
+ }
+ const setUserId = (id) => {
+ // Si userLogged.value no es un objeto, lo inicializamos
+ if (typeof userLogged.value !== 'object' || userLogged.value === null) {
+ userLogged.value = {}
+ }
+ userLogged.value = {
+ ...userLogged.value,
+ id: id
+ }
+ userId.value = id
+ }
+
+
+ const setAccount = (account) => {
+ user.value = account
+ }
+
+ const setIsAuthenticated = (valor) => {
+ isAuthenticated.value = valor
+ }
+ const setProfilePhoto = (valor) => {
+ profilePhoto.value = valor
+ }
+
+ return {
+ user,
+ isAuthenticated,
+ setIsAuthenticated,
+ setAccount,
+ userAvatar,
+ setUser,
+ userName,
+ userEmail,
+ logout,
+ login,
+ userLogged,
+ profilePhoto,
+ setProfilePhoto,
+ setUserRole,
+ userRole,
+ getUserLogged,
+ setUserId,
+ userId,
+ // Funciones de seguridad de sesión
+ renewSession,
+ checkSessionExpiry,
+ clearSessionTimeout
+ }
+})
+
+export default useLoginStore
diff --git a/src/stores/main.js b/src/stores/main.js
index dee245b7..ad99b058 100644
--- a/src/stores/main.js
+++ b/src/stores/main.js
@@ -3,16 +3,24 @@ import { ref, computed } from 'vue'
import axios from 'axios'
export const useMainStore = defineStore('main', () => {
- const userName = ref('John Doe')
- const userEmail = ref('doe.doe.doe@example.com')
+
+ const userName = ref('Usuario')
+ const userEmail = ref('usuario@aqlara.com')
const userAvatar = computed(
() =>
- `https://api.dicebear.com/7.x/avataaars/svg?seed=${userEmail.value.replace(
+ `https://ui-avatars.com/api/?name=${userEmail.value.replace(
/[^a-z0-9]+/gi,
'-'
)}`
)
+ // const userAvatar = computed(
+ // () =>
+ // `https://api.dicebear.com/7.x/avataaars-neutral/svg?seed=${userEmail.value.replace(
+ // /[^a-z0-9]+/gi,
+ // '-'
+ // )}`
+ // )
const isFieldFocusRegistered = ref(false)
diff --git a/src/stores/plantas.js b/src/stores/plantas.js
new file mode 100644
index 00000000..448af756
--- /dev/null
+++ b/src/stores/plantas.js
@@ -0,0 +1,378 @@
+import { defineStore } from 'pinia'
+import { computed, ref } from 'vue'
+import { supabase } from '@/services/supabase'
+
+export const usePlantasStore = defineStore('plantasStore', () => {
+ const zonas = ref([])
+ const operarios = ref([])
+ const analiticas = ref([])
+ const puntosMuestreo = ref([])
+ const unidadesOperativas = ref([])
+ const comunidadesAutonomas = ref([])
+ const formZonas = ref([])
+ const infraestructuras = ref([])
+ const tipo_infraestructura = ref([])
+ const zonas_infraestructuras = ref([])
+ const analiticaToUpdate = ref(null)
+ const tipoPersonal = ref([])
+ const zonas_personal = ref([])
+
+ // onMounted(() => {
+ // loadZonas()
+ // loadOperarios()
+ // loadAnaliticas()
+ // loadPuntosMuestreo()
+ // loadUnidadesOperativas()
+ // loadComunidadesAutonomas()
+ // loadInfraestructuras()
+ // loadTipoInfraestructura()
+ // loadZonasInfraestructuras()
+ // loadTipoPersonal()
+ // })
+
+ // Función para inicializar datos básicos (sin analíticas)
+ const initializeStore = async () => {
+ try {
+ await Promise.all([
+ loadZonas(),
+ loadOperarios(),
+ // loadAnaliticas(), // ❌ Removido - se carga bajo demanda
+ loadPuntosMuestreo(),
+ loadUnidadesOperativas(),
+ loadComunidadesAutonomas(),
+ loadInfraestructuras(),
+ loadTipoInfraestructura(),
+ loadZonasInfraestructuras(),
+ loadTipoPersonal()
+ ])
+ console.log('📊 Store inicializado (sin analíticas)')
+ } catch (error) {
+ console.error('Error inicializando store:', error)
+ }
+ }
+
+ // Nueva función para inicializar incluyendo analíticas (solo cuando se necesite)
+ const initializeStoreWithAnalytics = async () => {
+ try {
+ await Promise.all([
+ loadZonas(),
+ loadOperarios(),
+ loadAnaliticas(), // ✅ Solo se carga cuando explícitamente se necesita
+ loadPuntosMuestreo(),
+ loadUnidadesOperativas(),
+ loadComunidadesAutonomas(),
+ loadInfraestructuras(),
+ loadTipoInfraestructura(),
+ loadZonasInfraestructuras(),
+ loadTipoPersonal()
+ ])
+ console.log('📊 Store inicializado (con analíticas)')
+ } catch (error) {
+ console.error('Error inicializando store completo:', error)
+ }
+ }
+
+ const loadZonas = async () => {
+ const { data } = await supabase.from('zonas_abastecimiento').select('*')
+ zonas.value = data
+ }
+ // const loadOperarios = async () => {
+ // const { data } = await supabase.from('personal').select('*')
+ // operarios.value = data
+ // }
+ const loadZonasOperarios = async () => {
+ const { data } = await supabase.from('zonas_personal').select('*')
+ zonas_personal.value = data
+ }
+ // const loadAnaliticas = async () => {
+ // const { data } = await supabase.from('analiticas').select('*')
+ // analiticas.value = data
+ // }
+
+ const loadAnaliticas = async () => {
+ try {
+ const { data, error } = await supabase
+ .from('analiticas')
+ .select(`
+ *,
+ puntos_muestreo (
+ id,
+ name,
+ infraestructuras (
+ id,
+ name,
+ zonas_abastecimiento (
+ id,
+ name,
+ unidades_operativas (
+ id,
+ name
+ ),
+ comunidades_autonomas (
+ id,
+ name
+ )
+ )
+ )
+ )
+ `)
+
+ if (error) throw error
+
+ if (data) {
+ const mappedData = data.map(item => {
+ const puntoMuestreo = item.puntos_muestreo
+ const infraestructura = puntoMuestreo?.infraestructuras || {}
+ const zonaAbastecimiento = infraestructura?.zonas_abastecimiento || {}
+ const unidadOperativa = zonaAbastecimiento?.unidades_operativas || {}
+ const comunidadAutonoma = zonaAbastecimiento?.comunidades_autonomas || {}
+
+ return {
+ ...item,
+ punto_muestreo_id: puntoMuestreo?.id,
+ punto_muestreo_name: puntoMuestreo?.name,
+ infraestructura_id: infraestructura?.id,
+ infraestructura_name: infraestructura?.name,
+ zona_id: zonaAbastecimiento?.id,
+ zona_name: zonaAbastecimiento?.name,
+ unidad_id: unidadOperativa?.id,
+ unidad_name: unidadOperativa?.name,
+ comunidad_id: comunidadAutonoma?.id,
+ comunidad_name: comunidadAutonoma?.name
+ }
+ })
+
+ analiticas.value = mappedData
+ return mappedData
+ }
+
+ } catch (error) {
+ console.error('Error al cargar analíticas:', error.message)
+ throw error
+ }
+ }
+
+ // Modificar loadOperarios para incluir las zonas de cada operario
+const loadOperarios = async () => {
+ try {
+ const { data: operariosData, error } = await supabase.from('personal').select('*')
+
+ if (error) throw error
+
+ const {data:zonasPersonal } = await supabase.from('zonas_personal').select('*')
+
+ // Enriquecer cada operario con sus zonas asignadas
+ operarios.value = operariosData.map(operario => {
+ // Filtrar las relaciones de zonas_personal para este operario
+
+
+
+ const relacionesZonas = zonasPersonal.filter(
+ relacion => relacion.personal_fk === operario.id).map(relacion => relacion.zonas_fk)
+
+
+ // console.log('relacionesZonas: ',relacionesZonas);
+
+ // Obtener los IDs de las zonas asignadas a este operario
+ // const zonasIds = relacionesZonas.map(relacion => relacion.zonas_fk)
+
+ // console.log('zonasIds: ',zonasIds);
+
+ // Buscar los datos completos de cada zona
+ // const zonasOperario = zonas.value.filter(zona =>
+ // zonasIds.includes(zona.id)
+ // )
+
+ // console.log('zonasOperario:',zonasOperario)
+ // Añadir las zonas al objeto operario
+ return {
+ ...operario,
+ zonas: relacionesZonas || []
+ }
+ })
+
+ return operarios.value
+ } catch (error) {
+ console.error('Error cargando operarios:', error)
+ return []
+ }
+}
+
+ const loadTipoPersonal = async () => {
+ const { data } = await supabase.from('tipo_personal').select('*')
+ tipoPersonal.value = data
+ }
+
+ const loadPuntosMuestreo = async () => {
+ const { data } = await supabase.from('puntos_muestreo').select('*')
+ puntosMuestreo.value = data
+ }
+ const loadUnidadesOperativas = async () => {
+ const { data } = await supabase.from('unidades_operativas').select('*')
+ unidadesOperativas.value = data
+ }
+ const loadComunidadesAutonomas = async () => {
+ const { data } = await supabase.from('comunidades_autonomas').select('*')
+ comunidadesAutonomas.value = data
+ }
+
+ const loadZonasInfraestructuras = async () => {
+ const { data } = await supabase.from('zonas_infraestructuras').select('*')
+ zonas_infraestructuras.value = data
+ }
+ const loadTipoInfraestructura = async () => {
+ const { data } = await supabase.from('tipo_infraestructura').select('*')
+ tipo_infraestructura.value = data
+ }
+
+ const loadInfraestructuras = async () => {
+ const { data } = await supabase.from('infraestructuras').select('*')
+ infraestructuras.value = data
+ }
+
+ const getZonas = computed(() => {
+ return zonas.value
+ })
+
+ const getOperarios = computed(() => {
+ return operarios.value
+ })
+
+
+ const getAnaliticas = computed(() => {
+ return analiticas.value
+ })
+
+ // Computed para verificar si las analíticas están cargadas
+ const isAnalyticasLoaded = computed(() => {
+ return analiticas.value.length > 0
+ })
+
+ // Computed para obtener el número de analíticas cargadas
+ const analyticsCount = computed(() => {
+ return analiticas.value.length
+ })
+
+ const getPuntosMuestreo = computed(() => {
+ return puntosMuestreo.value
+ })
+ const getUnidadesOperativas = computed(() => {
+ return unidadesOperativas.value
+ })
+ const getComunidadesAutonomas = computed(() => {
+ return comunidadesAutonomas.value
+ })
+
+ const getInfraestructuras = computed(() => {
+ return infraestructuras.value
+ })
+ const getZonasInfraestructuras = computed(() => {
+ return zonas_infraestructuras.value
+ })
+ const getTipoInfraestructura = computed(() => {
+ return tipo_infraestructura.value
+ })
+
+ const getTipoPersonal = computed(() => {
+ return tipoPersonal.value
+ })
+
+ const getPuntosMuestreoTotal = computed(() => {
+ //quiero qeu devuelva todos los puntos de muestreo con su zona de abastecimiento y su zona de infraestructura
+ return puntosMuestreo.value.map((punto) => {
+ const zonaAbastecimiento = zonas.value.find((zona) => zona.id === punto.zona_fk)
+ const zonaInfraestructura = infraestructuras.value.find(
+ (infraestructura) => infraestructura.id === punto.infraestructura_fk
+ )
+ return {
+ ...punto,
+ zonaAbastecimiento,
+ zonaInfraestructura
+ }
+ })
+ })
+
+ // Añadir un computed getter para facilitar el acceso a las zonas de un operario específico
+const getZonasOperario = computed(() => {
+ return (operarioId) => {
+ const operario = operarios.value.find(op => op.id === operarioId)
+ return operario?.zonas || []
+ }
+})
+
+ const getAnaliticasTotal = computed(() => {
+ //quiero qeu devuelva todos los puntos de muestreo con su zona de abastecimiento y su zona de infraestructura
+
+ return analiticas.value.map((analitica) => {
+ const puntoMuestreo = puntosMuestreo.value.find(
+ (punto) => punto.id === analitica.punto_muestreo_fk
+ )
+
+ if (puntoMuestreo) {
+
+ const zona_fk = puntoMuestreo.zona_fk
+ const infraestructura_fk = puntoMuestreo.infraestructura_fk
+
+
+
+ return {
+ ...analitica,
+ zona_fk,
+ infraestructura_fk,
+ }
+ }
+ })
+ })
+
+ // getTipoOperario = (id) => {
+ // const operario = operarios.value.find((operario) => operario.id === id)
+ // if (operario) {
+ // return operario.tipo
+ // } else {
+ // return ''
+ // }
+ // }
+
+ return {
+ getZonas,
+ getOperarios,
+ getAnaliticas,
+ isAnalyticasLoaded,
+ analyticsCount,
+ getPuntosMuestreo,
+ getUnidadesOperativas,
+ getComunidadesAutonomas,
+ getInfraestructuras,
+ getZonasInfraestructuras,
+ getTipoInfraestructura,
+ getTipoPersonal,
+ tipo_infraestructura,
+ zonas,
+ operarios,
+ analiticas,
+ puntosMuestreo,
+ unidadesOperativas,
+ comunidadesAutonomas,
+ formZonas,
+ infraestructuras,
+ zonas_infraestructuras,
+ tipoPersonal,
+ loadAnaliticas,
+ getPuntosMuestreoTotal,
+ getAnaliticasTotal,
+ loadOperarios,
+ loadZonas,
+ loadPuntosMuestreo,
+ loadUnidadesOperativas,
+ loadComunidadesAutonomas,
+ loadInfraestructuras,
+ loadZonasInfraestructuras,
+ loadTipoPersonal,
+ analiticaToUpdate,
+ initializeStore,
+ initializeStoreWithAnalytics,
+ zonas_personal,
+ loadZonasOperarios,
+ getZonasOperario
+ }
+})
diff --git a/src/views/AnaliticsTableView.vue b/src/views/AnaliticsTableView.vue
new file mode 100644
index 00000000..9d6d86ee
--- /dev/null
+++ b/src/views/AnaliticsTableView.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Modo Server-Side Activo ⚡
+
Optimizado para grandes volúmenes de datos. Solo carga 20-100 registros por página.
+
+
+
+
+
+
+
+
+
+
Modo Client-Side Activo
+
Carga todas las analíticas al iniciar. Ideal para conjuntos pequeños de datos (<1000 registros).
+
+ 📊 Estado: {{ plantasStore.isAnalyticasLoaded ?
+ `${plantasStore.analyticsCount} analíticas cargadas` :
+ 'Analíticas no cargadas aún'
+ }}
+
+
+
+
+
+
+
+```
\ No newline at end of file
diff --git a/src/views/ExcelUploaderView.vue b/src/views/ExcelUploaderView.vue
new file mode 100644
index 00000000..bf1facba
--- /dev/null
+++ b/src/views/ExcelUploaderView.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Subir archivo
+
+
+
+
+ Exportar a XML
+
+
+
+
+
+
+
+
+
+
+
+
{{ sheetName }}
+
+
+
+
+
+
+ {{ row[getHeaders(sheetData)[0]] }}
+
+
+ {{ row[getHeaders(sheetData)[1]] }}
+
+
+ {{ row[getHeaders(sheetData)[2]] }}
+
+
+ {{ row[getHeaders(sheetData)[5]] }}
+
+
+ {{ row[getHeaders(sheetData)[6]] }}
+
+
+ {{ row[getHeaders(sheetData)[7]] }}
+
+
+ {{ row[getHeaders(sheetData)[8]] }}
+
+
+ {{ row[getHeaders(sheetData)[11]] }}
+
+
+ {{ row[getHeaders(sheetData)[12]] }}
+
+
+
+
+ {{ row[header] }}
+
+
+
+
+
+
+ {{ row[header] }}
+
+
+
+
+
+
+
+
+
+
+ {{ row[header] }}
+
+
+
+
+
+
+ {{ row[header] }}
+
+
+
+
+
Esta hoja está vacía
+
+
+
{{ errorMessage }}
+
+
+
+
+
+
+
diff --git a/src/views/FormAnaliticaView-copy.vue b/src/views/FormAnaliticaView-copy.vue
new file mode 100644
index 00000000..59932644
--- /dev/null
+++ b/src/views/FormAnaliticaView-copy.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Caracteristicas Organolépticas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/FormAnaliticaView.vue b/src/views/FormAnaliticaView.vue
new file mode 100644
index 00000000..51ba909e
--- /dev/null
+++ b/src/views/FormAnaliticaView.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/FormsView.vue b/src/views/FormsView.vue
index 208ff4af..8778c405 100644
--- a/src/views/FormsView.vue
+++ b/src/views/FormsView.vue
@@ -1,10 +1,10 @@
-
+
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
+
+
+
+
-
-
-
+
+
+
Analítica {{ form.type === '29' ? ' de Rutina' : ' Operacional' }}
+
+
+
-
+
+
+
+ mg/l
+
+
+
+ ud
+
+
+
+ UNF
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
- Custom elements
+
diff --git a/src/views/InfraestructurasAdminView.vue b/src/views/InfraestructurasAdminView.vue
new file mode 100644
index 00000000..623db9c6
--- /dev/null
+++ b/src/views/InfraestructurasAdminView.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/LoginAqlaraView.vue b/src/views/LoginAqlaraView.vue
new file mode 100644
index 00000000..046fae78
--- /dev/null
+++ b/src/views/LoginAqlaraView.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
diff --git a/src/views/LoginAqlaraView_bak.vue b/src/views/LoginAqlaraView_bak.vue
new file mode 100644
index 00000000..e6887679
--- /dev/null
+++ b/src/views/LoginAqlaraView_bak.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
diff --git a/src/views/LoginAqlaraView_conJWT.vue b/src/views/LoginAqlaraView_conJWT.vue
new file mode 100644
index 00000000..f93a9093
--- /dev/null
+++ b/src/views/LoginAqlaraView_conJWT.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
diff --git a/src/views/MapaPuntoMuestrasView.vue b/src/views/MapaPuntoMuestrasView.vue
new file mode 100644
index 00000000..a6420a9c
--- /dev/null
+++ b/src/views/MapaPuntoMuestrasView.vue
@@ -0,0 +1,345 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Oficinas Centrales
+
Rda. de Narcís Monturiol, nº 4
+
oficina 214-A, 46980 Paterna, Valencia
+
Tfno: 963 153 232
+
+
+
+
+
+
+
+
+
+
+
{{ punto.name }}
+
id: {{ punto.id }}
+
+
+
+
+
+
{{ punto.name }}
+
+
SINAC Id: {{ punto.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/OperariosView.vue b/src/views/OperariosView.vue
new file mode 100644
index 00000000..e0ef4e1a
--- /dev/null
+++ b/src/views/OperariosView.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ProfileView.vue b/src/views/ProfileView.vue
index 142dea2d..32677f74 100644
--- a/src/views/ProfileView.vue
+++ b/src/views/ProfileView.vue
@@ -13,9 +13,13 @@ import BaseButtons from '@/components/BaseButtons.vue'
import UserCard from '@/components/UserCard.vue'
import LayoutAuthenticated from '@/layouts/LayoutAuthenticated.vue'
import SectionTitleLineWithButton from '@/components/SectionTitleLineWithButton.vue'
+import { useLoginStore } from '@/stores/login'
+
const mainStore = useMainStore()
+const loginStore = useLoginStore()
+
const profileForm = reactive({
name: mainStore.userName,
email: mainStore.userEmail
@@ -59,9 +63,9 @@ const submitPass = () => {
-
+
{
+import { mdiFlaskEmptyOutline, mdiFilter, mdiDownload, mdiSetCenter, mdiPlusBox, mdiTestTube } from '@mdi/js'
+import SectionMain from '@/components/SectionMain.vue'
+import {ref} from 'vue'
+import CardBox from '@/components/CardBox.vue'
+import LayoutAuthenticated from '@/layouts/LayoutAuthenticated.vue'
+import SectionTitleLineWithButton from '@/components/SectionTitleLineWithButton.vue'
+import BaseButton from '@/components/BaseButton.vue'
+import UOTable from '@/components/UOTable.vue'
+import CardBoxModalForm from '@/components/CardBoxModalForm.vue'
+import FormUO from '@/components/FormUO.vue'
+import ZonasTable from '@/components/ZonasTable.vue'
+import PuntosMuestreoTable from '@/components/PuntosMuestreoTable.vue'
+
+
+const isModalActive = ref(false)
+const table = ref(null)
+
+const nuevaZona = {
+ esNuevo: true,
+ id: null,
+ nombre: null,
+ descripcion: null,
+ estado: null,
+ id_unidad_operativa: null,
+ id_unidad_operativa_nombre: null
+}
+
+
+const createPunto = () => {
+ // isModalActive.value = true;
+ table.value.openModal()
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
new file mode 100644
index 00000000..0f0bf907
--- /dev/null
+++ b/src/views/SettingsView.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
Panel de control
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/UOTableView.vue b/src/views/UOTableView.vue
new file mode 100644
index 00000000..868b61ad
--- /dev/null
+++ b/src/views/UOTableView.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/ZonasAdminView.vue b/src/views/ZonasAdminView.vue
new file mode 100644
index 00000000..7b6edcd7
--- /dev/null
+++ b/src/views/ZonasAdminView.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index e97af6e0..a54edd41 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -3,7 +3,7 @@
const plugin = require("tailwindcss/plugin");
module.exports = {
- content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
+ content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}", "./formkit.theme.mjs"],
darkMode: "class", // or 'media' or 'class'
theme: {
asideScrollbars: {
@@ -43,6 +43,7 @@ module.exports = {
},
plugins: [
require("@tailwindcss/forms"),
+ require('tailwindcss-primeui'),
plugin(function ({ matchUtilities, theme }) {
matchUtilities(
{
diff --git a/test-output.css b/test-output.css
new file mode 100644
index 00000000..cae54c49
--- /dev/null
+++ b/test-output.css
@@ -0,0 +1,77 @@
+/* Primary and Surface Palettes */
+
+:root {
+ --p-primary-50: #ecfdf5;
+ --p-primary-100: #d1fae5;
+ --p-primary-200: #a7f3d0;
+ --p-primary-300: #6ee7b7;
+ --p-primary-400: #34d399;
+ --p-primary-500: #10b981;
+ --p-primary-600: #059669;
+ --p-primary-700: #047857;
+ --p-primary-800: #065f46;
+ --p-primary-900: #064e3b;
+ --p-primary-950: #022c22;
+ --p-surface-0: #ffffff;
+ --p-surface-50: #fafafa;
+ --p-surface-100: #f4f4f5;
+ --p-surface-200: #e4e4e7;
+ --p-surface-300: #d4d4d8;
+ --p-surface-400: #a1a1aa;
+ --p-surface-500: #71717a;
+ --p-surface-600: #52525b;
+ --p-surface-700: #3f3f46;
+ --p-surface-800: #27272a;
+ --p-surface-900: #18181b;
+ --p-surface-950: #09090b;
+ --p-content-border-radius: 6px;
+}
+
+/* Light */
+
+:root {
+ --p-primary-color: var(--p-primary-500);
+ --p-primary-contrast-color: var(--p-surface-0);
+ --p-primary-hover-color: var(--p-primary-600);
+ --p-primary-active-color: var(--p-primary-700);
+ --p-content-border-color: var(--p-surface-200);
+ --p-content-hover-background: var(--p-surface-100);
+ --p-content-hover-color: var(--p-surface-800);
+ --p-highlight-background: var(--p-primary-50);
+ --p-highlight-color: var(--p-primary-700);
+ --p-highlight-focus-background: var(--p-primary-100);
+ --p-highlight-focus-color: var(--p-primary-800);
+ --p-text-color: var(--p-surface-700);
+ --p-text-hover-color: var(--p-surface-800);
+ --p-text-muted-color: var(--p-surface-500);
+ --p-text-hover-muted-color: var(--p-surface-600);
+}
+
+/*
+ * Dark Mode
+ * Defaults to system, change the selector to match the darkMode in tailwind.config.
+ * For example;
+ * darkMode: ['selector', '[class*="app-dark"]']
+ * should be;
+ * :root[class="app-dark"] {
+*/
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --p-primary-color: var(--p-primary-400);
+ --p-primary-contrast-color: var(--p-surface-900);
+ --p-primary-hover-color: var(--p-primary-300);
+ --p-primary-active-color: var(--p-primary-200);
+ --p-content-border-color: var(--p-surface-700);
+ --p-content-hover-background: var(--p-surface-800);
+ --p-content-hover-color: var(--p-surface-0);
+ --p-highlight-background: color-mix(in srgb, var(--p-primary-400), transparent 84%);
+ --p-highlight-color: rgba(255, 255, 255, 0.87);
+ --p-highlight-focus-background: color-mix(in srgb, var(--p-primary-400), transparent 76%);
+ --p-highlight-focus-color: rgba(255, 255, 255, 0.87);
+ --p-text-color: var(--p-surface-0);
+ --p-text-hover-color: var(--p-surface-0);
+ --p-text-muted-color: var(--p-surface-400);
+ --p-text-hover-muted-color: var(--p-surface-300);
+ }
+}
diff --git a/vite.config.js b/vite.config.js
index 8276be86..619b1f3a 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,15 +1,45 @@
import { fileURLToPath, URL } from "node:url";
-
-import { defineConfig } from "vite";
+import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
-export default defineConfig({
- base: "/admin-one-vue-tailwind/",
- plugins: [vue()],
- resolve: {
- alias: {
- "@": fileURLToPath(new URL("./src", import.meta.url)),
+// export default defineConfig({
+// base: "/sinaq/",
+// plugins: [vue()],
+// build: {
+// target: 'ES2022' // Asegura que esbuild soporta Top-level await
+// },
+// define: {
+// '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false,
+// // Otras banderas de características que necesites
+// },
+// resolve: {
+// alias: {
+// "@": fileURLToPath(new URL("./src", import.meta.url)),
+// },
+// },
+// });
+export default defineConfig(({ mode }) => {
+ // Cargar variables de entorno (Doppler las inyectará automáticamente)
+ const env = loadEnv(mode, process.cwd(), '');
+
+ return {
+ base: env.VITE_BASE_URL || "/sinaq/",
+ plugins: [vue()],
+ build: {
+ target: 'ES2022'
+ },
+ define: {
+ '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false,
+ },
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
+ },
},
- },
-});
+ server: {
+ // Configuración específica para desarrollo
+ port: parseInt(env.VITE_DEV_PORT) || 3000,
+ }
+ };
+});
\ No newline at end of file
diff --git a/vueform.config.js b/vueform.config.js
new file mode 100644
index 00000000..c34f45ac
--- /dev/null
+++ b/vueform.config.js
@@ -0,0 +1,14 @@
+// vueform.config.(js|ts)
+
+import es from '@vueform/vueform/locales/es'
+import vueform from '@vueform/vueform/dist/vueform'
+import { defineConfig } from '@vueform/vueform'
+
+// You might place these anywhere else in your project
+import '@vueform/vueform/dist/vueform.css';
+
+export default defineConfig({
+ theme: vueform,
+ locales: { es },
+ locale: 'es',
+})
\ No newline at end of file