edgar.mendez 71454dda61 feat(purchases): add purchase details, form, and listing components
- Implemented PurchaseDetails.vue for displaying detailed purchase information.
- Created PurchaseForm.vue for submitting new purchase requests with supplier and item management.
- Developed Purchases.vue for listing all purchase orders with actions for approval, rejection, and conversion.
- Added purchaseServices.ts for API interactions related to purchases.
- Defined types for purchase forms and purchases in respective TypeScript files.
- Integrated PrimeVue components for UI consistency and functionality.
2026-02-10 02:02:58 -06:00

318 lines
17 KiB
Vue

<template>
<div class="purchase-details-container">
<!-- Breadcrumbs -->
<Breadcrumb :home="home" :model="breadcrumbItems">
<template #item="{ item }">
<a v-if="item.route" :href="item.route" @click.prevent="router.push(item.route)"
class="text-primary hover:underline">
{{ item.label }}
</a>
<span v-else class="text-[#617589] text-sm font-bold leading-normal">
{{ item.label }}
</span>
</template>
</Breadcrumb>
<!-- Page Heading & Actions -->
<div v-if="!loading && purchase"
class="flex flex-wrap justify-between items-end gap-4 bg-white dark:bg-background-dark p-6 rounded-xl border border-[#dbe0e6] dark:border-gray-800 mb-6 shadow-sm">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<h1 class="text-[#111418] dark:text-white text-4xl font-black leading-tight tracking-[-0.033em]">{{
purchase.purchase_number }}</h1>
<span
class="px-3 py-1 bg-primary/10 text-primary text-xs font-bold rounded-full border border-primary/20">{{
PURCHASE_STATUS_MAP[purchase.status] || purchase.status }}</span>
</div>
<p class="text-[#617589] text-base font-normal leading-normal">Creada el {{
formatDate(purchase.request_date) }}
</p>
</div>
<div class="flex gap-3">
<Button icon="pi pi-check-circle" label="Convertir a compra" class="min-w-[140px] h-10 px-4" />
</div>
</div>
<div v-else-if="loading" class="flex justify-center items-center min-h-[200px]">
<span class="pi pi-spin pi-spinner text-3xl text-primary"></span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left Column: Details & Items -->
<div class="lg:col-span-2 flex flex-col gap-6">
<!-- Order Information Card -->
<Card v-if="!loading && purchase"
class="rounded-xl border border-[#dbe0e6] dark:border-gray-800 overflow-hidden shadow-sm">
<template #header>
<div
class="p-4 border-b border-[#dbe0e6] dark:border-gray-800 bg-[#f8f9fa] dark:bg-gray-800/50 flex items-center gap-2">
<span class="pi pi-info-circle text-sm text-[#617589]"></span>
<span class="font-bold text-sm uppercase tracking-wider text-[#617589]">Información de la solicitud</span>
</div>
</template>
<template #content>
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
<div class="flex flex-col gap-1 border-l-4 border-primary pl-4">
<p class="text-[#617589] text-xs font-semibold uppercase">Información del proveedor</p>
<p class="text-[#111418] dark:text-white text-base font-bold">
{{ purchase.supplier && purchase.supplier.name ? purchase.supplier.name : 'Sin proveedor' }}
</p>
<p class="text-[#617589] text-sm">
<span v-if="purchase.supplier && purchase.supplier.contact_email">{{
purchase.supplier.contact_email }}</span>
<span
v-if="purchase.supplier && purchase.supplier.contact_email && purchase.supplier.phone_number">
| </span>
<span v-if="purchase.supplier && purchase.supplier.phone_number">{{
purchase.supplier.phone_number }}</span>
</p>
</div>
<div class="flex flex-col gap-1 border-l-4 border-gray-300 dark:border-gray-600 pl-4">
<p class="text-[#617589] text-xs font-semibold uppercase">Referencia de la solicitud de compra</p>
<p class="text-[#111418] dark:text-white text-base font-bold">{{
purchase.purchase_number }}</p>
</div>
<div class="flex flex-col gap-1 border-l-4 border-gray-300 dark:border-gray-600 pl-4">
<p class="text-[#617589] text-xs font-semibold uppercase">Notas</p>
<p class="text-[#111418] dark:text-white text-base font-bold">{{ purchase.notes }}</p>
</div>
<div class="flex flex-col gap-1 border-l-4 border-gray-300 dark:border-gray-600 pl-4">
<p class="text-[#617589] text-xs font-semibold uppercase">Fecha de Entrega</p>
<p class="text-[#111418] dark:text-white text-base font-bold">{{
formatDate(purchase.delivery_date) }}</p>
</div>
</div>
</template>
</Card>
<!-- Ordered Items Card -->
<Card v-if="!loading && purchase"
class="rounded-xl border border-[#dbe0e6] dark:border-gray-800 overflow-hidden shadow-sm">
<template #header>
<div
class="p-4 border-b border-[#dbe0e6] dark:border-gray-800 bg-[#f8f9fa] dark:bg-gray-800/50 flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="pi pi-list text-sm text-[#617589]"></span>
<span class="font-bold text-sm uppercase tracking-wider text-[#617589]">Productos</span>
</div>
<span class="text-xs text-[#617589] font-medium">{{ purchase.items ? purchase.items.length :
0 }} Productos listados</span>
</div>
</template>
<template #content>
<DataTable v-if="purchase.items && purchase.items.length" :value="purchase.items"
class="w-full">
<Column field="product_id" header="ID" />
<Column field="quantity" header="Cantidad" body-class="text-center" />
<Column field="unit_price" header="Precio Unitario" body-class="text-right">
<template #body="{ data }">
<span>{{ formatCurrency(data.unit_price) }}</span>
</template>
</Column>
<Column field="subtotal" header="Subtotal" body-class="text-right">
<template #body="{ data }">
<span class="font-bold text-primary">{{ formatCurrency(data.subtotal) }}</span>
</template>
</Column>
<Column field="notes" header="Notas" />
</DataTable>
<div v-else class="text-center text-[#617589] py-6">No hay items para mostrar.</div>
</template>
</Card>
</div>
<!-- Right Column: Logistics & Financial Summary -->
<div class="flex flex-col gap-6">
<!-- Logistics Tracking Card -->
<!-- <Card class="rounded-xl border border-[#dbe0e6] dark:border-gray-800 overflow-hidden shadow-sm">
<template #header>
<div
class="p-4 border-b border-[#dbe0e6] dark:border-gray-800 bg-[#f8f9fa] dark:bg-gray-800/50 flex items-center gap-2">
<span class="pi pi-truck text-sm text-[#617589]"></span>
<span class="font-bold text-sm uppercase tracking-wider text-[#617589]">Logistics</span>
</div>
</template>
<div class="p-6">
<div class="mb-6">
<p class="text-[#617589] text-xs font-semibold uppercase mb-2">Delivery Status</p>
<div class="flex items-center gap-3">
<div class="w-full bg-[#f0f2f4] dark:bg-gray-800 rounded-full h-2">
<div class="bg-primary h-2 rounded-full" style="width: 40%"></div>
</div>
<span class="text-xs font-bold text-primary whitespace-nowrap">In Transit</span>
</div>
<p class="text-[#617589] text-[11px] mt-2 italic">Estimated delivery: Oct 31, 2023</p>
</div>
<div>
<p class="text-[#617589] text-xs font-semibold uppercase mb-2">Destination</p>
<div class="flex items-start gap-3">
<span class="pi pi-map-marker text-primary mt-1"></span>
<div class="text-sm">
<p class="font-bold">Main Distribution Center</p>
<p class="text-[#617589]">Warehouse B, Dock 4</p>
<p class="text-[#617589]">882 Logistics Way</p>
<p class="text-[#617589]">Chicago, IL 60601</p>
</div>
</div>
</div>
<div class="mt-6 pt-6 border-t border-[#dbe0e6] dark:border-gray-800">
<div class="rounded-lg overflow-hidden h-32 bg-gray-100 relative group">
<div class="absolute inset-0 bg-primary/10 flex items-center justify-center">
<span class="pi pi-map-marker text-3xl text-primary animate-bounce"></span>
</div>
<img class="w-full h-full object-cover grayscale"
alt="Map view showing delivery destination"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuCm1mh8AyZ_GOLrQWGg6_fQE6_m-PpLiUyQDhDK_cooMkYT8QRVfPsWPGyLAICz7vKq7tVhRDGTJk9e0XjZZ-91XpsMoguksdn7YEB_WJflyuZvPsCqcxApGFkkC11P9N1iPNw6YcDM5VPuF1AQVB1JpyQijgQIZTsnuK5A1Fm6--evPpKckIawOpHRj8FBB8wh2YQM9fZ5Zl45ex3f4q9fTYU3k0k0Bgu6pG2hZC31PEqu2QZgJp3-LNsWIozBAEJMjrOjLzRnR7aj" />
</div>
</div>
</div>
</Card> -->
<!-- Financial Summary Card -->
<Card class="rounded-xl border-2 border-primary overflow-hidden shadow-lg">
<template #header>
<div
class="p-4 border-b border-[#dbe0e6] dark:border-gray-800 bg-primary/5 flex items-center gap-2">
<span class="pi pi-credit-card text-sm text-primary"></span>
<span class="font-bold text-sm uppercase tracking-wider text-primary">Resumen
Financiero</span>
</div>
</template>
<template #content>
<div class="p-6 flex flex-col gap-4">
<div class="flex justify-between items-center">
<span class="text-[#617589] text-sm">Subtotal</span>
<span class="font-bold text-primary">{{ formatCurrency(purchase?.total_amount ? purchase.total_amount : 0) }}</span>
</div>
<!-- <div class="flex justify-between items-center">
<span class="text-[#617589] text-sm">Impuesto</span>
<span class="font-bold text-primary">{{ formatCurrency(summary.tax) }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-[#617589] text-sm">Envío</span>
<span class="font-bold text-primary">{{ formatCurrency(summary.shipping) }}</span>
</div> -->
<div class="flex justify-between items-center pt-4 border-t border-[#dbe0e6] dark:border-gray-800">
<span class="text-[#617589] text-sm font-semibold uppercase">Total</span>
<span class="font-bold text-xl text-primary">{{ formatCurrency(purchase?.total_amount ? purchase.total_amount : 0) }}</span>
</div>
</div>
</template>
<div class="p-4 bg-[#f8f9fa] dark:bg-gray-800/50 text-center">
<p class="text-[11px] text-[#617589] font-medium leading-relaxed">
La factura final puede variar según el peso exacto del envío y los impuestos locales en el momento de la entrega.
</p>
</div>
</Card>
</div>
</div>
<!-- Footer Audit Log -->
<div
class="mt-8 flex items-center justify-between text-[#617589] text-[11px] font-medium uppercase tracking-widest px-4">
<div class="flex items-center gap-4">
<span>Last modified: Oct 25, 2023 10:42 AM</span>
<span class="size-1 rounded-full bg-[#dbe0e6]"></span>
<span>Revision: v2.1</span>
</div>
<div class="flex items-center gap-2">
<span class="pi pi-lock text-xs"></span>
<span>Secure Transaction</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Card from 'primevue/card';
import Button from 'primevue/button';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Breadcrumb from 'primevue/breadcrumb';
import { useRouter } from 'vue-router';
import { purchaseServices } from '../services/purchaseServices';
import type { PurchaseDetailResponse, PurchaseDetailData, PurchaseItem } from '../types/purchases';
import { useRoute } from 'vue-router';
import { PURCHASE_STATUS_MAP } from '../types/purchases.d';
const items = ref([
{
product: 'Advanced Logic Controller v4',
sku: 'ALC-V4-99',
qty: 10,
unitPrice: 450,
tax: 8.5,
lineTotal: 4882.5,
},
{
product: 'Industrial Power Supply 12V',
sku: 'PWR-IND-12',
qty: 25,
unitPrice: 85,
tax: 8.5,
lineTotal: 2305.63,
},
{
product: 'Ethernet Patch Cable Cat6 (2m)',
sku: 'CAB-CAT6-2M',
qty: 100,
unitPrice: 4.5,
tax: 0,
lineTotal: 450,
},
{
product: 'Sensor Mount Bracket A',
sku: 'BRA-SENS-A',
qty: 50,
unitPrice: 12,
tax: 8.5,
lineTotal: 651,
},
]);
const summary = ref({
subtotal: 7625,
tax: 664.13,
shipping: 0,
total: 8289.13,
});
const router = useRouter();
const breadcrumbItems = [
{ label: 'Inicio', route: '/' },
{ label: 'Compras', route: '/purchases/requests' },
{ label: 'Órdenes', route: '/purchases/requests' },
{ label: 'Detalle' }
];
const home = { icon: 'pi pi-home', route: '/' };
const route = useRoute();
const purchase = ref<PurchaseDetailData | null>(null);
const loading = ref(true);
onMounted(async () => {
loading.value = true;
try {
const id = Number(route.params.id);
if (id) {
const response: PurchaseDetailResponse = await purchaseServices.getPurchaseById(id);
console.log('Fetched purchase details:', response);
purchase.value = response.data;
}
} finally {
loading.value = false;
}
});
function formatCurrency(value: number | string) {
const num = typeof value === 'string' ? parseFloat(value) : value;
return num.toLocaleString('en-US', {
style: 'currency',
currency: 'MXN',
minimumFractionDigits: 2,
});
}
function formatDate(dateStr: string) {
if (!dateStr) return '';
const d = new Date(dateStr.replace(' ', 'T'));
return d.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
</script>