#49 feat: CRUD admin de organizaciones, proveedores y geografía
This commit is contained in:
@@ -111,6 +111,9 @@
|
|||||||
{ title: 'Imágenes de Catálogo', route: '/admin/catalogue-images', icon: 'mdi-image-multiple'},
|
{ title: 'Imágenes de Catálogo', route: '/admin/catalogue-images', icon: 'mdi-image-multiple'},
|
||||||
{ title: 'Datos de la Tienda', route: '/admin/store-settings', icon: 'mdi-map-marker'},
|
{ title: 'Datos de la Tienda', route: '/admin/store-settings', icon: 'mdi-map-marker'},
|
||||||
{ title: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'},
|
{ title: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'},
|
||||||
|
{ title: 'Organizaciones', route: '/admin/organizations', icon: 'mdi-domain'},
|
||||||
|
{ title: 'Proveedores', route: '/admin/suppliers', icon: 'mdi-truck'},
|
||||||
|
{ title: 'Geografía', route: '/admin/geography', icon: 'mdi-earth'},
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ header: 'Sincronización Tryton' },
|
{ header: 'Sincronización Tryton' },
|
||||||
{ title: 'Importar Productos', route: '/sincronizar_productos_tryton', icon: 'mdi-download'},
|
{ title: 'Importar Productos', route: '/sincronizar_productos_tryton', icon: 'mdi-download'},
|
||||||
|
|||||||
687
src/components/provenance/admin/GeographyManagement.vue
Normal file
687
src/components/provenance/admin/GeographyManagement.vue
Normal file
@@ -0,0 +1,687 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<h1 class="text-h4 mb-4">Gestión de Geografía</h1>
|
||||||
|
|
||||||
|
<v-tabs v-model="activeTab" color="primary">
|
||||||
|
<v-tab
|
||||||
|
data-testid="geo-tab-countries"
|
||||||
|
value="countries"
|
||||||
|
>
|
||||||
|
Países
|
||||||
|
</v-tab>
|
||||||
|
<v-tab
|
||||||
|
data-testid="geo-tab-departments"
|
||||||
|
value="departments"
|
||||||
|
>
|
||||||
|
Departamentos
|
||||||
|
</v-tab>
|
||||||
|
<v-tab
|
||||||
|
data-testid="geo-tab-municipalities"
|
||||||
|
value="municipalities"
|
||||||
|
>
|
||||||
|
Municipios
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<!-- Países -->
|
||||||
|
<v-row v-if="activeTab === 'countries'" class="mt-2">
|
||||||
|
<v-col class="text-right" cols="12">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="geo-country-create"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openCreateCountry"
|
||||||
|
>
|
||||||
|
Nuevo país
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
density="compact"
|
||||||
|
:headers="countryHeaders"
|
||||||
|
item-value="id"
|
||||||
|
:items="countries"
|
||||||
|
items-per-page="25"
|
||||||
|
:items-per-page-options="[10, 25, 50, 100]"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
:data-testid="'geo-country-edit-' + item.id"
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openEditCountry(item)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:data-testid="'geo-country-delete-' + item.id"
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openDeleteCountry(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #loading>
|
||||||
|
<v-skeleton-loader type="table-row@10" />
|
||||||
|
</template>
|
||||||
|
<template #no-data>
|
||||||
|
<v-alert class="my-4" type="info" variant="tonal">
|
||||||
|
No hay países para mostrar
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Departamentos -->
|
||||||
|
<v-row v-if="activeTab === 'departments'" class="mt-2">
|
||||||
|
<v-col class="text-right" cols="12">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="geo-department-create"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openCreateDepartment"
|
||||||
|
>
|
||||||
|
Nuevo departamento
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
density="compact"
|
||||||
|
:headers="departmentHeaders"
|
||||||
|
item-value="id"
|
||||||
|
:items="departments"
|
||||||
|
items-per-page="25"
|
||||||
|
:items-per-page-options="[10, 25, 50, 100]"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #item.country="{ item }">
|
||||||
|
{{ item.country_detail?.name }}
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
:data-testid="'geo-department-edit-' + item.id"
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openEditDepartment(item)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:data-testid="'geo-department-delete-' + item.id"
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openDeleteDepartment(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #loading>
|
||||||
|
<v-skeleton-loader type="table-row@10" />
|
||||||
|
</template>
|
||||||
|
<template #no-data>
|
||||||
|
<v-alert class="my-4" type="info" variant="tonal">
|
||||||
|
No hay departamentos para mostrar
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Municipios -->
|
||||||
|
<v-row v-if="activeTab === 'municipalities'" class="mt-2">
|
||||||
|
<v-col class="text-right" cols="12">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="geo-municipality-create"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openCreateMunicipality"
|
||||||
|
>
|
||||||
|
Nuevo municipio
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
density="compact"
|
||||||
|
:headers="municipalityHeaders"
|
||||||
|
item-value="id"
|
||||||
|
:items="municipalities"
|
||||||
|
items-per-page="25"
|
||||||
|
:items-per-page-options="[10, 25, 50, 100]"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #item.department="{ item }">
|
||||||
|
{{ item.department_detail?.name }}
|
||||||
|
</template>
|
||||||
|
<template #item.country="{ item }">
|
||||||
|
{{ item.country_detail?.name }}
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
:data-testid="'geo-municipality-edit-' + item.id"
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openEditMunicipality(item)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:data-testid="'geo-municipality-delete-' + item.id"
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openDeleteMunicipality(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #loading>
|
||||||
|
<v-skeleton-loader type="table-row@10" />
|
||||||
|
</template>
|
||||||
|
<template #no-data>
|
||||||
|
<v-alert class="my-4" type="info" variant="tonal">
|
||||||
|
No hay municipios para mostrar
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Diálogo país -->
|
||||||
|
<v-dialog v-model="countryDialog.show" max-width="480">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
{{ countryDialog.isEdit ? 'Editar país' : 'Nuevo país' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="saveCountry">
|
||||||
|
<v-text-field
|
||||||
|
v-model="countryForm.name"
|
||||||
|
data-testid="geo-country-form-name"
|
||||||
|
label="Nombre"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="countryForm.code"
|
||||||
|
data-testid="geo-country-form-code"
|
||||||
|
label="Código (ISO)"
|
||||||
|
maxlength="3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="countryDialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="geo-country-save"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="saveCountry"
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Diálogo departamento -->
|
||||||
|
<v-dialog v-model="departmentDialog.show" max-width="480">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
{{ departmentDialog.isEdit ? 'Editar departamento' : 'Nuevo departamento' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="saveDepartment">
|
||||||
|
<v-text-field
|
||||||
|
v-model="departmentForm.name"
|
||||||
|
data-testid="geo-department-form-name"
|
||||||
|
label="Nombre"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="departmentForm.country"
|
||||||
|
data-testid="geo-department-form-country"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
:items="countries"
|
||||||
|
label="País"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="departmentDialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="geo-department-save"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="saveDepartment"
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Diálogo municipio -->
|
||||||
|
<v-dialog v-model="municipalityDialog.show" max-width="480">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
{{ municipalityDialog.isEdit ? 'Editar municipio' : 'Nuevo municipio' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="saveMunicipality">
|
||||||
|
<v-text-field
|
||||||
|
v-model="municipalityForm.name"
|
||||||
|
data-testid="geo-municipality-form-name"
|
||||||
|
label="Nombre"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="municipalityForm.department"
|
||||||
|
data-testid="geo-municipality-form-department"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
:items="departmentItems"
|
||||||
|
label="Departamento"
|
||||||
|
required
|
||||||
|
:search-input="departmentSearch"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="municipalityForm.country"
|
||||||
|
data-testid="geo-municipality-form-country"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
:items="countries"
|
||||||
|
label="País"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="municipalityDialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="geo-municipality-save"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="saveMunicipality"
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleteCountryDialog" max-width="420">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Eliminar país</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
¿Está seguro de eliminar "{{ deleteCountryTarget?.name }}"?
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteCountryDialog = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="confirmDeleteCountry"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleteDepartmentDialog" max-width="420">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Eliminar departamento</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
¿Está seguro de eliminar "{{ deleteDepartmentTarget?.name }}"?
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteDepartmentDialog = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="confirmDeleteDepartment"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleteMunicipalityDialog" max-width="420">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Eliminar municipio</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
¿Está seguro de eliminar "{{ deleteMunicipalityTarget?.name }}"?
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteMunicipalityDialog = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="confirmDeleteMunicipality"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.show"
|
||||||
|
:color="snackbar.color"
|
||||||
|
location="top"
|
||||||
|
:timeout="3000"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
<template #actions>
|
||||||
|
<v-btn variant="text" @click="snackbar.show = false">Cerrar</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const api = inject('api')
|
||||||
|
|
||||||
|
const activeTab = ref('countries')
|
||||||
|
const countries = ref([])
|
||||||
|
const departments = ref([])
|
||||||
|
const municipalities = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const departmentSearch = ref('')
|
||||||
|
const snackbar = ref({ show: false, message: '', color: 'success' })
|
||||||
|
|
||||||
|
const countryHeaders = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Nombre', key: 'name' },
|
||||||
|
{ title: 'Código', key: 'code' },
|
||||||
|
{ title: 'Acciones', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const departmentHeaders = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Nombre', key: 'name' },
|
||||||
|
{ title: 'País', key: 'country' },
|
||||||
|
{ title: 'Acciones', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const municipalityHeaders = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Nombre', key: 'name' },
|
||||||
|
{ title: 'Departamento', key: 'department' },
|
||||||
|
{ title: 'País', key: 'country' },
|
||||||
|
{ title: 'Acciones', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const countryDialog = ref({ show: false, isEdit: false, item: null })
|
||||||
|
const countryForm = ref({ name: '', code: '' })
|
||||||
|
|
||||||
|
const departmentDialog = ref({ show: false, isEdit: false, item: null })
|
||||||
|
const departmentForm = ref({ name: '', country: null })
|
||||||
|
|
||||||
|
const municipalityDialog = ref({ show: false, isEdit: false, item: null })
|
||||||
|
const municipalityForm = ref({ name: '', department: null, country: null })
|
||||||
|
|
||||||
|
const departmentItems = computed(() => {
|
||||||
|
const query = departmentSearch.value.trim().toLowerCase()
|
||||||
|
let filtered = departments.value
|
||||||
|
if (query) {
|
||||||
|
filtered = departments.value.filter(department => {
|
||||||
|
return (department.name || '').toLowerCase().includes(query)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const selectedId = Number(municipalityForm.value.department)
|
||||||
|
const hasSelected = filtered.some(department => department.id === selectedId)
|
||||||
|
if (selectedId && !hasSelected) {
|
||||||
|
const selected = departments.value.find(department => department.id === selectedId)
|
||||||
|
if (selected) filtered = [selected, ...filtered]
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadCountries () {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
countries.value = await api.getCountries()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar países:', error)
|
||||||
|
showSnackbar('Error al cargar países', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDepartments () {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
departments.value = await api.getDepartments()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar departamentos:', error)
|
||||||
|
showSnackbar('Error al cargar departamentos', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMunicipalities () {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
municipalities.value = await api.getMunicipalities()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar municipios:', error)
|
||||||
|
showSnackbar('Error al cargar municipios', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateCountry () {
|
||||||
|
countryForm.value = { name: '', code: '' }
|
||||||
|
countryDialog.value = { show: true, isEdit: false, item: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditCountry (item) {
|
||||||
|
countryForm.value = { name: item.name, code: item.code }
|
||||||
|
countryDialog.value = { show: true, isEdit: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCountry () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (countryDialog.value.isEdit) {
|
||||||
|
await api.updateCountry(countryDialog.value.item.id, countryForm.value)
|
||||||
|
showSnackbar('País actualizado', 'success')
|
||||||
|
} else {
|
||||||
|
await api.createCountry(countryForm.value)
|
||||||
|
showSnackbar('País creado', 'success')
|
||||||
|
}
|
||||||
|
countryDialog.value.show = false
|
||||||
|
await loadCountries()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al guardar país:', error)
|
||||||
|
showSnackbar('Error al guardar país', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteCountry (item) {
|
||||||
|
deleteCountryTarget.value = item
|
||||||
|
deleteCountryDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCountryDialog = ref(false)
|
||||||
|
const deleteCountryTarget = ref(null)
|
||||||
|
|
||||||
|
async function confirmDeleteCountry () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await api.deleteCountry(deleteCountryTarget.value.id)
|
||||||
|
deleteCountryDialog.value = false
|
||||||
|
showSnackbar('País eliminado', 'success')
|
||||||
|
await loadCountries()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar país:', error)
|
||||||
|
showSnackbar('Error al eliminar país', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDepartment () {
|
||||||
|
departmentForm.value = { name: '', country: null }
|
||||||
|
departmentDialog.value = { show: true, isEdit: false, item: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDepartment (item) {
|
||||||
|
departmentForm.value = { name: item.name, country: item.country }
|
||||||
|
departmentDialog.value = { show: true, isEdit: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDepartment () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (departmentDialog.value.isEdit) {
|
||||||
|
await api.updateDepartment(departmentDialog.value.item.id, departmentForm.value)
|
||||||
|
showSnackbar('Departamento actualizado', 'success')
|
||||||
|
} else {
|
||||||
|
await api.createDepartment(departmentForm.value)
|
||||||
|
showSnackbar('Departamento creado', 'success')
|
||||||
|
}
|
||||||
|
departmentDialog.value.show = false
|
||||||
|
await loadDepartments()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al guardar departamento:', error)
|
||||||
|
showSnackbar('Error al guardar departamento', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDepartmentDialog = ref(false)
|
||||||
|
const deleteDepartmentTarget = ref(null)
|
||||||
|
|
||||||
|
function openDeleteDepartment (item) {
|
||||||
|
deleteDepartmentTarget.value = item
|
||||||
|
deleteDepartmentDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteDepartment () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await api.deleteDepartment(deleteDepartmentTarget.value.id)
|
||||||
|
deleteDepartmentDialog.value = false
|
||||||
|
showSnackbar('Departamento eliminado', 'success')
|
||||||
|
await loadDepartments()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar departamento:', error)
|
||||||
|
showSnackbar('Error al eliminar departamento', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateMunicipality () {
|
||||||
|
municipalityForm.value = { name: '', department: null, country: null }
|
||||||
|
municipalityDialog.value = { show: true, isEdit: false, item: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditMunicipality (item) {
|
||||||
|
municipalityForm.value = {
|
||||||
|
name: item.name,
|
||||||
|
department: item.department,
|
||||||
|
country: item.country,
|
||||||
|
}
|
||||||
|
municipalityDialog.value = { show: true, isEdit: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => municipalityForm.value.department,
|
||||||
|
departmentId => {
|
||||||
|
if (!departmentId) return
|
||||||
|
const department = departments.value.find(item => item.id === departmentId)
|
||||||
|
if (department && department.country) {
|
||||||
|
municipalityForm.value.country = department.country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function saveMunicipality () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (municipalityDialog.value.isEdit) {
|
||||||
|
await api.updateMunicipality(municipalityDialog.value.item.id, municipalityForm.value)
|
||||||
|
showSnackbar('Municipio actualizado', 'success')
|
||||||
|
} else {
|
||||||
|
await api.createMunicipality(municipalityForm.value)
|
||||||
|
showSnackbar('Municipio creado', 'success')
|
||||||
|
}
|
||||||
|
municipalityDialog.value.show = false
|
||||||
|
await loadMunicipalities()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al guardar municipio:', error)
|
||||||
|
showSnackbar('Error al guardar municipio', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMunicipalityDialog = ref(false)
|
||||||
|
const deleteMunicipalityTarget = ref(null)
|
||||||
|
|
||||||
|
function openDeleteMunicipality (item) {
|
||||||
|
deleteMunicipalityTarget.value = item
|
||||||
|
deleteMunicipalityDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteMunicipality () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await api.deleteMunicipality(deleteMunicipalityTarget.value.id)
|
||||||
|
deleteMunicipalityDialog.value = false
|
||||||
|
showSnackbar('Municipio eliminado', 'success')
|
||||||
|
await loadMunicipalities()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar municipio:', error)
|
||||||
|
showSnackbar('Error al eliminar municipio', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSnackbar (message, color) {
|
||||||
|
snackbar.value = { show: true, message, color }
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(activeTab, tab => {
|
||||||
|
if (tab === 'departments') loadDepartments()
|
||||||
|
if (tab === 'municipalities') {
|
||||||
|
loadMunicipalities()
|
||||||
|
loadDepartments()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(loadCountries)
|
||||||
|
</script>
|
||||||
299
src/components/provenance/admin/OrganizationsManagement.vue
Normal file
299
src/components/provenance/admin/OrganizationsManagement.vue
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h1 class="text-h4">Gestión de Organizaciones</h1>
|
||||||
|
</v-col>
|
||||||
|
<v-col class="text-md-right" cols="12" md="6">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="org-create"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
Nueva organización
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
clearable
|
||||||
|
data-testid="org-search"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
label="Buscar por nombre"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
density="compact"
|
||||||
|
:headers="headers"
|
||||||
|
item-value="id"
|
||||||
|
:items="filteredOrganizations"
|
||||||
|
items-per-page="25"
|
||||||
|
:items-per-page-options="[10, 25, 50, 100]"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #item.website="{ item }">
|
||||||
|
<a
|
||||||
|
v-if="item.website"
|
||||||
|
:href="item.website"
|
||||||
|
rel="noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ item.website }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
:data-testid="'org-edit-' + item.id"
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openEdit(item)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:data-testid="'org-delete-' + item.id"
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openDelete(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #loading>
|
||||||
|
<v-skeleton-loader type="table-row@10" />
|
||||||
|
</template>
|
||||||
|
<template #no-data>
|
||||||
|
<v-alert class="my-4" type="info" variant="tonal">
|
||||||
|
No hay organizaciones para mostrar
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-dialog v-model="dialog.show" max-width="520">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
{{ dialog.isEdit ? 'Editar organización' : 'Nueva organización' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="save">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.name"
|
||||||
|
data-testid="org-form-name"
|
||||||
|
label="Nombre"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
data-testid="org-form-description"
|
||||||
|
label="Descripción"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.website"
|
||||||
|
data-testid="org-form-website"
|
||||||
|
label="Sitio web"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.contact_email"
|
||||||
|
data-testid="org-form-email"
|
||||||
|
label="Correo de contacto"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.contact_phone"
|
||||||
|
data-testid="org-form-phone"
|
||||||
|
label="Teléfono de contacto"
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="dialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="org-save"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleteDialog.show" max-width="420">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Eliminar organización</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
¿Está seguro de eliminar "{{ deleteDialog.item?.name }}"?
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteDialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
data-testid="org-confirm-delete"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="confirmDelete"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.show"
|
||||||
|
:color="snackbar.color"
|
||||||
|
location="top"
|
||||||
|
:timeout="3000"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
<template #actions>
|
||||||
|
<v-btn variant="text" @click="snackbar.show = false">Cerrar</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const api = inject('api')
|
||||||
|
|
||||||
|
const organizations = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const snackbar = ref({ show: false, message: '', color: 'success' })
|
||||||
|
const dialog = ref({ show: false, isEdit: false, item: null })
|
||||||
|
const deleteDialog = ref({ show: false, item: null })
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Nombre', key: 'name' },
|
||||||
|
{ title: 'Descripción', key: 'description' },
|
||||||
|
{ title: 'Sitio web', key: 'website' },
|
||||||
|
{ title: 'Correo', key: 'contact_email' },
|
||||||
|
{ title: 'Teléfono', key: 'contact_phone' },
|
||||||
|
{ title: 'Acciones', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
website: '',
|
||||||
|
contact_email: '',
|
||||||
|
contact_phone: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = ref(emptyForm())
|
||||||
|
|
||||||
|
const filteredOrganizations = computed(() => {
|
||||||
|
const query = searchQuery.value.trim().toLowerCase()
|
||||||
|
if (!query) return organizations.value
|
||||||
|
return organizations.value.filter(organization => {
|
||||||
|
return (organization.name || '').toLowerCase().includes(query)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function load () {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
organizations.value = await api.getOrganizations()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar organizaciones:', error)
|
||||||
|
showSnackbar('Error al cargar organizaciones', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate () {
|
||||||
|
formData.value = emptyForm()
|
||||||
|
dialog.value = { show: true, isEdit: false, item: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit (item) {
|
||||||
|
formData.value = {
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
website: item.website,
|
||||||
|
contact_email: item.contact_email,
|
||||||
|
contact_phone: item.contact_phone,
|
||||||
|
}
|
||||||
|
dialog.value = { show: true, isEdit: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (dialog.value.isEdit) {
|
||||||
|
await api.updateOrganization(dialog.value.item.id, formData.value)
|
||||||
|
showSnackbar('Organización actualizada', 'success')
|
||||||
|
} else {
|
||||||
|
await api.createOrganization(formData.value)
|
||||||
|
showSnackbar('Organización creada', 'success')
|
||||||
|
}
|
||||||
|
dialog.value.show = false
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al guardar organización:', error)
|
||||||
|
showSnackbar('Error al guardar organización', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDelete (item) {
|
||||||
|
deleteDialog.value = { show: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await api.deleteOrganization(deleteDialog.value.item.id)
|
||||||
|
deleteDialog.value.show = false
|
||||||
|
showSnackbar('Organización eliminada', 'success')
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar organización:', error)
|
||||||
|
showSnackbar('Error al eliminar organización', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSnackbar (message, color) {
|
||||||
|
snackbar.value = { show: true, message, color }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-md-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.text-md-right {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
338
src/components/provenance/admin/SuppliersManagement.vue
Normal file
338
src/components/provenance/admin/SuppliersManagement.vue
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h1 class="text-h4">Gestión de Proveedores</h1>
|
||||||
|
</v-col>
|
||||||
|
<v-col class="text-md-right" cols="12" md="6">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="supplier-create"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openCreate"
|
||||||
|
>
|
||||||
|
Nuevo proveedor
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
clearable
|
||||||
|
data-testid="supplier-search"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
label="Buscar por nombre"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
density="compact"
|
||||||
|
:headers="headers"
|
||||||
|
item-value="id"
|
||||||
|
:items="filteredSuppliers"
|
||||||
|
items-per-page="25"
|
||||||
|
:items-per-page-options="[10, 25, 50, 100]"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #item.organization="{ item }">
|
||||||
|
{{ item.organization_detail?.name }}
|
||||||
|
</template>
|
||||||
|
<template #item.municipality="{ item }">
|
||||||
|
{{ item.municipality_detail?.name }}
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
:data-testid="'supplier-edit-' + item.id"
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openEdit(item)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:data-testid="'supplier-delete-' + item.id"
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openDelete(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #loading>
|
||||||
|
<v-skeleton-loader type="table-row@10" />
|
||||||
|
</template>
|
||||||
|
<template #no-data>
|
||||||
|
<v-alert class="my-4" type="info" variant="tonal">
|
||||||
|
No hay proveedores para mostrar
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-dialog v-model="dialog.show" max-width="560">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
{{ dialog.isEdit ? 'Editar proveedor' : 'Nuevo proveedor' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="save">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.name"
|
||||||
|
data-testid="supplier-form-name"
|
||||||
|
label="Nombre"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
data-testid="supplier-form-description"
|
||||||
|
label="Descripción"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="formData.organization"
|
||||||
|
clearable
|
||||||
|
data-testid="supplier-form-organization"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
:items="organizations"
|
||||||
|
label="Organización"
|
||||||
|
/>
|
||||||
|
<v-autocomplete
|
||||||
|
v-model="formData.municipality"
|
||||||
|
clearable
|
||||||
|
data-testid="supplier-form-municipality"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
:items="municipalityItems"
|
||||||
|
label="Municipio"
|
||||||
|
:search-input="municipalitySearch"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.contact_email"
|
||||||
|
data-testid="supplier-form-email"
|
||||||
|
label="Correo de contacto"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.contact_phone"
|
||||||
|
data-testid="supplier-form-phone"
|
||||||
|
label="Teléfono de contacto"
|
||||||
|
/>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="dialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
data-testid="supplier-save"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleteDialog.show" max-width="420">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Eliminar proveedor</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
¿Está seguro de eliminar "{{ deleteDialog.item?.name }}"?
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteDialog.show = false">Cancelar</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
data-testid="supplier-confirm-delete"
|
||||||
|
:disabled="saving"
|
||||||
|
variant="elevated"
|
||||||
|
@click="confirmDelete"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.show"
|
||||||
|
:color="snackbar.color"
|
||||||
|
location="top"
|
||||||
|
:timeout="3000"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
<template #actions>
|
||||||
|
<v-btn variant="text" @click="snackbar.show = false">Cerrar</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const api = inject('api')
|
||||||
|
|
||||||
|
const suppliers = ref([])
|
||||||
|
const organizations = ref([])
|
||||||
|
const municipalities = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const municipalitySearch = ref('')
|
||||||
|
const snackbar = ref({ show: false, message: '', color: 'success' })
|
||||||
|
const dialog = ref({ show: false, isEdit: false, item: null })
|
||||||
|
const deleteDialog = ref({ show: false, item: null })
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'ID', key: 'id' },
|
||||||
|
{ title: 'Nombre', key: 'name' },
|
||||||
|
{ title: 'Organización', key: 'organization' },
|
||||||
|
{ title: 'Municipio', key: 'municipality' },
|
||||||
|
{ title: 'Correo', key: 'contact_email' },
|
||||||
|
{ title: 'Teléfono', key: 'contact_phone' },
|
||||||
|
{ title: 'Acciones', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const emptyForm = () => ({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
organization: null,
|
||||||
|
municipality: null,
|
||||||
|
contact_email: '',
|
||||||
|
contact_phone: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = ref(emptyForm())
|
||||||
|
|
||||||
|
const filteredSuppliers = computed(() => {
|
||||||
|
const query = searchQuery.value.trim().toLowerCase()
|
||||||
|
if (!query) return suppliers.value
|
||||||
|
return suppliers.value.filter(supplier => {
|
||||||
|
return (supplier.name || '').toLowerCase().includes(query)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const municipalityItems = computed(() => {
|
||||||
|
const query = municipalitySearch.value.trim().toLowerCase()
|
||||||
|
let filtered = municipalities.value
|
||||||
|
if (query) {
|
||||||
|
filtered = municipalities.value.filter(municipality => {
|
||||||
|
return (municipality.name || '').toLowerCase().includes(query)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const selectedId = Number(formData.value.municipality)
|
||||||
|
const hasSelected = filtered.some(municipality => municipality.id === selectedId)
|
||||||
|
if (selectedId && !hasSelected) {
|
||||||
|
const selected = municipalities.value.find(municipality => municipality.id === selectedId)
|
||||||
|
if (selected) filtered = [selected, ...filtered]
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
async function load () {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [suppliersData, organizationsData, municipalitiesData] = await Promise.all([
|
||||||
|
api.getSuppliers(),
|
||||||
|
api.getOrganizations(),
|
||||||
|
api.getMunicipalities(),
|
||||||
|
])
|
||||||
|
suppliers.value = suppliersData
|
||||||
|
organizations.value = organizationsData
|
||||||
|
municipalities.value = municipalitiesData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar proveedores:', error)
|
||||||
|
showSnackbar('Error al cargar proveedores', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate () {
|
||||||
|
formData.value = emptyForm()
|
||||||
|
dialog.value = { show: true, isEdit: false, item: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit (item) {
|
||||||
|
formData.value = {
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
organization: item.organization,
|
||||||
|
municipality: item.municipality,
|
||||||
|
contact_email: item.contact_email,
|
||||||
|
contact_phone: item.contact_phone,
|
||||||
|
}
|
||||||
|
dialog.value = { show: true, isEdit: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (dialog.value.isEdit) {
|
||||||
|
await api.updateSupplier(dialog.value.item.id, formData.value)
|
||||||
|
showSnackbar('Proveedor actualizado', 'success')
|
||||||
|
} else {
|
||||||
|
await api.createSupplier(formData.value)
|
||||||
|
showSnackbar('Proveedor creado', 'success')
|
||||||
|
}
|
||||||
|
dialog.value.show = false
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al guardar proveedor:', error)
|
||||||
|
showSnackbar('Error al guardar proveedor', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDelete (item) {
|
||||||
|
deleteDialog.value = { show: true, item }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete () {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await api.deleteSupplier(deleteDialog.value.item.id)
|
||||||
|
deleteDialog.value.show = false
|
||||||
|
showSnackbar('Proveedor eliminado', 'success')
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar proveedor:', error)
|
||||||
|
showSnackbar('Error al eliminar proveedor', 'error')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSnackbar (message, color) {
|
||||||
|
snackbar.value = { show: true, message, color }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-md-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.text-md-right {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
src/pages/admin/geography.vue
Normal file
10
src/pages/admin/geography.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<GeographyManagement v-if="authStore.isAdmin" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import GeographyManagement from '@/components/provenance/admin/GeographyManagement.vue'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
</script>
|
||||||
10
src/pages/admin/organizations.vue
Normal file
10
src/pages/admin/organizations.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<OrganizationsManagement v-if="authStore.isAdmin" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import OrganizationsManagement from '@/components/provenance/admin/OrganizationsManagement.vue'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
</script>
|
||||||
10
src/pages/admin/suppliers.vue
Normal file
10
src/pages/admin/suppliers.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<SuppliersManagement v-if="authStore.isAdmin" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import SuppliersManagement from '@/components/provenance/admin/SuppliersManagement.vue'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
</script>
|
||||||
@@ -25,6 +25,9 @@ const ADMIN_ROUTES = [
|
|||||||
'/admin/catalog-sales',
|
'/admin/catalog-sales',
|
||||||
'/admin/catalogue-images',
|
'/admin/catalogue-images',
|
||||||
'/admin/store-settings',
|
'/admin/store-settings',
|
||||||
|
'/admin/organizations',
|
||||||
|
'/admin/suppliers',
|
||||||
|
'/admin/geography',
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
|
import GeographyManagement from '@/components/provenance/admin/GeographyManagement.vue'
|
||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
|
import { clickBody, setBodyInput } from './helpers'
|
||||||
|
|
||||||
|
const countries = [
|
||||||
|
{ id: 1, name: 'Colombia', code: 'CO' },
|
||||||
|
{ id: 2, name: 'Venezuela', code: 'VE' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const departments = [
|
||||||
|
{ id: 2, name: 'Cundinamarca', country: 1, country_detail: { id: 1, name: 'Colombia', code: 'CO' } },
|
||||||
|
{ id: 3, name: 'Antioquia', country: 1, country_detail: { id: 1, name: 'Colombia', code: 'CO' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
const municipalities = [
|
||||||
|
{ id: 7, name: 'La Mesa', department: 2, country: 1, department_detail: { id: 2, name: 'Cundinamarca' } },
|
||||||
|
{ id: 8, name: 'San Antonio', department: 3, country: 1, department_detail: { id: 3, name: 'Antioquia' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
function mockApi () {
|
||||||
|
return {
|
||||||
|
getCountries: vi.fn().mockResolvedValue(countries),
|
||||||
|
createCountry: vi.fn().mockResolvedValue({}),
|
||||||
|
updateCountry: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteCountry: vi.fn().mockResolvedValue({}),
|
||||||
|
getDepartments: vi.fn().mockResolvedValue(departments),
|
||||||
|
createDepartment: vi.fn().mockResolvedValue({}),
|
||||||
|
updateDepartment: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteDepartment: vi.fn().mockResolvedValue({}),
|
||||||
|
getMunicipalities: vi.fn().mockResolvedValue(municipalities),
|
||||||
|
createMunicipality: vi.fn().mockResolvedValue({}),
|
||||||
|
updateMunicipality: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteMunicipality: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountComponent (api) {
|
||||||
|
return mount(GeographyManagement, {
|
||||||
|
global: { plugins: [vuetify], provide: { api } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GeographyManagement', () => {
|
||||||
|
it('muestra los países por defecto y permite crear uno', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.getCountries).toHaveBeenCalled()
|
||||||
|
expect(wrapper.text()).toContain('Colombia')
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="geo-country-create"]').trigger('click')
|
||||||
|
setBodyInput('[data-testid="geo-country-form-name"] input', 'Ecuador')
|
||||||
|
setBodyInput('[data-testid="geo-country-form-code"] input', 'EC')
|
||||||
|
clickBody('[data-testid="geo-country-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.createCountry).toHaveBeenCalledWith({ name: 'Ecuador', code: 'EC' })
|
||||||
|
expect(api.getCountries).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cambia a la pestaña de departamentos y crea uno', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="geo-tab-departments"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.getDepartments).toHaveBeenCalled()
|
||||||
|
expect(wrapper.text()).toContain('Cundinamarca')
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="geo-department-create"]').trigger('click')
|
||||||
|
setBodyInput('[data-testid="geo-department-form-name"] input', 'Risaralda')
|
||||||
|
const countrySelect = wrapper.findAllComponents({ name: 'VSelect' })
|
||||||
|
.find(select => select.props('items').some(item => item.id === 2))
|
||||||
|
await countrySelect.vm.$emit('update:modelValue', 2)
|
||||||
|
clickBody('[data-testid="geo-department-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.createDepartment).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
name: 'Risaralda',
|
||||||
|
country: 2,
|
||||||
|
}))
|
||||||
|
expect(api.getDepartments).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cambia a la pestaña de municipios y crea uno con departamento y país', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="geo-tab-municipalities"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.getMunicipalities).toHaveBeenCalled()
|
||||||
|
expect(wrapper.text()).toContain('San Antonio')
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="geo-municipality-create"]').trigger('click')
|
||||||
|
setBodyInput('[data-testid="geo-municipality-form-name"] input', 'Pereira')
|
||||||
|
await wrapper.findAllComponents({ name: 'VAutocomplete' })[0].vm.$emit('update:modelValue', 3)
|
||||||
|
clickBody('[data-testid="geo-municipality-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.createMunicipality).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
name: 'Pereira',
|
||||||
|
department: 3,
|
||||||
|
country: 1,
|
||||||
|
}))
|
||||||
|
expect(api.getMunicipalities).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
|
import OrganizationsManagement from '@/components/provenance/admin/OrganizationsManagement.vue'
|
||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
|
import { clickBody, setBodyInput } from './helpers'
|
||||||
|
|
||||||
|
const organizations = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Red de Economía Solidaria',
|
||||||
|
description: 'Red de organizaciones',
|
||||||
|
website: 'https://red.example.org',
|
||||||
|
contact_email: 'contacto@red.example.org',
|
||||||
|
contact_phone: '3001112233',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Cooperativa El Campo',
|
||||||
|
description: null,
|
||||||
|
website: null,
|
||||||
|
contact_email: null,
|
||||||
|
contact_phone: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function mockApi () {
|
||||||
|
return {
|
||||||
|
getOrganizations: vi.fn().mockResolvedValue(organizations),
|
||||||
|
createOrganization: vi.fn().mockResolvedValue({}),
|
||||||
|
updateOrganization: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteOrganization: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountComponent (api) {
|
||||||
|
return mount(OrganizationsManagement, {
|
||||||
|
global: { plugins: [vuetify], provide: { api } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm () {
|
||||||
|
setBodyInput('[data-testid="org-form-name"] input', 'Fundación Montaña')
|
||||||
|
setBodyInput('[data-testid="org-form-description"] textarea', 'Fundación')
|
||||||
|
setBodyInput('[data-testid="org-form-website"] input', 'https://montana.example.org')
|
||||||
|
setBodyInput('[data-testid="org-form-email"] input', 'info@montana.example.org')
|
||||||
|
setBodyInput('[data-testid="org-form-phone"] input', '3109876543')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OrganizationsManagement', () => {
|
||||||
|
it('carga y muestra las organizaciones', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.getOrganizations).toHaveBeenCalled()
|
||||||
|
expect(wrapper.text()).toContain('Red de Economía Solidaria')
|
||||||
|
expect(wrapper.text()).toContain('Cooperativa El Campo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filtra las organizaciones por búsqueda', async () => {
|
||||||
|
const wrapper = mountComponent(mockApi())
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="org-search"] input').setValue('Cooperativa')
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Cooperativa El Campo')
|
||||||
|
expect(wrapper.text()).not.toContain('Red de Economía Solidaria')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('crea una organización', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="org-create"]').trigger('click')
|
||||||
|
fillForm()
|
||||||
|
clickBody('[data-testid="org-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.createOrganization).toHaveBeenCalledWith({
|
||||||
|
name: 'Fundación Montaña',
|
||||||
|
description: 'Fundación',
|
||||||
|
website: 'https://montana.example.org',
|
||||||
|
contact_email: 'info@montana.example.org',
|
||||||
|
contact_phone: '3109876543',
|
||||||
|
})
|
||||||
|
expect(api.getOrganizations).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('edita una organización', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="org-edit-1"]').trigger('click')
|
||||||
|
setBodyInput('[data-testid="org-form-name"] input', 'Red de Economía Solidaria Colombia')
|
||||||
|
clickBody('[data-testid="org-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.updateOrganization).toHaveBeenCalledWith(1, expect.objectContaining({
|
||||||
|
name: 'Red de Economía Solidaria Colombia',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('elimina una organización', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="org-delete-2"]').trigger('click')
|
||||||
|
clickBody('[data-testid="org-confirm-delete"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.deleteOrganization).toHaveBeenCalledWith(2)
|
||||||
|
expect(api.getOrganizations).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
|
import SuppliersManagement from '@/components/provenance/admin/SuppliersManagement.vue'
|
||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
|
import { clickBody, setBodyInput } from './helpers'
|
||||||
|
|
||||||
|
const organizations = [
|
||||||
|
{ id: 3, name: 'Red de Economía Solidaria' },
|
||||||
|
{ id: 4, name: 'Cooperativa El Campo' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const municipalities = [
|
||||||
|
{ id: 7, name: 'La Mesa' },
|
||||||
|
{ id: 8, name: 'San Antonio' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const suppliers = [
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Asociación Agropecuaria La Mesa',
|
||||||
|
description: 'Cooperativa de campesinos',
|
||||||
|
organization: 3,
|
||||||
|
organization_detail: { id: 3, name: 'Red de Economía Solidaria' },
|
||||||
|
municipality: 7,
|
||||||
|
municipality_detail: { id: 7, name: 'La Mesa' },
|
||||||
|
contact_email: 'contacto@agro.example.org',
|
||||||
|
contact_phone: '300 123 4567',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Finca El Paraíso',
|
||||||
|
description: null,
|
||||||
|
organization: null,
|
||||||
|
organization_detail: null,
|
||||||
|
municipality: null,
|
||||||
|
municipality_detail: null,
|
||||||
|
contact_email: null,
|
||||||
|
contact_phone: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function mockApi () {
|
||||||
|
return {
|
||||||
|
getSuppliers: vi.fn().mockResolvedValue(suppliers),
|
||||||
|
getOrganizations: vi.fn().mockResolvedValue(organizations),
|
||||||
|
getMunicipalities: vi.fn().mockResolvedValue(municipalities),
|
||||||
|
createSupplier: vi.fn().mockResolvedValue({}),
|
||||||
|
updateSupplier: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteSupplier: vi.fn().mockResolvedValue({}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountComponent (api) {
|
||||||
|
return mount(SuppliersManagement, {
|
||||||
|
global: { plugins: [vuetify], provide: { api } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SuppliersManagement', () => {
|
||||||
|
it('carga y muestra los proveedores con su organización y municipio', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.getSuppliers).toHaveBeenCalled()
|
||||||
|
expect(wrapper.text()).toContain('Asociación Agropecuaria La Mesa')
|
||||||
|
expect(wrapper.text()).toContain('Red de Economía Solidaria')
|
||||||
|
expect(wrapper.text()).toContain('La Mesa')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('crea un proveedor con organización y municipio', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="supplier-create"]').trigger('click')
|
||||||
|
|
||||||
|
setBodyInput('[data-testid="supplier-form-name"] input', 'Asociación El Cedro')
|
||||||
|
setBodyInput('[data-testid="supplier-form-description"] textarea', 'Caficultores')
|
||||||
|
const orgSelect = wrapper.findAllComponents({ name: 'VSelect' })
|
||||||
|
.find(select => select.props('items').some(item => item.id === 4))
|
||||||
|
await orgSelect.vm.$emit('update:modelValue', 4)
|
||||||
|
await wrapper.findAllComponents({ name: 'VAutocomplete' })[0].vm.$emit('update:modelValue', 8)
|
||||||
|
clickBody('[data-testid="supplier-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.createSupplier).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
name: 'Asociación El Cedro',
|
||||||
|
description: 'Caficultores',
|
||||||
|
organization: 4,
|
||||||
|
municipality: 8,
|
||||||
|
}))
|
||||||
|
expect(api.getSuppliers).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('edita un proveedor conservando la organización seleccionada', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="supplier-edit-5"]').trigger('click')
|
||||||
|
setBodyInput('[data-testid="supplier-form-name"] input', 'Asociación Agropecuaria La Mesa Renovada')
|
||||||
|
clickBody('[data-testid="supplier-save"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.updateSupplier).toHaveBeenCalledWith(5, expect.objectContaining({
|
||||||
|
name: 'Asociación Agropecuaria La Mesa Renovada',
|
||||||
|
organization: 3,
|
||||||
|
municipality: 7,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('elimina un proveedor', async () => {
|
||||||
|
const api = mockApi()
|
||||||
|
const wrapper = mountComponent(api)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="supplier-delete-6"]').trigger('click')
|
||||||
|
clickBody('[data-testid="supplier-confirm-delete"]')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.deleteSupplier).toHaveBeenCalledWith(6)
|
||||||
|
expect(api.getSuppliers).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
23
tests/unit/components/provenance/admin/helpers.js
Normal file
23
tests/unit/components/provenance/admin/helpers.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Helpers para interactuar con el contenido de diálogos, que Vuetify
|
||||||
|
// teleporta a document.body fuera del DOM del wrapper.
|
||||||
|
|
||||||
|
function setNativeValue (el, value) {
|
||||||
|
const proto = el.tagName === 'TEXTAREA'
|
||||||
|
? window.HTMLTextAreaElement.prototype
|
||||||
|
: window.HTMLInputElement.prototype
|
||||||
|
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set
|
||||||
|
setter.call(el, value)
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBodyInput (selector, value) {
|
||||||
|
const el = document.body.querySelector(selector)
|
||||||
|
if (!el) throw new Error(`Input no encontrado en body: ${selector}`)
|
||||||
|
setNativeValue(el, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clickBody (selector) {
|
||||||
|
const el = document.body.querySelector(selector)
|
||||||
|
if (!el) throw new Error(`Elemento no encontrado en body: ${selector}`)
|
||||||
|
el.click()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user