Fix purchase order items display in edit mode

Problem:
- When editing a PO in the dashboard, the products section showed "[object Object]" instead of the actual product list
- The EditViewModal's 'list' type expects simple text arrays, not complex objects

Solution:
- Created EditablePurchaseOrderItems custom component to properly render and edit PO items
- Added form state management with useState and useEffect to track edited values
- Implemented handleFieldChange to update form data when users modify fields
- Changed field type from 'list' to 'component' with the custom editor
- Added editable input fields for quantity, unit of measure, and unit price
- Displays real-time item totals and grand total

Technical Details:
- Custom component receives value and onChange props from EditViewModal
- Form data is initialized when entering edit mode with all PO item details
- Each item shows: product name, SKU, quantity with unit selector, and price
- Unit options include kg, g, l, ml, units, boxes, bags
- Proper decimal handling for prices (parseFloat for display, string for API)
- Save handler validates items and updates only priority, delivery date, and notes
  (item modifications are validated but not persisted in this iteration)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Urtzi Alfaro
2025-11-18 12:04:06 +01:00
parent 3c3d3ce042
commit a26bcc779e

View File

@@ -7,7 +7,7 @@
* Now using EditViewModal with proper API response structure
*/
import React, { useState, useMemo } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import {
Package,
Building2,
@@ -45,6 +45,49 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
const [mode, setMode] = useState<'view' | 'edit'>(initialMode);
const updatePurchaseOrderMutation = useUpdatePurchaseOrder();
// Form state for edit mode
const [formData, setFormData] = useState<Record<string, any>>({});
// Initialize form data when entering edit mode
useEffect(() => {
if (mode === 'edit' && po) {
setFormData({
priority: po.priority,
required_delivery_date: po.required_delivery_date || '',
notes: po.notes || '',
items: (po.items || []).map((item: PurchaseOrderItem) => ({
id: item.id,
inventory_product_id: item.inventory_product_id,
product_code: item.product_code || '',
product_name: item.product_name || '',
ordered_quantity: item.ordered_quantity,
unit_of_measure: item.unit_of_measure,
unit_price: parseFloat(item.unit_price),
})),
});
}
}, [mode, po]);
// Field change handler for edit mode
const handleFieldChange = (sectionIndex: number, fieldIndex: number, value: any) => {
// Map section/field indices to form field names
// Section 0 = supplier_info (not editable)
// Section 1 = order_details: [priority, required_delivery_date, notes]
// Section 2 = products: [items]
if (sectionIndex === 1) {
// Order details section
const fieldNames = ['priority', 'required_delivery_date', 'notes'];
const fieldName = fieldNames[fieldIndex];
if (fieldName) {
setFormData(prev => ({ ...prev, [fieldName]: value }));
}
} else if (sectionIndex === 2 && fieldIndex === 0) {
// Products section - items field
setFormData(prev => ({ ...prev, items: value }));
}
};
// Component to display user name with data fetching
const UserName: React.FC<{ userId: string | undefined | null }> = ({ userId }) => {
if (!userId) return <>{t('common:not_available')}</>;
@@ -292,6 +335,113 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
return sections;
};
// Component to edit PO items
const EditablePurchaseOrderItems: React.FC<{ value: any; onChange?: (value: any) => void }> = ({ value: items, onChange }) => {
const handleItemChange = (index: number, field: string, value: any) => {
if (!items || !onChange) return;
const updatedItems = [...items];
updatedItems[index] = {
...updatedItems[index],
[field]: value
};
onChange(updatedItems);
};
if (!items || items.length === 0) {
return (
<div className="text-center py-8 text-[var(--text-secondary)] border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<Package className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>{t('no_items')}</p>
</div>
);
}
const totalAmount = items.reduce((sum, item) => {
const price = parseFloat(item.unit_price) || 0;
const quantity = item.ordered_quantity || 0;
return sum + (price * quantity);
}, 0);
return (
<div className="space-y-3">
{items.map((item: any, index: number) => {
const unitPrice = parseFloat(item.unit_price) || 0;
const quantity = item.ordered_quantity || 0;
const itemTotal = unitPrice * quantity;
const productName = item.product_name || `${t('product')} ${index + 1}`;
return (
<div
key={item.id || index}
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/50 space-y-3"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<h4 className="font-semibold text-[var(--text-primary)]">{productName}</h4>
{item.product_code && (
<p className="text-sm text-[var(--text-secondary)]">
{t('sku')}: {item.product_code}
</p>
)}
</div>
<div className="text-right">
<p className="font-bold text-lg text-[var(--color-primary-600)]">
{itemTotal.toFixed(2)}
</p>
</div>
</div>
{/* Editable fields */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-1">
{t('quantity')}
</label>
<div className="flex gap-2">
<input
type="number"
value={quantity}
onChange={(e) => handleItemChange(index, 'ordered_quantity', parseFloat(e.target.value) || 0)}
min="0"
step="0.01"
className="flex-1 px-3 py-2 border border-[var(--border-secondary)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)]"
/>
<select
value={item.unit_of_measure}
onChange={(e) => handleItemChange(index, 'unit_of_measure', e.target.value)}
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)]"
>
{unitOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-1">
{t('unit_price')}
</label>
<input
type="number"
value={unitPrice}
onChange={(e) => handleItemChange(index, 'unit_price', e.target.value)}
min="0"
step="0.01"
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)]"
/>
</div>
</div>
</div>
);
})}
<div className="flex justify-between items-center pt-4 border-t-2 border-[var(--border-primary)]">
<span className="font-semibold text-lg text-[var(--text-primary)]">{t('total')}</span>
<span className="font-bold text-2xl text-[var(--color-primary-600)]">{totalAmount.toFixed(2)}</span>
</div>
</div>
);
};
// Build sections for edit mode
const buildEditSections = (): EditViewModalSection[] => {
if (!po) return [];
@@ -316,7 +466,7 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
fields: [
{
label: t('priority'),
value: po.priority,
value: formData.priority || po.priority,
type: 'select' as const,
editable: true,
options: priorityOptions,
@@ -324,14 +474,14 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
},
{
label: t('required_delivery_date'),
value: po.required_delivery_date || '',
value: formData.required_delivery_date || po.required_delivery_date || '',
type: 'date' as const,
editable: true,
helpText: t('delivery_deadline')
},
{
label: t('notes'),
value: po.notes || '',
value: formData.notes !== undefined ? formData.notes : (po.notes || ''),
type: 'textarea' as const,
editable: true,
placeholder: t('special_instructions'),
@@ -346,7 +496,7 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
fields: [
{
label: t('products'),
value: (po.items || []).map((item: PurchaseOrderItem) => ({
value: formData.items || (po.items || []).map((item: PurchaseOrderItem) => ({
id: item.id,
inventory_product_id: item.inventory_product_id,
product_code: item.product_code || '',
@@ -355,8 +505,8 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
unit_of_measure: item.unit_of_measure,
unit_price: parseFloat(item.unit_price),
})),
type: 'list' as const,
editable: true,
type: 'component' as const,
component: EditablePurchaseOrderItems,
span: 2,
helpText: t('modify_quantities')
}
@@ -366,7 +516,7 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
};
// Save handler for edit mode
const handleSave = async (formData: Record<string, any>) => {
const handleSave = async () => {
try {
const items = formData.items || [];
@@ -435,7 +585,9 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
return undefined;
};
const sections = mode === 'view' ? buildViewSections() : buildEditSections();
const sections = useMemo(() => {
return mode === 'view' ? buildViewSections() : buildEditSections();
}, [mode, po, formData, i18n.language]);
return (
<EditViewModal
@@ -460,6 +612,7 @@ export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps>
onEdit={po?.status === 'pending_approval' ? () => setMode('edit') : undefined}
onSave={mode === 'edit' ? handleSave : undefined}
onCancel={mode === 'edit' ? () => setMode('view') : undefined}
onFieldChange={handleFieldChange}
saveLabel={t('actions.save')}
cancelLabel={t('actions.cancel')}
/>