mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 17:43:19 +00:00
382 lines
12 KiB
Vue
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>
|