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

382 lines
12 KiB
Vue

<template>
<Dialog
v-model:visible="visible"
modal
:header="dialogTitle"
class="imss-mov-dialog"
:style="{ width: 'min(480px, 96vw)' }"
@hide="reset"
>
<Tabs v-model:value="tab" class="imss-mov-tabs">
<TabList>
<Tab value="alta">Alta</Tab>
<Tab value="baja" :disabled="!canRegisterBaja">Baja</Tab>
<Tab value="historial">Historial</Tab>
</TabList>
<TabPanels>
<TabPanel value="alta">
<p class="muted imss-mov-intro">
Adjunta el PDF (o imagen) del movimiento de alta, la empresa patrón y la fecha.
</p>
<div class="imss-mov-form">
<FormField label="Empresa patrón">
<Select
v-model="imssCompanyId"
:options="companies"
:option-label="companyOptionLabel"
option-value="id"
placeholder="Seleccione empresa patrón"
class="w-full"
/>
</FormField>
<FormField label="Fecha de alta">
<DatePicker v-model="imssAltaAt" date-format="dd/mm/yy" show-icon class="w-full" />
</FormField>
<FormField label="Archivo (PDF o imagen)">
<FileUpload
mode="basic"
:auto="false"
choose-label="Seleccionar PDF"
accept=".pdf,.jpg,.jpeg,.png,.webp"
custom-upload
:disabled="uploading"
class="imss-file-upload"
@select="onPick"
/>
<div class="imss-file-name muted">{{ file ? file.name : "Ningún archivo seleccionado" }}</div>
</FormField>
<Message v-if="error && tab === 'alta'" severity="error" :closable="false">{{ error }}</Message>
</div>
</TabPanel>
<TabPanel value="baja">
<p class="muted imss-mov-intro">
Registra la baja IMSS con la fecha del movimiento y el PDF o imagen de respaldo.
</p>
<Message v-if="!canRegisterBaja" severity="warn" :closable="false" class="mb-3">
Solo disponible cuando el trabajador tiene alta IMSS vigente.
</Message>
<div v-else class="imss-mov-form">
<FormField label="Fecha de baja">
<DatePicker v-model="imssBajaAt" date-format="dd/mm/yy" show-icon class="w-full" />
</FormField>
<FormField label="Archivo (PDF o imagen)">
<FileUpload
mode="basic"
:auto="false"
choose-label="Seleccionar PDF"
accept=".pdf,.jpg,.jpeg,.png,.webp"
custom-upload
:disabled="uploading"
class="imss-file-upload"
@select="onPick"
/>
<div class="imss-file-name muted">{{ file ? file.name : "Ningún archivo seleccionado" }}</div>
</FormField>
<Message v-if="error && tab === 'baja'" severity="error" :closable="false">{{ error }}</Message>
</div>
</TabPanel>
<TabPanel value="historial">
<p class="muted imss-mov-intro">Movimientos de alta y baja registrados en el expediente.</p>
<div v-if="!history.length" class="imss-history-empty muted">Sin movimientos IMSS aún.</div>
<ul v-else class="imss-history-list">
<li v-for="row in history" :key="row.id" class="imss-history-item">
<div class="imss-history-main">
<Tag
:value="row.type_code === 'baja_imss' ? 'Baja' : 'Alta'"
:severity="row.type_code === 'baja_imss' ? 'warn' : 'success'"
/>
<div>
<strong>{{ formatDay(row.imss_alta_at || row.issued_at || row.uploaded_at) }}</strong>
<div class="muted">{{ row.original_name }}</div>
<div v-if="companyName(row.imss_company_id)" class="muted">
Patrón: {{ companyName(row.imss_company_id) }}
</div>
</div>
</div>
<Button
icon="pi pi-eye"
text
rounded
severity="secondary"
aria-label="Ver documento"
@click="preview(row)"
/>
</li>
</ul>
</TabPanel>
</TabPanels>
</Tabs>
<template #footer>
<Button label="Cerrar" severity="secondary" text :disabled="uploading" @click="visible = false" />
<Button
v-if="tab === 'alta'"
label="Guardar alta"
icon="pi pi-check"
:loading="uploading"
:disabled="!canSaveAlta"
@click="submitAlta"
/>
<Button
v-else-if="tab === 'baja' && canRegisterBaja"
label="Guardar baja"
icon="pi pi-check"
severity="warn"
:loading="uploading"
:disabled="!canSaveBaja"
@click="submitBaja"
/>
</template>
</Dialog>
<DocumentPreviewDialog
v-model="previewOpen"
:path="previewPath"
:name="previewName"
:mime="previewMime"
/>
</template>
<script setup lang="ts">
import FormField from "~/components/padron/FormField.vue";
import DocumentPreviewDialog from "~/components/DocumentPreviewDialog.vue";
import type { CompanyOption } from "~/types/padron";
import { companyLabel } from "~/types/padron";
type ImssDoc = {
id: number;
type_code: string;
original_name: string;
uploaded_at: string;
mime?: string;
issued_at?: string | null;
imss_company_id?: number | null;
imss_alta_at?: string | null;
};
const props = defineProps<{
modelValue: boolean;
workerId: number;
companies: CompanyOption[];
documents?: ImssDoc[];
defaultCompanyId?: number | null;
imssStatus?: string | null;
initialTab?: "alta" | "baja" | "historial";
}>();
const emit = defineEmits<{ "update:modelValue": [value: boolean]; saved: [kind: "alta" | "baja"] }>();
const { api } = useApi();
const visible = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
const tab = ref<"alta" | "baja" | "historial">("alta");
const imssCompanyId = ref<number | null>(null);
const imssAltaAt = ref<Date | null>(new Date());
const imssBajaAt = ref<Date | null>(new Date());
const file = ref<File | null>(null);
const uploading = ref(false);
const error = ref("");
const previewOpen = ref(false);
const previewPath = ref("");
const previewName = ref("");
const previewMime = ref<string | null>(null);
const canRegisterBaja = computed(() => props.imssStatus === "alta");
const dialogTitle = computed(() => {
if (tab.value === "baja") return "Registrar baja IMSS";
if (tab.value === "historial") return "Historial IMSS";
return "Registrar alta IMSS";
});
const history = computed(() =>
(props.documents || [])
.filter((doc) => doc.type_code === "alta_imss" || doc.type_code === "baja_imss")
.slice()
.sort((a, b) => String(b.uploaded_at).localeCompare(String(a.uploaded_at))),
);
const canSaveAlta = computed(() =>
Boolean(imssCompanyId.value && imssAltaAt.value && file.value && !uploading.value),
);
const canSaveBaja = computed(() =>
Boolean(canRegisterBaja.value && imssBajaAt.value && file.value && !uploading.value),
);
function companyOptionLabel(company: CompanyOption) {
const base = companyLabel(company);
return company.registro_patronal ? `${base} · RP ${company.registro_patronal}` : base;
}
function companyName(id?: number | null) {
if (!id) return "";
const company = props.companies.find((item) => item.id === id);
return company ? companyLabel(company) : "";
}
function toIsoDate(value: Date) {
const y = value.getFullYear();
const m = String(value.getMonth() + 1).padStart(2, "0");
const d = String(value.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
function formatDay(value?: string | null) {
if (!value) return "—";
const date = new Date(`${String(value).slice(0, 10)}T12:00:00`);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString("es-MX", { day: "2-digit", month: "short", year: "numeric" });
}
function onPick(event: { files: File[] }) {
file.value = event.files?.[0] || null;
error.value = "";
}
function reset() {
file.value = null;
error.value = "";
uploading.value = false;
imssAltaAt.value = new Date();
imssBajaAt.value = new Date();
imssCompanyId.value = props.defaultCompanyId || props.companies[0]?.id || null;
tab.value = props.initialTab || "alta";
}
function preview(row: ImssDoc) {
previewPath.value = `/v1/workers/${props.workerId}/documents/${row.id}`;
previewName.value = row.original_name;
previewMime.value = row.mime || null;
previewOpen.value = true;
}
async function submitAlta() {
if (!canSaveAlta.value || !file.value || !imssCompanyId.value || !imssAltaAt.value) return;
uploading.value = true;
error.value = "";
try {
const body = new FormData();
body.append("type", "alta_imss");
body.append("file", file.value);
body.append("imss_company_id", String(imssCompanyId.value));
body.append("imss_alta_at", toIsoDate(imssAltaAt.value));
await api(`/v1/workers/${props.workerId}/documents`, { method: "POST", body });
visible.value = false;
emit("saved", "alta");
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "No se pudo guardar el alta";
} finally {
uploading.value = false;
}
}
async function submitBaja() {
if (!canSaveBaja.value || !file.value || !imssBajaAt.value) return;
uploading.value = true;
error.value = "";
try {
const body = new FormData();
body.append("type", "baja_imss");
body.append("file", file.value);
body.append("imss_baja_at", toIsoDate(imssBajaAt.value));
await api(`/v1/workers/${props.workerId}/documents`, { method: "POST", body });
visible.value = false;
emit("saved", "baja");
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "No se pudo guardar la baja";
} finally {
uploading.value = false;
}
}
watch(
() => props.modelValue,
(open) => {
if (open) {
reset();
if (props.initialTab) tab.value = props.initialTab;
}
},
);
watch(tab, () => {
file.value = null;
error.value = "";
});
</script>
<style scoped>
.imss-mov-intro {
margin: 0 0 16px;
font-size: 12px;
line-height: 1.45;
}
.imss-mov-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.imss-mov-form :deep(.p-select),
.imss-mov-form :deep(.p-datepicker),
.imss-mov-form :deep(.p-inputtext) {
width: 100%;
}
.imss-mov-form :deep(.p-datepicker) {
display: flex;
}
.imss-file-upload :deep(.p-fileupload-basic),
.imss-file-upload :deep(.p-fileupload) {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
width: 100%;
}
.imss-file-upload :deep(.p-button) {
width: 100%;
justify-content: center;
}
.imss-file-name {
margin-top: 6px;
font-size: 12px;
word-break: break-all;
}
.imss-history-empty {
padding: 18px 4px;
text-align: center;
}
.imss-history-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
max-height: min(360px, 50vh);
overflow: auto;
}
.imss-history-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border: 1px solid #e4e9f0;
border-radius: 10px;
background: #fafbfd;
}
.imss-history-main {
display: flex;
align-items: flex-start;
gap: 10px;
min-width: 0;
}
.imss-history-main strong {
font-size: 13px;
}
.imss-mov-tabs :deep(.p-tabpanels) {
padding: 14px 0 0;
}
</style>