- 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.
318 lines
17 KiB
Vue
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>
|