mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 20:43:17 +00:00
331 lines
14 KiB
Vue
331 lines
14 KiB
Vue
<template>
|
|
<div class="ficha worker-detail">
|
|
<div v-if="!workerId" class="detail-empty">
|
|
<i class="pi pi-file detail-empty-icon" />
|
|
<strong>Seleccione un registro</strong>
|
|
<span>La ficha aparecerá en este panel.</span>
|
|
</div>
|
|
<div v-else-if="loading" class="detail-loading"><ProgressSpinner /></div>
|
|
<div v-else-if="data" class="ficha-body worker-record">
|
|
<div class="worker-record-code">{{ recordCode }}</div>
|
|
|
|
<div class="worker-photo worker-record-photo">
|
|
<img v-if="photoOk" :src="photoSrc" :alt="title" @error="photoOk = false" />
|
|
<Avatar v-else :label="initials(data.worker.first_name, data.worker.last_name_p)" shape="circle" size="xlarge" />
|
|
</div>
|
|
|
|
<div class="worker-record-lead">
|
|
<div class="worker-detail-name">{{ title }}</div>
|
|
<div class="muted">{{ data.worker.position }}</div>
|
|
</div>
|
|
|
|
<div class="profile-list worker-record-fields">
|
|
<div v-for="row in recordRows" :key="row.label">
|
|
<span>{{ row.label }}</span>
|
|
<strong :class="{ identifier: row.identifier }">{{ row.value }}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="worker-record-status">
|
|
<span>Estatus</span>
|
|
<div class="ficha-tags">
|
|
<Tag :value="pipelineLabel(data.worker.pipeline_status)" :severity="pipelineSeverity(data.worker.pipeline_status)" />
|
|
<Tag :value="riskLevelLabel(data.worker.risk_code)" :severity="riskLevelSeverity(data.worker.risk_code)" />
|
|
<Tag v-if="empresaTag" :value="empresaTag" :severity="hireSeverity(empresaTag)" />
|
|
<Tag v-else value="Sin patrón" severity="secondary" />
|
|
<Tag :value="imssStatusLabel" :severity="imssStatusSeverity" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="worker-imss-block">
|
|
<div class="worker-imss-head">
|
|
<div class="worker-imss-title">IMSS</div>
|
|
<div class="worker-imss-actions">
|
|
<template v-if="canEdit">
|
|
<Button
|
|
v-if="data.worker.imss_status !== 'alta'"
|
|
label="Registrar alta"
|
|
icon="pi pi-file-pdf"
|
|
size="small"
|
|
@click="openImss('alta')"
|
|
/>
|
|
<template v-else>
|
|
<Button
|
|
label="Reemplazar alta"
|
|
icon="pi pi-upload"
|
|
size="small"
|
|
outlined
|
|
@click="openImss('alta')"
|
|
/>
|
|
<Button
|
|
label="Registrar baja"
|
|
icon="pi pi-times-circle"
|
|
size="small"
|
|
severity="warn"
|
|
outlined
|
|
@click="openImss('baja')"
|
|
/>
|
|
</template>
|
|
</template>
|
|
<Button
|
|
label="Historial"
|
|
icon="pi pi-history"
|
|
size="small"
|
|
text
|
|
@click="openImss('historial')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="profile-list worker-record-fields">
|
|
<div><span>Estado</span><strong>{{ imssStatusLabel }}</strong></div>
|
|
<div v-if="data.worker.imss_status === 'alta'">
|
|
<span>Alta desde</span>
|
|
<strong>{{ formatDay(data.worker.imss_alta_at) }}</strong>
|
|
</div>
|
|
<div v-if="data.worker.imss_status === 'baja_imss' && data.worker.imss_baja_at">
|
|
<span>Baja desde</span>
|
|
<strong>{{ formatDay(data.worker.imss_baja_at) }}</strong>
|
|
</div>
|
|
<div v-if="data.worker.imss_company_name || data.worker.imss_company_code">
|
|
<span>Patrón</span>
|
|
<strong>{{ data.worker.imss_company_name || data.worker.imss_company_code }}</strong>
|
|
</div>
|
|
<div v-if="data.worker.imss_registro_patronal">
|
|
<span>Registro patronal</span>
|
|
<strong class="identifier">{{ data.worker.imss_registro_patronal }}</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
v-if="!canEdit"
|
|
label="Ir a Padrón"
|
|
icon="pi pi-users"
|
|
outlined
|
|
class="w-full worker-record-edit"
|
|
@click="goPadron"
|
|
/>
|
|
|
|
<div class="worker-mini-stats">
|
|
<div><strong>{{ requiredDone }}/{{ requiredItems.length }}</strong><span>Documentos</span></div>
|
|
<div><strong>{{ activeAssignments }}</strong><span>Proyectos</span></div>
|
|
<div><strong>{{ money(loanBalance) }}</strong><span>Préstamos</span></div>
|
|
</div>
|
|
|
|
<Tabs v-model:value="tab" class="mt-3 detail-tabs">
|
|
<TabList>
|
|
<Tab value="docs">Expediente</Tab>
|
|
<Tab value="proyectos">Proyectos</Tab>
|
|
<Tab value="nomina">Nómina</Tab>
|
|
</TabList>
|
|
<TabPanels>
|
|
<TabPanel value="docs">
|
|
<WorkerDocuments
|
|
:worker-id="workerId"
|
|
:documents="data.documents"
|
|
:checklist="data.checklist"
|
|
:companies="companies"
|
|
:initial-type="doc"
|
|
:readonly="!canEdit"
|
|
:freshness-required="data.freshness_required || data.worker.freshness_required"
|
|
:imss-ready="data.worker.imss_ready"
|
|
:in-project-without-imss="data.worker.in_project_without_imss"
|
|
@changed="refreshRelated"
|
|
@register-alta="openImss('alta')"
|
|
/>
|
|
</TabPanel>
|
|
<TabPanel value="proyectos">
|
|
<WorkerAssignments
|
|
:worker-id="workerId"
|
|
:assignments="data.assignments"
|
|
:projects="projects"
|
|
:readonly="!canEdit"
|
|
@changed="refreshRelated"
|
|
/>
|
|
</TabPanel>
|
|
<TabPanel value="nomina">
|
|
<Message :closable="false" class="nomina-jornal-msg">Jornal {{ money(Number(data.worker.daily_wage)) }} · {{ data.worker.work_type === "D" ? "Destajo" : "Normal" }}</Message>
|
|
<Message severity="secondary" :closable="false" class="mt-2">
|
|
<template v-if="canEdit">Use <strong>Préstamo</strong> en la barra para registrar. Esta ficha no edita los datos de la persona.</template>
|
|
<template v-else>La ficha es solo consulta. El préstamo se registra aquí; para editar datos de la persona, abra Padrón.</template>
|
|
</Message>
|
|
<Button
|
|
label="Registrar préstamo"
|
|
icon="pi pi-wallet"
|
|
class="w-full mt-2"
|
|
@click="registerLoan"
|
|
/>
|
|
<DataTable :value="data.loans" size="small" class="mt-3" empty-message="Sin préstamos">
|
|
<Column header="Entregado"><template #body="{ data: row }">{{ money(Number(row.delivered || row.amount)) }}</template></Column>
|
|
<Column header="Saldo"><template #body="{ data: row }">{{ money(Number(row.balance)) }}</template></Column>
|
|
<Column header="Vence"><template #body="{ data: row }">{{ row.first_due || "—" }}</template></Column>
|
|
<Column header="">
|
|
<template #body="{ data: row }">
|
|
<Button icon="pi pi-file-pdf" text rounded size="small" aria-label="Recibo" @click="downloadLoan(row.id)" />
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
|
|
<AltaImssDialog
|
|
v-if="workerId"
|
|
v-model="showAltaImss"
|
|
:worker-id="workerId"
|
|
:companies="companies"
|
|
:documents="data.documents"
|
|
:default-company-id="data.worker.company_id"
|
|
:imss-status="data.worker.imss_status"
|
|
:initial-tab="imssDialogTab"
|
|
@saved="onImssSaved"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import WorkerDocuments from "~/components/padron/WorkerDocuments.vue";
|
|
import WorkerAssignments from "~/components/padron/WorkerAssignments.vue";
|
|
import AltaImssDialog from "~/components/padron/AltaImssDialog.vue";
|
|
import type { ChecklistItem, CompanyOption, ProjectOption, Worker } from "~/types/padron";
|
|
|
|
type DetailData = {
|
|
worker: Worker & { full_name: string };
|
|
documents: {
|
|
id: number;
|
|
type_code: string;
|
|
original_name: string;
|
|
uploaded_at: string;
|
|
is_current: number;
|
|
mime?: string;
|
|
issued_at?: string | null;
|
|
expires_at?: string | null;
|
|
imss_company_id?: number | null;
|
|
imss_alta_at?: string | null;
|
|
}[];
|
|
assignments: { id: number; project_id: number; project_name: string; project_code: string; active: number; start_date: string; end_date?: string | null }[];
|
|
loans: { id: number; amount: number; delivered?: number; balance: number; weekly_payment: number; first_due?: string | null }[];
|
|
checklist: ChecklistItem[];
|
|
freshness_required?: boolean;
|
|
};
|
|
|
|
const props = defineProps<{ workerId?: number; doc?: string; tab?: string }>();
|
|
const emit = defineEmits<{ edit: []; changed: [] }>();
|
|
const { pipelineLabel, pipelineSeverity, initials, riskLevelLabel, riskLevelSeverity, hireSeverity } = useUi();
|
|
const { api, download } = useApi();
|
|
const toast = useToast();
|
|
const desk = useDesktop();
|
|
const route = useRoute();
|
|
const canEdit = computed(() => route.path.startsWith("/padron"));
|
|
|
|
const data = ref<DetailData | null>(null);
|
|
const projects = ref<ProjectOption[]>([]);
|
|
const companies = ref<CompanyOption[]>([]);
|
|
const loading = ref(false);
|
|
const tab = ref("docs");
|
|
const photoOk = ref(true);
|
|
const showAltaImss = ref(false);
|
|
const imssDialogTab = ref<"alta" | "baja" | "historial">("alta");
|
|
const title = computed(() => data.value ? [data.value.worker.first_name, data.value.worker.middle_name, data.value.worker.last_name_p, data.value.worker.last_name_m].filter(Boolean).join(" ") : "Expediente");
|
|
const recordCode = computed(() => data.value ? `PER-${String(data.value.worker.id).padStart(4, "0")}` : "PER-0000");
|
|
const photoSrc = computed(() => `/v1/workers/${props.workerId}/photo?v=${desk.detailRev.value}`);
|
|
const requiredItems = computed(() => data.value?.checklist.filter((item) => item.required) || []);
|
|
const requiredDone = computed(() => requiredItems.value.filter((item) => item.present && item.valid !== false).length);
|
|
const activeAssignments = computed(() => data.value?.assignments.filter((assignment) => assignment.active).length || 0);
|
|
const activeProjectNames = computed(() => data.value?.assignments.filter((assignment) => assignment.active).map((assignment) => assignment.project_name).join(", ") || "Sin asignar");
|
|
const loanBalance = computed(() => data.value?.loans.reduce((sum, loan) => sum + Math.max(0, Number(loan.balance)), 0) || 0);
|
|
const imssStatusLabel = computed(() => {
|
|
const status = data.value?.worker.imss_status;
|
|
if (status === "alta") return "Alta IMSS";
|
|
if (status === "baja_imss") return "Baja IMSS";
|
|
return "Sin alta IMSS";
|
|
});
|
|
const imssStatusSeverity = computed(() => {
|
|
const status = data.value?.worker.imss_status;
|
|
if (status === "alta") return "success";
|
|
if (data.value?.worker.in_project_without_imss) return "danger";
|
|
return "warn";
|
|
});
|
|
const empresaTag = computed(() => {
|
|
const worker = data.value?.worker;
|
|
if (!worker || worker.imss_status !== "alta") return "";
|
|
return worker.imss_company_code || worker.imss_company_name || worker.company_name || worker.hire_type || "";
|
|
});
|
|
const recordRows = computed(() => {
|
|
const worker = data.value?.worker;
|
|
if (!worker) return [];
|
|
return [
|
|
{ label: "Empresa", value: empresaTag.value || "Sin patrón" },
|
|
{ label: "Proyecto", value: activeProjectNames.value },
|
|
{ label: "CURP", value: worker.curp, identifier: true },
|
|
{ label: "RFC", value: worker.rfc, identifier: true },
|
|
{ label: "NSS", value: worker.nss, identifier: true },
|
|
{ label: "Teléfono", value: worker.phone },
|
|
{ label: "Correo", value: worker.email },
|
|
{ label: "Dirección", value: worker.address },
|
|
{ label: "Tipo de sangre", value: worker.blood_type || "—" },
|
|
{ label: "Jornal", value: money(Number(worker.daily_wage)) },
|
|
];
|
|
});
|
|
|
|
function money(value: number) {
|
|
return value.toLocaleString("es-MX", { style: "currency", currency: "MXN", maximumFractionDigits: 0 });
|
|
}
|
|
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" });
|
|
}
|
|
async function load() {
|
|
if (!props.workerId) { data.value = null; return; }
|
|
loading.value = true;
|
|
try {
|
|
const [detail, projectResponse, companyResponse] = await Promise.all([
|
|
api<DetailData>(`/v1/workers/${props.workerId}`),
|
|
api<{ projects: ProjectOption[] }>("/v1/projects?status=activo"),
|
|
api<{ companies: CompanyOption[] }>("/v1/companies"),
|
|
]);
|
|
data.value = detail;
|
|
projects.value = projectResponse.projects;
|
|
companies.value = companyResponse.companies.filter((c) => c.status === "activo");
|
|
photoOk.value = true;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
function refreshRelated() {
|
|
desk.bumpDetail();
|
|
}
|
|
function openImss(tab: "alta" | "baja" | "historial") {
|
|
imssDialogTab.value = tab;
|
|
showAltaImss.value = true;
|
|
}
|
|
function onImssSaved(kind: "alta" | "baja") {
|
|
toast.add({
|
|
severity: "success",
|
|
summary: kind === "baja" ? "Baja IMSS registrada" : "Alta IMSS registrada",
|
|
life: 2200,
|
|
});
|
|
refreshRelated();
|
|
}
|
|
function goPadron() {
|
|
if (!props.workerId) return;
|
|
navigateTo(`/padron?worker=${props.workerId}`);
|
|
}
|
|
function registerLoan() {
|
|
if (!props.workerId) return;
|
|
desk.openLoan(props.workerId, title.value);
|
|
}
|
|
function downloadLoan(id: number) {
|
|
download(`/v1/loans/${id}/recibo`, `prestamo-aceptacion-${id}.pdf`);
|
|
}
|
|
|
|
watch(() => [props.workerId, props.tab] as const, ([, fichaTab]) => {
|
|
tab.value = fichaTab || "docs";
|
|
});
|
|
watch(() => [props.workerId, desk.detailRev.value] as const, () => {
|
|
load();
|
|
}, { immediate: true });
|
|
</script>
|