344 lines
12 KiB
Vue
344 lines
12 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<Toast />
|
|
|
|
<!-- Page Title & CTA -->
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold tracking-tight">Gestión de Empresas</h1>
|
|
<p class="text-slate-500 dark:text-slate-400">Administra y monitorea todas las entidades legales registradas en el sistema.</p>
|
|
</div>
|
|
<Button
|
|
label="Registrar Nueva Empresa"
|
|
icon="pi pi-plus"
|
|
@click="openCreateDialog"
|
|
class="p-button-primary"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Table Section -->
|
|
<Card>
|
|
<template #content>
|
|
<DataTable
|
|
:value="companies"
|
|
:lazy="true"
|
|
:paginator="true"
|
|
:rows="perPage"
|
|
:totalRecords="totalRecords"
|
|
:loading="loading"
|
|
:rowsPerPageOptions="[10, 20, 50]"
|
|
@page="onPageChange"
|
|
responsiveLayout="scroll"
|
|
class="p-datatable-sm"
|
|
>
|
|
|
|
<Column field="company_name" header="Empresa" :sortable="true" style="min-width: 250px">
|
|
<template #body="{ data }">
|
|
<div class="flex items-center gap-3">
|
|
<Avatar
|
|
:label="getInitials(data.company_name)"
|
|
shape="square"
|
|
size="large"
|
|
class="bg-primary/10 text-primary font-bold"
|
|
/>
|
|
<div>
|
|
<p class="font-semibold text-sm">{{ data.company_name }}</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="rfc" header="RFC" :sortable="true" style="min-width: 150px">
|
|
<template #body="{ data }">
|
|
<span class="font-mono text-sm">{{ data.rfc }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="curp" header="CURP / Registro" :sortable="true" style="min-width: 150px">
|
|
<template #body="{ data }">
|
|
<span class="font-mono text-sm">{{ data.curp || 'N/A' }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="address" header="Ubicación" style="min-width: 200px">
|
|
<template #body="{ data }">
|
|
<div v-if="data.address">
|
|
<p class="text-sm">{{ data.address.city }}</p>
|
|
<p class="text-xs text-slate-400">{{ data.address.state }}</p>
|
|
</div>
|
|
<span v-else class="text-slate-400">Sin dirección</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Acciones" :exportable="false" style="min-width: 120px">
|
|
<template #body="{ data }">
|
|
<div class="flex gap-2">
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
class="p-button-rounded p-button-text p-button-sm"
|
|
@click="editCompany(data)"
|
|
v-tooltip.top="'Editar'"
|
|
/>
|
|
<Button
|
|
icon="pi pi-eye"
|
|
class="p-button-rounded p-button-text p-button-sm"
|
|
@click="viewCompany(data)"
|
|
v-tooltip.top="'Ver detalles'"
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
class="p-button-rounded p-button-text p-button-sm p-button-danger"
|
|
@click="confirmDelete(data)"
|
|
v-tooltip.top="'Eliminar'"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
|
|
<template #empty>
|
|
<div class="text-center py-8">
|
|
<i class="pi pi-inbox text-4xl text-slate-300 mb-3"></i>
|
|
<p class="text-slate-500">No se encontraron empresas registradas</p>
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Form Dialog -->
|
|
<CompaniesForm
|
|
ref="companiesFormRef"
|
|
:visible="dialogVisible"
|
|
:company="selectedCompany"
|
|
@close="dialogVisible = false"
|
|
@save="handleSave"
|
|
/>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<Dialog v-model:visible="deleteDialogVisible" :modal="true" header="Confirmar Eliminación" :style="{ width: '450px' }">
|
|
<div class="flex items-center gap-3">
|
|
<i class="pi pi-exclamation-triangle text-3xl text-amber-500"></i>
|
|
<span>¿Está seguro de eliminar la empresa <strong>{{ companyToDelete?.company_name }}</strong>?</span>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Cancelar" icon="pi pi-times" @click="deleteDialogVisible = false" class="p-button-text" />
|
|
<Button label="Eliminar" icon="pi pi-check" @click="deleteCompany" class="p-button-danger" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import CompaniesForm from './CompaniesForm.vue';
|
|
import { companyService } from './companies.service';
|
|
import type { Company, PaginatedResponse } from './companies.types';
|
|
|
|
// Toast
|
|
const toast = useToast();
|
|
|
|
// Refs
|
|
const companiesFormRef = ref<InstanceType<typeof CompaniesForm> | null>(null);
|
|
|
|
// State
|
|
const companies = ref<Company[]>([]);
|
|
const loading = ref(false);
|
|
const dialogVisible = ref(false);
|
|
const deleteDialogVisible = ref(false);
|
|
const selectedCompany = ref<Company | null>(null);
|
|
const companyToDelete = ref<Company | null>(null);
|
|
|
|
// Pagination
|
|
const currentPage = ref(1);
|
|
const totalRecords = ref(0);
|
|
const perPage = ref(10);
|
|
|
|
// Search filters
|
|
const searchFilters = ref({
|
|
rfc: '',
|
|
curp: '',
|
|
company_name: ''
|
|
});
|
|
|
|
// Methods
|
|
const loadCompanies = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const params = {
|
|
paginate: true,
|
|
page: currentPage.value,
|
|
...searchFilters.value
|
|
};
|
|
|
|
const response = await companyService.getAll(params);
|
|
|
|
if ('current_page' in response) {
|
|
// Respuesta paginada
|
|
const paginatedData = response as PaginatedResponse<Company>;
|
|
companies.value = paginatedData.data;
|
|
currentPage.value = paginatedData.current_page;
|
|
totalRecords.value = paginatedData.total;
|
|
perPage.value = paginatedData.per_page;
|
|
} else {
|
|
// Respuesta sin paginación
|
|
companies.value = response as Company[];
|
|
totalRecords.value = companies.value.length;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading companies:', error);
|
|
companies.value = [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const onPageChange = (event: any) => {
|
|
currentPage.value = event.page + 1; // PrimeVue usa índice base 0
|
|
loadCompanies();
|
|
};
|
|
|
|
const openCreateDialog = () => {
|
|
selectedCompany.value = null;
|
|
dialogVisible.value = true;
|
|
};
|
|
|
|
const editCompany = (company: any) => {
|
|
selectedCompany.value = { ...company };
|
|
dialogVisible.value = true;
|
|
};
|
|
|
|
const viewCompany = (company: any) => {
|
|
// TODO: Implementar vista de detalles
|
|
console.log('View company:', company);
|
|
};
|
|
|
|
const confirmDelete = (company: any) => {
|
|
companyToDelete.value = company;
|
|
deleteDialogVisible.value = true;
|
|
};
|
|
|
|
const deleteCompany = async () => {
|
|
if (!companyToDelete.value?.id) return;
|
|
|
|
try {
|
|
await companyService.delete(companyToDelete.value.id);
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Empresa eliminada',
|
|
detail: `La empresa "${companyToDelete.value.company_name}" ha sido eliminada exitosamente`,
|
|
life: 3000
|
|
});
|
|
|
|
// Recargar la lista después de eliminar
|
|
await loadCompanies();
|
|
|
|
deleteDialogVisible.value = false;
|
|
companyToDelete.value = null;
|
|
} catch (error: any) {
|
|
console.error('Error deleting company:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error al eliminar',
|
|
detail: error.response?.data?.message || 'No se pudo eliminar la empresa',
|
|
life: 5000
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSave = async (companyData: any) => {
|
|
try {
|
|
if (selectedCompany.value?.id) {
|
|
// Actualizar empresa existente
|
|
await companyService.update(selectedCompany.value.id, companyData);
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Empresa actualizada',
|
|
detail: `La empresa "${companyData.company_name}" ha sido actualizada exitosamente`,
|
|
life: 3000
|
|
});
|
|
} else {
|
|
// Crear nueva empresa
|
|
await companyService.create(companyData);
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Empresa creada',
|
|
detail: `La empresa "${companyData.company_name}" ha sido registrada exitosamente`,
|
|
life: 3000
|
|
});
|
|
}
|
|
|
|
// Recargar la lista después de guardar
|
|
await loadCompanies();
|
|
|
|
dialogVisible.value = false;
|
|
selectedCompany.value = null;
|
|
|
|
return true;
|
|
} catch (error: any) {
|
|
console.error('Error saving company:', error);
|
|
|
|
// Verificar si es un error de validación (422)
|
|
if (error.response?.status === 422 && error.response?.data?.errors) {
|
|
const backendErrors = error.response.data.errors;
|
|
|
|
// Setear los errores en el formulario
|
|
if (companiesFormRef.value) {
|
|
companiesFormRef.value.setValidationErrors(backendErrors);
|
|
}
|
|
|
|
// Mostrar toast con mensaje general
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Errores de validación',
|
|
detail: error.response.data.message || 'Por favor, corrija los errores en el formulario',
|
|
life: 5000
|
|
});
|
|
} else {
|
|
// Otros errores
|
|
const errorMessage = error.response?.data?.message ||
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'No se pudo guardar la empresa';
|
|
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: selectedCompany.value?.id ? 'Error al actualizar' : 'Error al crear',
|
|
detail: errorMessage,
|
|
life: 5000
|
|
});
|
|
|
|
// Resetear loading en el formulario para otros tipos de error
|
|
if (companiesFormRef.value) {
|
|
companiesFormRef.value.setValidationErrors({});
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getInitials = (name: string): string => {
|
|
return name
|
|
.split(' ')
|
|
.map(word => word[0])
|
|
.join('')
|
|
.substring(0, 2)
|
|
.toUpperCase();
|
|
};
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadCompanies();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.space-y-6 > * + * {
|
|
margin-top: 1.5rem;
|
|
}
|
|
</style> |