mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 23:33:18 +00:00
303 lines
10 KiB
Vue
303 lines
10 KiB
Vue
<template>
|
|
<section class="padron-list">
|
|
<DataTable
|
|
v-model:selection="selection"
|
|
v-model:filters="filters"
|
|
:value="workers"
|
|
data-key="id"
|
|
class="padron-datatable"
|
|
paginator
|
|
:rows="25"
|
|
:rows-per-page-options="[25, 50, 100]"
|
|
removable-sort
|
|
scrollable
|
|
scroll-height="flex"
|
|
selection-mode="single"
|
|
:meta-key-selection="false"
|
|
:global-filter-fields="['full_name', 'position', 'curp', 'rfc', 'nss', 'proyectos', 'proyecto_pairs']"
|
|
context-menu
|
|
:context-menu-selection="selection"
|
|
empty-message="No hay personal que coincida con esta vista."
|
|
table-style="min-width: 52rem"
|
|
@row-click="onRow"
|
|
@row-dblclick="emit('edit', $event.data)"
|
|
@row-contextmenu="onContext"
|
|
>
|
|
<Column selection-mode="single" header-style="width: 2.75rem" />
|
|
<Column field="full_name" header="Persona" sortable>
|
|
<template #body="{ data }">
|
|
<div class="person-cell">
|
|
<Avatar :label="initials(data.first_name, data.last_name_p)" shape="circle" size="small" />
|
|
<div class="person-cell-copy"><div class="person-name">{{ data.full_name }}</div><div class="muted">{{ data.position }}</div></div>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
<Column field="risk_level" header="Riesgo" sortable header-style="width: 4.5rem" body-style="text-align: center">
|
|
<template #body="{ data }">
|
|
<Tag
|
|
:icon="riskIcon(data.risk_code)"
|
|
:severity="riskLevelSeverity(data.risk_code)"
|
|
rounded
|
|
class="status-icon-tag"
|
|
v-tooltip.top="riskLevelLabel(data.risk_code)"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
<Column field="hire_type" header="Empresa" sortable>
|
|
<template #body="{ data }">
|
|
<Tag
|
|
v-if="empresaLabel(data)"
|
|
:value="empresaLabel(data)"
|
|
:severity="hireSeverity(empresaLabel(data))"
|
|
/>
|
|
<span v-else class="muted">Sin patrón</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="imss_status" header="IMSS" sortable header-style="width: 5.5rem" body-style="text-align: center">
|
|
<template #body="{ data }">
|
|
<Tag
|
|
:icon="imssIcon(data)"
|
|
:severity="imssSeverity(data)"
|
|
rounded
|
|
class="status-icon-tag"
|
|
v-tooltip.top="imssTooltip(data)"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
<Column field="curp" header="CURP" sortable>
|
|
<template #body="{ data }"><span class="identifier">{{ data.curp }}</span></template>
|
|
</Column>
|
|
<Column field="nss" header="NSS" sortable>
|
|
<template #body="{ data }"><span class="identifier">{{ data.nss }}</span></template>
|
|
</Column>
|
|
<Column field="proyectos" header="Proyecto" sortable style="width: 8.5rem; min-width: 8.5rem">
|
|
<template #body="{ data }">
|
|
<div v-if="projectBadges(data).length" class="project-badges">
|
|
<Tag
|
|
v-for="item in projectBadges(data)"
|
|
:key="item.code"
|
|
:value="item.code"
|
|
severity="info"
|
|
class="project-code-tag"
|
|
v-tooltip.top="item.name"
|
|
/>
|
|
</div>
|
|
<span v-else class="muted">Sin asignar</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="pipeline_status" header="Expediente" sortable header-style="width: 5.5rem" body-style="text-align: center">
|
|
<template #body="{ data }">
|
|
<Tag
|
|
:icon="pipelineIcon(data.pipeline_status)"
|
|
:severity="pipelineSeverity(data.pipeline_status)"
|
|
rounded
|
|
class="status-icon-tag"
|
|
v-tooltip.top="pipelineLabel(data.pipeline_status)"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
<Column field="gafete_key" header="Gafete" sortable header-style="width: 5rem" body-style="text-align: center">
|
|
<template #body="{ data }">
|
|
<Tag
|
|
v-if="gafeteIcon(data.pipeline_status)"
|
|
:icon="gafeteIcon(data.pipeline_status)"
|
|
:severity="gafeteSeverity(data.pipeline_status)"
|
|
rounded
|
|
class="status-icon-tag"
|
|
v-tooltip.top="gafeteLabel(data.pipeline_status)"
|
|
/>
|
|
<span v-else class="muted">—</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="status" header="Estatus" sortable header-style="width: 5rem" body-style="text-align: center">
|
|
<template #body="{ data }">
|
|
<Tag
|
|
:icon="data.status === 'activo' ? 'pi pi-check' : 'pi pi-times'"
|
|
:severity="data.status === 'activo' ? 'success' : 'danger'"
|
|
rounded
|
|
class="status-icon-tag"
|
|
v-tooltip.top="data.status === 'activo' ? 'Activo' : 'Baja'"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
<ContextMenu ref="contextMenu" :model="contextItems" />
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { FilterMatchMode } from "@primevue/core/api";
|
|
import type { ProjectOption, Worker } from "~/types/padron";
|
|
import { projectIdsOf } from "~/types/padron";
|
|
|
|
const props = defineProps<{
|
|
workers: Worker[];
|
|
modelValue: Worker | null;
|
|
query?: string;
|
|
projects?: ProjectOption[];
|
|
}>();
|
|
const emit = defineEmits<{
|
|
"update:modelValue": [worker: Worker | null];
|
|
open: [worker: Worker];
|
|
edit: [worker: Worker];
|
|
baja: [worker: Worker];
|
|
reactivate: [worker: Worker];
|
|
}>();
|
|
const { initials, riskLevel, riskLevelLabel, riskLevelSeverity, hireSeverity, pipelineLabel, pipelineSeverity, gafeteLabel, gafeteSeverity } = useUi();
|
|
const selection = computed({ get: () => props.modelValue, set: (value) => emit("update:modelValue", value) });
|
|
const filters = ref({
|
|
global: { value: null as string | null, matchMode: FilterMatchMode.CONTAINS },
|
|
});
|
|
const contextMenu = ref();
|
|
const menuWorker = ref<Worker | null>(null);
|
|
const projectById = computed(() => {
|
|
const map = new Map<number, ProjectOption>();
|
|
for (const project of props.projects || []) map.set(project.id, project);
|
|
return map;
|
|
});
|
|
|
|
const contextItems = computed(() => [
|
|
{ label: "Abrir ficha", icon: "pi pi-folder-open", command: () => menuWorker.value && emit("open", menuWorker.value) },
|
|
{ label: "Editar datos", icon: "pi pi-pencil", command: () => menuWorker.value && emit("edit", menuWorker.value) },
|
|
{ separator: true },
|
|
{
|
|
label: "Dar de baja",
|
|
icon: "pi pi-user-minus",
|
|
disabled: menuWorker.value?.status === "baja",
|
|
command: () => menuWorker.value && emit("baja", menuWorker.value),
|
|
},
|
|
{
|
|
label: "Reactivar",
|
|
icon: "pi pi-user-plus",
|
|
disabled: menuWorker.value?.status !== "baja",
|
|
command: () => menuWorker.value && emit("reactivate", menuWorker.value),
|
|
},
|
|
]);
|
|
|
|
function imssIcon(worker: Worker) {
|
|
if (worker.imss_status === "alta") return "pi pi-verified";
|
|
if (worker.in_project_without_imss) return "pi pi-exclamation-triangle";
|
|
if (worker.imss_ready) return "pi pi-send";
|
|
return "pi pi-times-circle";
|
|
}
|
|
function imssSeverity(worker: Worker) {
|
|
if (worker.imss_status === "alta") return "success";
|
|
if (worker.in_project_without_imss) return "danger";
|
|
if (worker.imss_ready) return "info";
|
|
return "warn";
|
|
}
|
|
function imssTooltip(worker: Worker) {
|
|
if (worker.imss_status === "alta") {
|
|
const since = worker.imss_alta_at ? ` desde ${worker.imss_alta_at}` : "";
|
|
const patron = worker.imss_company_code || worker.imss_company_name || "";
|
|
return `Alta IMSS${since}${patron ? ` · ${patron}` : ""}`;
|
|
}
|
|
if (worker.in_project_without_imss) return "En obra sin alta IMSS";
|
|
if (worker.imss_ready) return "Expediente listo para alta IMSS";
|
|
return "Sin alta IMSS";
|
|
}
|
|
|
|
function empresaLabel(worker: Worker) {
|
|
if (worker.imss_status !== "alta") return "";
|
|
return worker.imss_company_code || worker.hire_type || worker.imss_company_name || worker.company_name || "";
|
|
}
|
|
|
|
function riskIcon(code: string) {
|
|
switch (riskLevel(code)) {
|
|
case "alto":
|
|
return "pi pi-exclamation-triangle";
|
|
case "medio":
|
|
return "pi pi-minus-circle";
|
|
default:
|
|
return "pi pi-check-circle";
|
|
}
|
|
}
|
|
|
|
function pipelineIcon(status: string) {
|
|
switch (status) {
|
|
case "incompleto":
|
|
return "pi pi-folder-open";
|
|
case "listo_gafete":
|
|
return "pi pi-check-circle";
|
|
case "impreso":
|
|
return "pi pi-id-card";
|
|
case "activo":
|
|
return "pi pi-briefcase";
|
|
case "baja":
|
|
return "pi pi-ban";
|
|
default:
|
|
return "pi pi-folder";
|
|
}
|
|
}
|
|
|
|
function gafeteIcon(status: string) {
|
|
switch (status) {
|
|
case "incompleto":
|
|
return "pi pi-times";
|
|
case "listo_gafete":
|
|
return "pi pi-print";
|
|
case "impreso":
|
|
case "activo":
|
|
return "pi pi-id-card";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function projectBadges(worker: Worker) {
|
|
const ids = projectIdsOf(worker);
|
|
if (ids.length) {
|
|
return ids.map((id) => {
|
|
const project = projectById.value.get(id);
|
|
return {
|
|
code: project?.code || `PRY-${String(id).padStart(4, "0")}`,
|
|
name: project?.name || worker.proyectos || `Proyecto ${id}`,
|
|
};
|
|
});
|
|
}
|
|
const raw = String(worker.proyecto_pairs || "").trim();
|
|
if (!raw) return [];
|
|
return raw.split(";;").map((part) => {
|
|
const sep = part.indexOf("|");
|
|
if (sep < 0) return { code: part, name: part };
|
|
return { code: part.slice(0, sep), name: part.slice(sep + 1) };
|
|
}).filter((item) => item.code && item.code !== item.name);
|
|
}
|
|
|
|
function onRow(event: { data: Worker }) { selection.value = event.data; emit("open", event.data); }
|
|
function onContext(event: { originalEvent: Event; data: Worker }) {
|
|
menuWorker.value = event.data; selection.value = event.data; emit("open", event.data); contextMenu.value.show(event.originalEvent);
|
|
}
|
|
watch(() => props.query, (value) => { filters.value.global.value = value || null; }, { immediate: true });
|
|
</script>
|
|
|
|
<style scoped>
|
|
.status-icon-tag {
|
|
width: 1.75rem;
|
|
height: 1.75rem;
|
|
padding: 0;
|
|
justify-content: center;
|
|
}
|
|
.status-icon-tag :deep(.p-tag-icon) {
|
|
margin: 0;
|
|
font-size: 0.8rem;
|
|
}
|
|
.status-icon-tag :deep(.p-tag-label) {
|
|
display: none;
|
|
}
|
|
.project-badges {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
align-items: center;
|
|
}
|
|
.project-code-tag {
|
|
font-family: "Cascadia Mono", "Segoe UI Mono", monospace;
|
|
letter-spacing: .02em;
|
|
white-space: nowrap;
|
|
max-width: none;
|
|
}
|
|
.project-code-tag :deep(.p-tag-label) {
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|