mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 20:23:17 +00:00
El toast de error salía vacío porque el preview 400 no traía message. Ahora solo se aceptan .xlsx/.xls, y si el Excel no tiene el formato de presupuesto se explica qué columnas o plantilla usar. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
601 lines
22 KiB
Vue
601 lines
22 KiB
Vue
<template>
|
|
<div class="list-page">
|
|
<DeskTableBar v-model="tableQ" placeholder="Buscar código, obra, empresa o domicilio">
|
|
<Chip :label="`${listed.length} proyectos`" icon="pi pi-building" />
|
|
<Chip :label="`${activeCount} activos`" icon="pi pi-play" />
|
|
<Chip :label="`${pausedCount} pausados`" icon="pi pi-pause" />
|
|
<Chip :label="`${pendingDocsCount} con expediente pendiente`" icon="pi pi-folder-open" />
|
|
</DeskTableBar>
|
|
<DataTable
|
|
v-model:selection="selected"
|
|
:value="listed"
|
|
data-key="id"
|
|
paginator
|
|
:rows="15"
|
|
:rows-per-page-options="[15, 25, 50, 100]"
|
|
empty-message="Sin proyectos"
|
|
selection-mode="single"
|
|
meta-key-selection="false"
|
|
scrollable
|
|
scroll-height="flex"
|
|
context-menu
|
|
:context-menu-selection="selected"
|
|
@row-click="onRow"
|
|
@row-contextmenu="onCtx"
|
|
@row-dblclick="(e: { data: Project }) => openEdit(e.data)"
|
|
>
|
|
<Column field="code" header="Código" sortable>
|
|
<template #body="{ data }">
|
|
<Tag :value="String(data.code)" />
|
|
</template>
|
|
</Column>
|
|
<Column field="name" header="Proyecto" sortable />
|
|
<Column field="company_name" header="Empresa" sortable>
|
|
<template #body="{ data }">{{ data.company_name || data.company_code || "—" }}</template>
|
|
</Column>
|
|
<Column field="address" header="Domicilio" />
|
|
<Column header="Estado" sortable field="status">
|
|
<template #body="{ data }">
|
|
<Tag :value="projectStatusLabel(String(data.status))" :severity="projectStatusSeverity(String(data.status))" />
|
|
</template>
|
|
</Column>
|
|
<Column header="Expediente">
|
|
<template #body="{ data }">
|
|
<Tag :value="Number(data.missing_docs) > 0 ? 'Falta expediente' : 'Completo'" :severity="Number(data.missing_docs) > 0 ? 'warn' : 'success'" />
|
|
</template>
|
|
</Column>
|
|
<Column header="Activos">
|
|
<template #body="{ data }">
|
|
<Badge :value="data.active_count ?? 0" />
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
<ContextMenu ref="cm" :model="ctxItems" />
|
|
<Dialog v-model:visible="show" :header="editId ? 'Editar proyecto' : 'Nuevo proyecto'" modal :style="{ width: '760px' }">
|
|
<Message v-if="editId" severity="secondary" :closable="false" class="mb-3">
|
|
Código {{ editCode }} · id {{ editId }}. No se modifican.
|
|
</Message>
|
|
<Message v-else severity="secondary" :closable="false" class="mb-3">
|
|
El código (PRY-0001, PRY-0002…) se genera solo. Asigne la empresa que opera esta obra.
|
|
</Message>
|
|
<Fluid>
|
|
<div class="form-grid form-grid-compact">
|
|
<FormField label="Empresa" required>
|
|
<Select v-model="companyId" :options="companyChoices" option-label="label" option-value="value" placeholder="Seleccione empresa" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Nombre del proyecto" required>
|
|
<InputText v-model="name" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Domicilio" class="span-2">
|
|
<InputText v-model="address" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Monto de contrato">
|
|
<InputNumber
|
|
v-model="contractAmount"
|
|
mode="currency"
|
|
currency="MXN"
|
|
locale="es-MX"
|
|
:min="0"
|
|
class="w-full"
|
|
placeholder="Opcional"
|
|
/>
|
|
<div class="muted mt-1">
|
|
Opcional al dar de alta. Si adjunta Excel de presupuesto, se completa con el subtotal.
|
|
<span v-if="budgetHint"> · {{ budgetHint }}</span>
|
|
</div>
|
|
</FormField>
|
|
<FormField label="4% sobre nómina">
|
|
<InputNumber v-model="payrollTaxPct" suffix=" %" :min="0" :max="100" :min-fraction-digits="0" :max-fraction-digits="2" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Fecha de inicio">
|
|
<DatePicker v-model="startDate" date-format="dd/mm/yy" show-icon class="w-full" />
|
|
</FormField>
|
|
<FormField label="Fecha de término">
|
|
<DatePicker v-model="endDate" date-format="dd/mm/yy" show-icon class="w-full" />
|
|
</FormField>
|
|
<FormField label="Residente de obra">
|
|
<InputText v-model="residentName" class="w-full" />
|
|
</FormField>
|
|
<FormField label="SIROC">
|
|
<InputText v-model="siroc" class="w-full identifier-input" />
|
|
</FormField>
|
|
<FormField label="Estado">
|
|
<Select v-model="status" :options="statusOpts" option-label="label" option-value="code" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Tema de gafete">
|
|
<Select v-model="theme" :options="themes" option-label="name" option-value="id" class="w-full" />
|
|
</FormField>
|
|
<FormField label="Contrato" :class="{ 'span-2': !editId }">
|
|
<div>
|
|
<FileUpload mode="basic" :auto="false" choose-label="Adjuntar contrato" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx" custom-upload @select="onPickContrato" />
|
|
<div v-if="contratoFile" class="muted mt-1">{{ contratoFile.name }}</div>
|
|
</div>
|
|
</FormField>
|
|
<FormField label="Presupuesto" :class="{ 'span-2': !editId }">
|
|
<div>
|
|
<FileUpload
|
|
mode="basic"
|
|
:auto="false"
|
|
choose-label="Adjuntar presupuesto"
|
|
accept=".pdf,.xlsx,.xls,.jpg,.jpeg,.png"
|
|
custom-upload
|
|
:disabled="readingBudget"
|
|
@select="onPickPresupuesto"
|
|
/>
|
|
<div v-if="readingBudget" class="muted mt-1">Leyendo Excel…</div>
|
|
<div v-else-if="presupuestoFile" class="muted mt-1">{{ presupuestoFile.name }}</div>
|
|
<div v-else class="muted mt-1">PDF para el expediente, o Excel (.xlsx / .xls) para cargar partidas y el monto.</div>
|
|
</div>
|
|
</FormField>
|
|
</div>
|
|
</Fluid>
|
|
<Message v-if="formError" severity="error" class="mt-3" :closable="false">{{ formError }}</Message>
|
|
<template #footer>
|
|
<Button label="Cancelar" text @click="show = false" />
|
|
<Button :label="editId ? 'Guardar' : 'Crear'" icon="pi pi-check" @click="save" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import FormField from "~/components/padron/FormField.vue";
|
|
import type { CompanyOption } from "~/types/padron";
|
|
import { companyLabel } from "~/types/padron";
|
|
import { displayErrorMessage } from "~/composables/api-response";
|
|
import { isExcelSpreadsheet } from "~/utils/excelFile";
|
|
|
|
type Project = Record<string, unknown> & {
|
|
id: number;
|
|
status: string;
|
|
name: string;
|
|
code: string;
|
|
address?: string;
|
|
theme_id?: string;
|
|
company_id?: number | null;
|
|
company_name?: string | null;
|
|
company_code?: string | null;
|
|
contract_amount?: number | null;
|
|
start_date?: string | null;
|
|
end_date?: string | null;
|
|
resident_name?: string | null;
|
|
siroc?: string | null;
|
|
payroll_tax_pct?: number | null;
|
|
active_count?: number;
|
|
missing_docs?: number;
|
|
};
|
|
const { projectStatusLabel, projectStatusSeverity } = useUi();
|
|
const desk = useDesktop();
|
|
const { api } = useApi();
|
|
const toast = useToast();
|
|
const confirm = useConfirm();
|
|
const projects = ref<Project[]>([]);
|
|
const companies = ref<CompanyOption[]>([]);
|
|
const themes = ref<{ id: string; name: string }[]>([]);
|
|
const statusOpts = ref<{ code: string; label: string }[]>([]);
|
|
const show = ref(false);
|
|
const editId = ref(0);
|
|
const editCode = ref("");
|
|
const name = ref("");
|
|
const address = ref("");
|
|
const status = ref("activo");
|
|
const theme = ref("arctec-dos-logos-fold");
|
|
const companyId = ref<number | null>(null);
|
|
const contractAmount = ref<number | null>(null);
|
|
const startDate = ref<Date | null>(null);
|
|
const endDate = ref<Date | null>(null);
|
|
const residentName = ref("");
|
|
const siroc = ref("");
|
|
const payrollTaxPct = ref(4);
|
|
const contratoFile = ref<File | null>(null);
|
|
const presupuestoFile = ref<File | null>(null);
|
|
const readingBudget = ref(false);
|
|
const budgetHint = ref("");
|
|
const formError = ref("");
|
|
const selected = ref<Project | null>(null);
|
|
const { tableQ, matches } = useTableSearch();
|
|
const cm = ref();
|
|
const companyChoices = computed(() =>
|
|
companies.value
|
|
.filter((company) => company.status !== "inactivo" || company.id === companyId.value)
|
|
.map((company) => ({
|
|
label: company.kind === "principal" ? `${companyLabel(company)} · principal` : companyLabel(company),
|
|
value: company.id,
|
|
})),
|
|
);
|
|
const visible = computed(() => {
|
|
const key = desk.treeKey.value || "all";
|
|
if (key.startsWith("c:")) {
|
|
const id = Number(key.slice(2));
|
|
return projects.value.filter((project) => Number(project.company_id) === id);
|
|
}
|
|
return key === "all" ? projects.value : projects.value.filter((project) => project.status === key);
|
|
});
|
|
const listed = computed(() =>
|
|
visible.value.filter((project) =>
|
|
matches(
|
|
project.code,
|
|
project.name,
|
|
project.company_name,
|
|
project.company_code,
|
|
project.address,
|
|
project.status,
|
|
project.resident_name,
|
|
project.siroc,
|
|
),
|
|
),
|
|
);
|
|
const activeCount = computed(() => listed.value.filter((project) => project.status === "activo").length);
|
|
const pausedCount = computed(() => listed.value.filter((project) => project.status === "pausado").length);
|
|
const pendingDocsCount = computed(() => listed.value.filter((project) => Number(project.missing_docs) > 0).length);
|
|
|
|
const ctxItems = computed(() => [
|
|
{ label: "Editar", icon: "pi pi-pencil", command: () => selected.value && openEdit(selected.value) },
|
|
{ separator: true },
|
|
{ label: "Activar", icon: "pi pi-play", command: () => selected.value && setStatus(selected.value, "activo") },
|
|
{ label: "Pausar", icon: "pi pi-pause", command: () => selected.value && setStatus(selected.value, "pausado") },
|
|
{ label: "Concluir", icon: "pi pi-check", command: () => selected.value && setStatus(selected.value, "concluido") },
|
|
{ label: "Cancelar proyecto", icon: "pi pi-times", command: () => selected.value && setStatus(selected.value, "cancelado") },
|
|
]);
|
|
|
|
function onRow(e: { data: Project }) {
|
|
selected.value = e.data;
|
|
desk.openProject(e.data);
|
|
}
|
|
|
|
function onCtx(e: { originalEvent: Event; data: Project }) {
|
|
selected.value = e.data;
|
|
desk.openProject(e.data);
|
|
cm.value.show(e.originalEvent);
|
|
}
|
|
|
|
function parseDate(value: unknown): Date | null {
|
|
const raw = String(value || "").slice(0, 10);
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return null;
|
|
const [year, month, day] = raw.split("-").map(Number);
|
|
return new Date(year, month - 1, day);
|
|
}
|
|
|
|
function isoDate(value: Date | null) {
|
|
if (!value) return null;
|
|
const year = value.getFullYear();
|
|
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
const day = String(value.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function fillForm(row?: Project) {
|
|
name.value = row?.name ?? "";
|
|
address.value = String(row?.address ?? "");
|
|
status.value = row?.status ?? "activo";
|
|
theme.value = String(row?.theme_id ?? "arctec-dos-logos-fold");
|
|
const principal = companies.value.find((company) => company.kind === "principal");
|
|
companyId.value = row?.company_id ? Number(row.company_id) : principal?.id ?? null;
|
|
contractAmount.value = row?.contract_amount == null ? null : Number(row.contract_amount);
|
|
startDate.value = parseDate(row?.start_date);
|
|
endDate.value = parseDate(row?.end_date);
|
|
residentName.value = String(row?.resident_name ?? "");
|
|
siroc.value = String(row?.siroc ?? "");
|
|
payrollTaxPct.value = row?.payroll_tax_pct == null ? 4 : Number(row.payroll_tax_pct);
|
|
editId.value = row?.id ?? 0;
|
|
editCode.value = row?.code ?? "";
|
|
formError.value = "";
|
|
contratoFile.value = null;
|
|
presupuestoFile.value = null;
|
|
budgetHint.value = "";
|
|
readingBudget.value = false;
|
|
}
|
|
|
|
function onPickContrato(event: { files: File[] }) {
|
|
contratoFile.value = event.files?.[0] || null;
|
|
}
|
|
|
|
function isExcelFile(file: File) {
|
|
return isExcelSpreadsheet(file);
|
|
}
|
|
|
|
function moneyMx(value: number) {
|
|
return value.toLocaleString("es-MX", { style: "currency", currency: "MXN" });
|
|
}
|
|
|
|
async function onPickPresupuesto(event: { files: File[] }) {
|
|
const file = event.files?.[0] || null;
|
|
presupuestoFile.value = file;
|
|
budgetHint.value = "";
|
|
if (!file) return;
|
|
if (!isExcelFile(file)) return;
|
|
readingBudget.value = true;
|
|
try {
|
|
const body = new FormData();
|
|
body.append("file", file);
|
|
const preview = await api<{
|
|
totals: { subtotal: number; iva: number; total: number; item_count: number };
|
|
format_label?: string;
|
|
errors?: { messages: string[] }[];
|
|
}>("/v1/budget/preview", { method: "POST", body });
|
|
const subtotal = Number(preview.totals?.subtotal || 0);
|
|
if (subtotal > 0) {
|
|
contractAmount.value = subtotal;
|
|
budgetHint.value = `${preview.totals.item_count} partidas · ${moneyMx(subtotal)} (sin IVA)`;
|
|
toast.add({
|
|
severity: "success",
|
|
summary: "Monto de contrato actualizado",
|
|
detail: `Subtotal del presupuesto: ${moneyMx(subtotal)}`,
|
|
life: 2800,
|
|
});
|
|
} else {
|
|
const message = preview.errors?.[0]?.messages?.[0]
|
|
|| "El Excel no tiene el formato de presupuesto esperado. Use la plantilla del panel u Opus/Neodata.";
|
|
toast.add({
|
|
severity: "warn",
|
|
summary: "Excel sin partidas",
|
|
detail: message,
|
|
life: 4500,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
const detail = error as { data?: unknown };
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "No se pudo leer el Excel",
|
|
detail: displayErrorMessage(detail.data ?? error, error instanceof Error ? error.message : "Revise que sea .xlsx o .xls con el formato de presupuesto"),
|
|
life: 4500,
|
|
});
|
|
} finally {
|
|
readingBudget.value = false;
|
|
}
|
|
}
|
|
|
|
async function uploadDocs(projectId: number) {
|
|
const jobs: [string, File | null][] = [
|
|
["contrato", contratoFile.value],
|
|
["presupuesto", presupuestoFile.value],
|
|
];
|
|
for (const [type, file] of jobs) {
|
|
if (!file) continue;
|
|
const body = new FormData();
|
|
body.append("type", type);
|
|
body.append("file", file);
|
|
await api(`/v1/projects/${projectId}/documents`, { method: "POST", body });
|
|
}
|
|
if (presupuestoFile.value && isExcelFile(presupuestoFile.value)) {
|
|
try {
|
|
const body = new FormData();
|
|
body.append("file", presupuestoFile.value);
|
|
const report = await api<{
|
|
inserted: number;
|
|
contract_amount?: number;
|
|
}>(`/v1/projects/${projectId}/budget/import`, { method: "POST", body });
|
|
if (report.contract_amount != null) {
|
|
contractAmount.value = Number(report.contract_amount);
|
|
}
|
|
if (report.inserted) {
|
|
toast.add({
|
|
severity: "info",
|
|
summary: "Presupuesto cargado",
|
|
detail: `${report.inserted} partidas · monto ${moneyMx(Number(report.contract_amount || contractAmount.value || 0))}`,
|
|
life: 3200,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
const detail = error as { data?: unknown };
|
|
toast.add({
|
|
severity: "warn",
|
|
summary: "Documento guardado, pero no se importaron partidas",
|
|
detail: displayErrorMessage(detail.data ?? error, error instanceof Error ? error.message : "Revise el formato del Excel"),
|
|
life: 4500,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function openNew() {
|
|
selected.value = null;
|
|
fillForm();
|
|
show.value = true;
|
|
}
|
|
|
|
function openEdit(row: Project) {
|
|
selected.value = row;
|
|
fillForm(row);
|
|
show.value = true;
|
|
}
|
|
|
|
function payloadOf(extra: Record<string, unknown> = {}) {
|
|
return {
|
|
name: name.value,
|
|
address: address.value,
|
|
theme_id: theme.value,
|
|
status: status.value,
|
|
company_id: companyId.value,
|
|
contract_amount: contractAmount.value,
|
|
start_date: isoDate(startDate.value),
|
|
end_date: isoDate(endDate.value),
|
|
resident_name: residentName.value,
|
|
siroc: siroc.value,
|
|
payroll_tax_pct: payrollTaxPct.value,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
async function load() {
|
|
const [p, c] = await Promise.all([
|
|
api<{ projects: Project[] }>("/v1/projects"),
|
|
api<{
|
|
themes: { id: string; name: string }[];
|
|
project_statuses: { code: string; label: string }[];
|
|
companies: CompanyOption[];
|
|
}>("/v1/catalogs"),
|
|
]);
|
|
projects.value = p.projects;
|
|
themes.value = c.themes;
|
|
statusOpts.value = c.project_statuses;
|
|
companies.value = c.companies || [];
|
|
syncChrome();
|
|
}
|
|
|
|
function syncChrome() {
|
|
const counts = (st: string) => projects.value.filter((p) => p.status === st).length;
|
|
const messages = [];
|
|
if (pendingDocsCount.value > 0) {
|
|
messages.push({
|
|
key: "proy_docs",
|
|
severity: "warn" as const,
|
|
title: `${pendingDocsCount.value} proyectos con expediente pendiente`,
|
|
detail: "Faltan documentos de obra. Completa el expediente del proyecto.",
|
|
route: "/proyectos",
|
|
treeKey: "activo",
|
|
});
|
|
}
|
|
if (pausedCount.value > 0) {
|
|
messages.push({
|
|
key: "proy_pausados",
|
|
severity: "info" as const,
|
|
title: `${pausedCount.value} proyectos pausados`,
|
|
detail: "Obras en pausa; revisa si deben reactivarse o concluirse.",
|
|
route: "/proyectos",
|
|
treeKey: "pausado",
|
|
});
|
|
}
|
|
desk.setStatusMessages(messages);
|
|
desk.configure({
|
|
tab: "Proyectos",
|
|
treeTitle: "Proyectos",
|
|
showTree: true,
|
|
showDetail: true,
|
|
treeKey: desk.treeKey.value || "all",
|
|
countLabel: `Total de registros: ${visible.value.length}`,
|
|
tree: [
|
|
{ key: "all", label: "Todos", icon: "pi pi-building", data: { count: projects.value.length } },
|
|
{
|
|
key: "statuses",
|
|
label: "Estatus",
|
|
icon: "pi pi-list",
|
|
selectable: false,
|
|
children: [
|
|
{ key: "activo", label: "Activo", icon: "pi pi-play", data: { count: counts("activo") } },
|
|
{ key: "pausado", label: "Pausado", icon: "pi pi-pause", data: { count: counts("pausado") } },
|
|
{ key: "concluido", label: "Concluido", icon: "pi pi-check", data: { count: counts("concluido") } },
|
|
{ key: "cancelado", label: "Cancelado", icon: "pi pi-times", data: { count: counts("cancelado") } },
|
|
],
|
|
},
|
|
{
|
|
key: "companies",
|
|
label: "Empresas",
|
|
icon: "pi pi-briefcase",
|
|
selectable: false,
|
|
children: companies.value.map((company) => ({
|
|
key: `c:${company.id}`,
|
|
label: company.name || company.code,
|
|
icon: "pi pi-folder",
|
|
data: { count: projects.value.filter((project) => Number(project.company_id) === company.id).length },
|
|
})),
|
|
},
|
|
],
|
|
ribbon: [
|
|
{
|
|
key: "acciones",
|
|
label: "Acciones",
|
|
items: [
|
|
{ key: "alta", label: "Nuevo", icon: "pi pi-plus", command: openNew },
|
|
{ key: "edit", label: "Editar", icon: "pi pi-pencil", disabled: !selected.value, command: () => selected.value && openEdit(selected.value) },
|
|
{ key: "docs", label: "Expediente", icon: "pi pi-folder-open", disabled: !selected.value, command: () => selected.value && desk.openProject(selected.value, "contrato") },
|
|
{ key: "budget", label: "Presupuesto", icon: "pi pi-calculator", disabled: !selected.value, command: () => selected.value && navigateTo(`/presupuesto?project=${selected.value.id}`) },
|
|
],
|
|
},
|
|
{
|
|
key: "vista",
|
|
label: "Vista",
|
|
items: [{ key: "refresh", label: "Actualizar", icon: "pi pi-refresh", command: load }],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
async function save() {
|
|
formError.value = "";
|
|
if (!name.value.trim()) {
|
|
formError.value = "Nombre de proyecto obligatorio";
|
|
return;
|
|
}
|
|
if (!companyId.value) {
|
|
formError.value = "Seleccione la empresa del proyecto";
|
|
return;
|
|
}
|
|
const apply = async () => {
|
|
try {
|
|
let id = editId.value;
|
|
if (editId.value) {
|
|
await api(`/v1/projects/${editId.value}`, { method: "PATCH", json: payloadOf() });
|
|
toast.add({ severity: "success", summary: `Guardado ${editCode.value}`, life: 2000 });
|
|
} else {
|
|
const r = await api<{ id: number; code: string }>("/v1/projects", { method: "POST", json: payloadOf() });
|
|
id = r.id;
|
|
toast.add({ severity: "success", summary: `Proyecto ${r.code}`, life: 2500 });
|
|
}
|
|
await uploadDocs(id);
|
|
show.value = false;
|
|
await load();
|
|
const row = projects.value.find((project) => project.id === id);
|
|
if (row) {
|
|
selected.value = row;
|
|
desk.openProject(row, contratoFile.value || presupuestoFile.value ? "contrato" : undefined);
|
|
}
|
|
} catch (error) {
|
|
const detail = error as { data?: { error?: string } };
|
|
formError.value = detail.data?.error || (error instanceof Error ? error.message : "No se pudo guardar");
|
|
}
|
|
};
|
|
if (editId.value && (status.value === "concluido" || status.value === "cancelado")) {
|
|
const current = projects.value.find((p) => p.id === editId.value);
|
|
if (current && current.status !== status.value) {
|
|
confirm.require({
|
|
header: status.value === "concluido" ? "Concluir proyecto" : "Cancelar proyecto",
|
|
message: status.value === "concluido"
|
|
? "Ya no se podrán asignar personas ni generar gafetes."
|
|
: "Queda en historial, sin operación nueva.",
|
|
icon: "pi pi-exclamation-triangle",
|
|
acceptLabel: "Guardar",
|
|
rejectLabel: "Volver",
|
|
accept: apply,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
await apply();
|
|
}
|
|
|
|
async function setStatus(row: Project, next: string) {
|
|
fillForm(row);
|
|
status.value = next;
|
|
const apply = async () => {
|
|
await api(`/v1/projects/${row.id}`, { method: "PATCH", json: payloadOf({ status: next }) });
|
|
toast.add({ severity: "success", summary: `${row.code}: ${projectStatusLabel(next)}`, life: 2000 });
|
|
await load();
|
|
};
|
|
if (next === "concluido" || next === "cancelado") {
|
|
confirm.require({
|
|
header: next === "concluido" ? "Concluir proyecto" : "Cancelar proyecto",
|
|
message: next === "concluido"
|
|
? `¿Marcar ${row.name} como concluido? Ya no se podrán asignar personas ni generar gafetes.`
|
|
: `¿Cancelar ${row.name}? Queda en historial, sin operación nueva.`,
|
|
icon: "pi pi-exclamation-triangle",
|
|
acceptLabel: next === "concluido" ? "Concluir" : "Cancelar proyecto",
|
|
rejectLabel: "Volver",
|
|
accept: apply,
|
|
});
|
|
return;
|
|
}
|
|
await apply();
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const key = desk.treeKey.value;
|
|
if (!key || (!["all", "activo", "pausado", "concluido", "cancelado"].includes(key) && !key.startsWith("c:"))) {
|
|
desk.treeKey.value = "all";
|
|
}
|
|
await load();
|
|
});
|
|
onBeforeUnmount(() => desk.clearStatusMessages());
|
|
watch([projects, selected, visible, companies], syncChrome);
|
|
</script>
|