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

166 lines
6.7 KiB
Vue

<template>
<div class="detail-section">
<div class="document-progress">
<div>
<div class="section-title">Expediente de obra</div>
<div class="muted">{{ requiredDone }} de {{ requiredItems.length }} documentos obligatorios</div>
</div>
<Tag :value="`${requiredProgress}%`" :severity="requiredProgress === 100 ? 'success' : 'warn'" />
</div>
<ProgressBar :value="requiredProgress" :show-value="false" class="mt-2" />
<div class="document-checklist">
<div v-for="item in requiredItems" :key="item.code" class="document-check" :class="{ complete: item.present }">
<i :class="item.present ? 'pi pi-check-circle' : 'pi pi-circle'" />
<span>{{ item.label }}</span>
<small>Obligatorio</small>
</div>
</div>
<Panel v-if="optionalItems.length" header="Documentos opcionales" toggleable collapsed class="mt-3">
<div class="document-checklist">
<div v-for="item in optionalItems" :key="item.code" class="document-check" :class="{ complete: item.present }">
<i :class="item.present ? 'pi pi-check-circle' : 'pi pi-circle'" /><span>{{ item.label }}</span>
</div>
</div>
</Panel>
<Fieldset legend="Subir o reemplazar documento" class="mt-3">
<FormField label="Tipo de documento">
<Select v-model="documentType" :options="documentTypes" option-label="label" option-value="code" class="w-full" />
</FormField>
<FileUpload
mode="basic"
:auto="true"
choose-label="Seleccionar archivo"
accept=".pdf,.xlsx,.xls,.doc,.docx,.jpg,.jpeg,.png"
custom-upload
class="mt-2"
:disabled="uploading"
@uploader="uploadDocument"
/>
<div class="muted mt-2">Al elegir el archivo se sube solo. PDF, Excel o imagen.</div>
</Fieldset>
<Tabs v-model:value="docsTab" class="mt-3 document-list-tabs">
<TabList>
<Tab value="current">Vigentes ({{ currentDocuments.length }})</Tab>
<Tab value="history">Histórico ({{ historyDocuments.length }})</Tab>
</TabList>
<TabPanels>
<TabPanel value="current">
<DataTable :value="currentDocuments" size="small" empty-message="No hay documentos vigentes">
<Column header="Tipo">
<template #body="{ data }">{{ typeLabel(data.type_code) }}</template>
</Column>
<Column field="original_name" header="Archivo" />
<Column header="">
<template #body="{ data }">
<Button icon="pi pi-eye" text rounded size="small" aria-label="Ver documento" @click="openDocument(data)" />
</template>
</Column>
</DataTable>
</TabPanel>
<TabPanel value="history">
<DataTable :value="historyDocuments" size="small" empty-message="Sin versiones anteriores">
<Column header="Tipo">
<template #body="{ data }">{{ typeLabel(data.type_code) }}</template>
</Column>
<Column field="original_name" header="Archivo" />
<Column header="Fecha">
<template #body="{ data }">{{ formatDate(data.uploaded_at) }}</template>
</Column>
<Column header="">
<template #body="{ data }">
<Button icon="pi pi-eye" text rounded size="small" aria-label="Ver documento" @click="openDocument(data)" />
</template>
</Column>
</DataTable>
</TabPanel>
</TabPanels>
</Tabs>
<DocumentPreviewDialog
v-model="previewOpen"
:path="previewPath"
:name="previewName"
:mime="previewMime"
/>
</div>
</template>
<script setup lang="ts">
import FormField from "~/components/padron/FormField.vue";
import DocumentPreviewDialog from "~/components/DocumentPreviewDialog.vue";
type ChecklistItem = { code: string; label: string; required: boolean; present: boolean };
type DocumentRow = {
id: number;
type_code: string;
original_name: string;
uploaded_at: string;
is_current: number;
mime?: string;
};
const props = defineProps<{ projectId: number; documents: DocumentRow[]; checklist: ChecklistItem[]; initialType?: string }>();
const emit = defineEmits<{ changed: [] }>();
const { api } = useApi();
const toast = useToast();
const documentType = ref(props.initialType || "contrato");
const uploading = ref(false);
const docsTab = ref("current");
const previewOpen = ref(false);
const previewPath = ref("");
const previewName = ref("");
const previewMime = ref<string | null>(null);
const documentTypes = computed(() => props.checklist.map(({ code, label }) => ({ code, label })));
const requiredItems = computed(() => props.checklist.filter((item) => item.required));
const optionalItems = computed(() => props.checklist.filter((item) => !item.required));
const requiredDone = computed(() => requiredItems.value.filter((item) => item.present).length);
const requiredProgress = computed(() => requiredItems.value.length ? Math.round(requiredDone.value / requiredItems.value.length * 100) : 0);
const currentDocuments = computed(() => props.documents.filter((doc) => doc.is_current));
const historyDocuments = computed(() => props.documents.filter((doc) => !doc.is_current));
function typeLabel(code: string) {
return props.checklist.find((item) => item.code === code)?.label || code;
}
function formatDate(value: string) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString("es-MX", { day: "2-digit", month: "short", year: "numeric" });
}
async function uploadDocument(event: { files: File[] }) {
const file = event.files?.[0];
if (!file || uploading.value) return;
uploading.value = true;
try {
const body = new FormData();
body.append("type", documentType.value);
body.append("file", file);
await api(`/v1/projects/${props.projectId}/documents`, { method: "POST", body });
toast.add({ severity: "success", summary: "Documento cifrado y guardado", life: 2200 });
docsTab.value = "current";
emit("changed");
} catch (error: unknown) {
toast.add({
severity: "error",
summary: error instanceof Error ? error.message : "No se pudo subir el archivo",
life: 3500,
});
} finally {
uploading.value = false;
}
}
function openDocument(row: DocumentRow) {
previewPath.value = `/v1/projects/${props.projectId}/documents/${row.id}`;
previewName.value = row.original_name;
previewMime.value = row.mime || null;
previewOpen.value = true;
}
watch(() => props.initialType, (value) => { if (value) documentType.value = value; });
watch(() => props.checklist, (items) => {
if (!items.some((item) => item.code === documentType.value) && items[0]) documentType.value = items[0].code;
}, { immediate: true });
</script>