panels-origin/web-panel/components/padron/WorkerImportDialog.vue
2026-08-21 16:55:01 -05:00

110 lines
5.4 KiB
Vue

<template>
<Dialog v-model:visible="visibleModel" header="Importar padrón desde Excel" modal :style="{ width: '820px' }">
<div class="import-layout">
<Message severity="info" :closable="false">Descargue la plantilla, complete la hoja PERSONAL y súbala. CURP, RFC y NSS se validan antes de guardar.</Message>
<div class="import-actions">
<Button label="Descargar plantilla" icon="pi pi-download" outlined :loading="templateLoading" @click="downloadTemplate" />
<FormField label="Proyecto activo opcional">
<Select v-model="projectId" :options="projects" option-label="name" option-value="id" placeholder="Sin asignación inicial" show-clear class="w-full" />
</FormField>
<FileUpload mode="basic" accept=".xlsx,.xls" :auto="true" choose-label="Seleccionar Excel" custom-upload :disabled="importing" @uploader="importFile" />
<div class="muted">Al elegir el Excel se importa solo.</div>
</div>
<ProgressBar v-if="importing" mode="indeterminate" style="height: 4px" />
<template v-if="report">
<div class="import-summary">
<div><strong>{{ report.inserted }}</strong><span>Insertados</span></div>
<div><strong>{{ report.existed }}</strong><span>Actualizados</span></div>
<div><strong>{{ report.errors }}</strong><span>Con error</span></div>
<div><strong>{{ report.photos }}</strong><span>Fotos</span></div>
</div>
<Panel v-if="report.existed_rows.length" header="Registros que ya existían" toggleable collapsed>
<DeskTableBar v-model="existedQ" placeholder="Buscar nombre o CURP">
<Chip :label="`${listedExisted.length} coincidencias`" icon="pi pi-users" />
</DeskTableBar>
<DataTable :value="listedExisted" size="small" class="mt-2">
<Column field="row" header="Fila" /><Column field="nombre" header="Nombre" /><Column field="curp" header="CURP" /><Column field="matched" header="Coincidencia" />
</DataTable>
</Panel>
<Panel v-if="report.error_rows.length" header="Errores de validación" toggleable>
<DeskTableBar v-model="errorQ" placeholder="Buscar nombre o CURP">
<Chip :label="`${listedErrors.length} errores`" icon="pi pi-exclamation-triangle" />
</DeskTableBar>
<DataTable :value="listedErrors" size="small" class="mt-2">
<Column field="row" header="Fila" /><Column field="nombre" header="Nombre" /><Column field="curp" header="CURP" />
<Column header="Detalle"><template #body="{ data }"><ul class="validation-list"><li v-for="message in data.messages" :key="message">{{ message }}</li></ul></template></Column>
</DataTable>
</Panel>
</template>
</div>
<template #footer><Button label="Cerrar" severity="secondary" text @click="visibleModel = false" /></template>
</Dialog>
</template>
<script setup lang="ts">
import type { ImportReport, ProjectOption } from "~/types/padron";
import FormField from "./FormField.vue";
const props = defineProps<{ visible: boolean; projects: ProjectOption[] }>();
const emit = defineEmits<{ "update:visible": [value: boolean]; imported: [] }>();
const visibleModel = computed({ get: () => props.visible, set: (value) => emit("update:visible", value) });
const { api, download } = useApi();
const toast = useToast();
const projectId = ref<number | null>(null);
const report = ref<ImportReport | null>(null);
const templateLoading = ref(false);
const importing = ref(false);
const existedQ = ref("");
const errorQ = ref("");
const listedExisted = computed(() => {
const rows = report.value?.existed_rows || [];
const q = existedQ.value.trim().toLowerCase();
if (!q) return rows;
return rows.filter((row) => [row.row, row.nombre, row.curp, row.matched].some((part) => String(part ?? "").toLowerCase().includes(q)));
});
const listedErrors = computed(() => {
const rows = report.value?.error_rows || [];
const q = errorQ.value.trim().toLowerCase();
if (!q) return rows;
return rows.filter((row) =>
[row.row, row.nombre, row.curp, ...(row.messages || [])].some((part) => String(part ?? "").toLowerCase().includes(q)),
);
});
async function downloadTemplate() {
templateLoading.value = true;
try {
await download("/v1/workers/import/template", "plantilla-padron-arctec.xlsx");
} catch (error) {
toast.add({ severity: "error", summary: error instanceof Error ? error.message : "No se pudo descargar", life: 3000 });
} finally {
templateLoading.value = false;
}
}
async function importFile(event: { files: File[] }) {
const file = event.files?.[0];
if (!file) return;
importing.value = true;
report.value = null;
try {
const body = new FormData();
body.append("file", file);
if (projectId.value) body.append("project_id", String(projectId.value));
report.value = await api<ImportReport>("/v1/workers/import", { method: "POST", body });
toast.add({
severity: report.value.errors ? "warn" : "success",
summary: `Insertados ${report.value.inserted}, actualizados ${report.value.existed}, errores ${report.value.errors}`,
life: 4000,
});
emit("imported");
} catch (error) {
toast.add({ severity: "error", summary: error instanceof Error ? error.message : "Falló la importación", life: 3500 });
} finally {
importing.value = false;
}
}
watch(() => props.visible, (visible) => {
if (visible) { report.value = null; projectId.value = null; }
});
</script>