CRITICAL BUG FIX: The useEffect hooks syncing wizard state with parent were
causing infinite re-render loops, completely blocking all UI interactions
(text inputs, clicks, selections, etc.).
Root cause: useEffect(() => onDataChange({...data, ...localState}), [localState])
creates infinite loop because localState is recreated on every render, triggering
the effect again, which updates parent, which re-renders component, repeat forever.
Solution: Remove problematic useEffect sync hooks and instead:
1. Create handler functions that update both local state AND parent state together
2. Call these handlers directly in onChange events (not in useEffect)
3. Only sync auto-generated fields (SKU, order number) in useEffect with proper deps
Files fixed:
- InventoryWizard.tsx: Added handleDataChange() function, updated all onChange
- QualityTemplateWizard.tsx: Added handleDataChange() function, updated all onChange
- CustomerOrderWizard.tsx: Fixed all 3 steps:
* Step 1: handleCustomerChange(), handleNewCustomerChange()
* Step 2: updateOrderItems()
* Step 3: handleOrderDataChange()
Testing: All text inputs, select dropdowns, checkboxes, and buttons now work correctly.
UI is responsive and performant without infinite re-rendering.
This was blocking all user interactions - highest priority fix.
1428 lines
64 KiB
TypeScript
1428 lines
64 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
|
|
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
|
|
import Tooltip from '../../../ui/Tooltip/Tooltip';
|
|
import {
|
|
Users,
|
|
Plus,
|
|
Package,
|
|
Truck,
|
|
CreditCard,
|
|
Search,
|
|
Calendar,
|
|
MapPin,
|
|
Info,
|
|
Loader2,
|
|
CheckCircle2,
|
|
} from 'lucide-react';
|
|
import { useTenant } from '../../../../stores/tenant.store';
|
|
import OrdersService from '../../../../api/services/orders';
|
|
import { inventoryService } from '../../../../api/services/inventory';
|
|
import { ProductType } from '../../../../api/types/inventory';
|
|
|
|
interface WizardDataProps extends WizardStepProps {
|
|
data: Record<string, any>;
|
|
onDataChange: (data: Record<string, any>) => void;
|
|
}
|
|
|
|
// Step 1: Customer Selection
|
|
const CustomerSelectionStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|
const { currentTenant } = useTenant();
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [showNewCustomerForm, setShowNewCustomerForm] = useState(false);
|
|
const [selectedCustomer, setSelectedCustomer] = useState(data.customer || null);
|
|
const [customers, setCustomers] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [newCustomer, setNewCustomer] = useState({
|
|
name: data.newCustomerName || '',
|
|
type: data.newCustomerType || 'retail',
|
|
phone: data.newCustomerPhone || '',
|
|
email: data.newCustomerEmail || '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchCustomers();
|
|
}, []);
|
|
|
|
// Update parent whenever customer selection changes
|
|
const handleCustomerChange = (newCustomer: any, newShowForm: boolean) => {
|
|
setSelectedCustomer(newCustomer);
|
|
setShowNewCustomerForm(newShowForm);
|
|
onDataChange({
|
|
...data,
|
|
customer: newCustomer,
|
|
showNewCustomerForm: newShowForm,
|
|
});
|
|
};
|
|
|
|
// Update new customer data
|
|
const handleNewCustomerChange = (updates: any) => {
|
|
const updated = { ...newCustomer, ...updates };
|
|
setNewCustomer(updated);
|
|
onDataChange({
|
|
...data,
|
|
showNewCustomerForm,
|
|
newCustomerName: updated.name,
|
|
newCustomerType: updated.type,
|
|
newCustomerPhone: updated.phone,
|
|
newCustomerEmail: updated.email,
|
|
});
|
|
};
|
|
|
|
const fetchCustomers = async () => {
|
|
if (!currentTenant?.id) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await OrdersService.getCustomers({
|
|
tenant_id: currentTenant.id,
|
|
active_only: true,
|
|
});
|
|
setCustomers(result);
|
|
} catch (err: any) {
|
|
console.error('Error loading customers:', err);
|
|
setError('Error loading customers');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filteredCustomers = customers.filter((customer) =>
|
|
customer.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
const handleSelectCustomer = (customer: any) => {
|
|
handleCustomerChange(customer, false);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
<Users className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
Select Customer
|
|
</h3>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Search for an existing customer or create a new one
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
<span className="ml-3 text-[var(--text-secondary)]">Loading customers...</span>
|
|
</div>
|
|
) : !showNewCustomerForm ? (
|
|
<>
|
|
{/* Search Bar */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Search customer by name..."
|
|
className="w-full pl-10 pr-4 py-3 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
{/* Customer List */}
|
|
<div className="space-y-3 max-h-96 overflow-y-auto pr-2">
|
|
{filteredCustomers.length === 0 ? (
|
|
<div className="text-center py-8 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
|
<Users className="w-12 h-12 mx-auto mb-3 text-[var(--text-tertiary)]" />
|
|
<p className="text-[var(--text-secondary)] mb-1">No customers found</p>
|
|
<p className="text-sm text-[var(--text-tertiary)]">Try a different search term</p>
|
|
</div>
|
|
) : (
|
|
filteredCustomers.map((customer) => (
|
|
<button
|
|
key={customer.id}
|
|
onClick={() => handleSelectCustomer(customer)}
|
|
className={`w-full p-4 rounded-xl border-2 transition-all text-left group hover:shadow-md ${
|
|
selectedCustomer?.id === customer.id
|
|
? 'border-[var(--color-primary)] bg-gradient-to-r from-[var(--color-primary)]/10 to-[var(--color-primary)]/5 shadow-sm'
|
|
: 'border-[var(--border-secondary)] hover:border-[var(--color-primary)]/50 hover:bg-[var(--bg-secondary)]/30'
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 transition-colors ${
|
|
selectedCustomer?.id === customer.id
|
|
? 'bg-[var(--color-primary)] text-white'
|
|
: 'bg-[var(--bg-tertiary)] text-[var(--text-tertiary)] group-hover:bg-[var(--color-primary)]/20 group-hover:text-[var(--color-primary)]'
|
|
}`}>
|
|
<Users className="w-6 h-6" />
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h4 className={`font-semibold truncate transition-colors ${
|
|
selectedCustomer?.id === customer.id
|
|
? 'text-[var(--color-primary)]'
|
|
: 'text-[var(--text-primary)]'
|
|
}`}>
|
|
{customer.name}
|
|
</h4>
|
|
{selectedCustomer?.id === customer.id && (
|
|
<CheckCircle2 className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
customer.customer_type === 'wholesale'
|
|
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
|
: customer.customer_type === 'restaurant'
|
|
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300'
|
|
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
|
}`}>
|
|
{customer.customer_type}
|
|
</span>
|
|
{customer.phone && <span>📱 {customer.phone}</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Create New Customer Button */}
|
|
<button
|
|
onClick={() => handleCustomerChange(null, true)}
|
|
className="w-full p-4 border-2 border-dashed border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] transition-colors text-[var(--text-secondary)] hover:text-[var(--color-primary)] flex items-center justify-center gap-2"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
<span className="font-medium">Create new customer</span>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* New Customer Form */}
|
|
<div className="p-4 bg-[var(--bg-secondary)]/30 rounded-lg border border-[var(--border-secondary)]">
|
|
<h4 className="font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<Plus className="w-5 h-5" />
|
|
New Customer
|
|
</h4>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Customer Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newCustomer.name}
|
|
onChange={(e) => handleNewCustomerChange({ name: e.target.value })}
|
|
placeholder="E.g., The Mill Restaurant"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Customer Type *
|
|
</label>
|
|
<select
|
|
value={newCustomer.type}
|
|
onChange={(e) => handleNewCustomerChange({ type: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="retail">Retail</option>
|
|
<option value="wholesale">Wholesale</option>
|
|
<option value="event">Event</option>
|
|
<option value="restaurant">Restaurant</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Phone *
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={newCustomer.phone}
|
|
onChange={(e) => handleNewCustomerChange({ phone: e.target.value })}
|
|
placeholder="+34 123 456 789"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Email (Optional)
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={newCustomer.email}
|
|
onChange={(e) => handleNewCustomerChange({ email: e.target.value })}
|
|
placeholder="contact@restaurant.com"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleCustomerChange(selectedCustomer, false)}
|
|
className="mt-4 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
>
|
|
← Back to customer list
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Step 2: Order Items
|
|
const OrderItemsStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|
const { currentTenant } = useTenant();
|
|
const [orderItems, setOrderItems] = useState(data.orderItems || []);
|
|
const [products, setProducts] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchProducts();
|
|
}, []);
|
|
|
|
// Update parent whenever order items change
|
|
const updateOrderItems = (newItems: any[]) => {
|
|
setOrderItems(newItems);
|
|
const totalAmount = newItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
|
onDataChange({ ...data, orderItems: newItems, totalAmount });
|
|
};
|
|
|
|
const fetchProducts = async () => {
|
|
if (!currentTenant?.id) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const allIngredients = await inventoryService.getIngredients(currentTenant.id);
|
|
const finishedProducts = allIngredients.filter(
|
|
(ingredient) => ingredient.product_type === ProductType.FINISHED_PRODUCT
|
|
);
|
|
setProducts(finishedProducts);
|
|
} catch (err: any) {
|
|
console.error('Error loading products:', err);
|
|
setError('Error loading products');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAddItem = () => {
|
|
updateOrderItems([
|
|
...orderItems,
|
|
{
|
|
id: Date.now(),
|
|
productId: '',
|
|
productName: '',
|
|
quantity: 1,
|
|
unitPrice: 0,
|
|
customRequirements: '',
|
|
subtotal: 0,
|
|
unitOfMeasure: 'units',
|
|
},
|
|
]);
|
|
};
|
|
|
|
const handleUpdateItem = (index: number, field: string, value: any) => {
|
|
const updated = orderItems.map((item: any, i: number) => {
|
|
if (i === index) {
|
|
const newItem = { ...item, [field]: value };
|
|
|
|
if (field === 'productId') {
|
|
const product = products.find((p) => p.id === value);
|
|
if (product) {
|
|
newItem.productName = product.name;
|
|
newItem.unitPrice = product.average_cost || product.last_purchase_price || 0;
|
|
newItem.unitOfMeasure = product.unit_of_measure;
|
|
}
|
|
}
|
|
|
|
if (field === 'quantity' || field === 'unitPrice' || field === 'productId') {
|
|
newItem.subtotal = (newItem.quantity || 0) * (newItem.unitPrice || 0);
|
|
}
|
|
return newItem;
|
|
}
|
|
return item;
|
|
});
|
|
updateOrderItems(updated);
|
|
};
|
|
|
|
const handleRemoveItem = (index: number) => {
|
|
updateOrderItems(orderItems.filter((_: any, i: number) => i !== index));
|
|
};
|
|
|
|
const calculateTotal = () => {
|
|
return orderItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
Order Items
|
|
</h3>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Customer: <span className="font-semibold">{data.customer?.name || 'New Customer'}</span>
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
<span className="ml-3 text-[var(--text-secondary)]">Loading products...</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)]">
|
|
Order Products
|
|
</label>
|
|
<button
|
|
onClick={handleAddItem}
|
|
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors flex items-center gap-1"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Product
|
|
</button>
|
|
</div>
|
|
|
|
{orderItems.length === 0 ? (
|
|
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg text-[var(--text-tertiary)]">
|
|
<Package className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
<p className="mb-2">No products in order</p>
|
|
<p className="text-sm">Click "Add Product" to start</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{orderItems.map((item: any, index: number) => (
|
|
<div
|
|
key={item.id}
|
|
className="p-4 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30 space-y-3"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-semibold text-[var(--text-primary)]">
|
|
Product #{index + 1}
|
|
</span>
|
|
<button
|
|
onClick={() => handleRemoveItem(index)}
|
|
className="p-1 text-red-500 hover:text-red-700 transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Product *
|
|
</label>
|
|
<select
|
|
value={item.productId}
|
|
onChange={(e) => handleUpdateItem(index, 'productId', e.target.value)}
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="">Select product...</option>
|
|
{products.map((product) => (
|
|
<option key={product.id} value={product.id}>
|
|
{product.name} - €{(product.average_cost || product.last_purchase_price || 0).toFixed(2)} / {product.unit_of_measure}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Quantity *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={item.quantity}
|
|
onChange={(e) => handleUpdateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
min="0"
|
|
step="1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Unit Price (€)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={item.unitPrice}
|
|
onChange={(e) => handleUpdateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)}
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
min="0"
|
|
step="0.01"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1">
|
|
Special Requirements (Optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={item.customRequirements}
|
|
onChange={(e) => handleUpdateItem(index, 'customRequirements', e.target.value)}
|
|
placeholder="E.g., No nuts, extra chocolate..."
|
|
className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-2 border-t border-[var(--border-primary)] text-sm">
|
|
<span className="font-semibold text-[var(--text-primary)]">
|
|
Subtotal: €{item.subtotal.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{orderItems.length > 0 && (
|
|
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded-lg border-2 border-[var(--color-primary)]/20">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-lg font-semibold text-[var(--text-primary)]">Order Total:</span>
|
|
<span className="text-2xl font-bold text-[var(--color-primary)]">
|
|
€{calculateTotal().toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Step 3: Delivery & Payment with ALL fields
|
|
const DeliveryPaymentStep: React.FC<WizardDataProps> = ({ data, onDataChange }) => {
|
|
const [orderData, setOrderData] = useState({
|
|
// Required fields
|
|
requestedDeliveryDate: data.requestedDeliveryDate || '',
|
|
orderNumber: data.orderNumber || '',
|
|
|
|
// Basic order info
|
|
orderType: data.orderType || 'standard',
|
|
priority: data.priority || 'normal',
|
|
status: data.status || 'pending',
|
|
|
|
// Delivery fields
|
|
deliveryMethod: data.deliveryMethod || 'pickup',
|
|
deliveryAddress: data.deliveryAddress || '',
|
|
deliveryInstructions: data.deliveryInstructions || '',
|
|
deliveryContactName: data.deliveryContactName || '',
|
|
deliveryContactPhone: data.deliveryContactPhone || '',
|
|
deliveryTimeWindow: data.deliveryTimeWindow || '',
|
|
deliveryFee: data.deliveryFee || '',
|
|
|
|
// Payment fields
|
|
paymentMethod: data.paymentMethod || 'invoice',
|
|
paymentTerms: data.paymentTerms || 'net_30',
|
|
paymentStatus: data.paymentStatus || 'pending',
|
|
paymentDueDate: data.paymentDueDate || '',
|
|
|
|
// Pricing fields
|
|
subtotalAmount: data.subtotalAmount || '',
|
|
taxAmount: data.taxAmount || '',
|
|
discountPercentage: data.discountPercentage || '',
|
|
discountAmount: data.discountAmount || '',
|
|
shippingCost: data.shippingCost || '',
|
|
|
|
// Production & scheduling
|
|
productionStartDate: data.productionStartDate || '',
|
|
productionDueDate: data.productionDueDate || '',
|
|
productionBatchNumber: data.productionBatchNumber || '',
|
|
productionNotes: data.productionNotes || '',
|
|
|
|
// Fulfillment
|
|
actualDeliveryDate: data.actualDeliveryDate || '',
|
|
pickupLocation: data.pickupLocation || '',
|
|
shippingTrackingNumber: data.shippingTrackingNumber || '',
|
|
shippingCarrier: data.shippingCarrier || '',
|
|
|
|
// Source & channel
|
|
orderSource: data.orderSource || 'manual',
|
|
salesChannel: data.salesChannel || 'direct',
|
|
salesRepId: data.salesRepId || '',
|
|
|
|
// Communication
|
|
customerPurchaseOrder: data.customerPurchaseOrder || '',
|
|
internalNotes: data.internalNotes || '',
|
|
customerNotes: data.customerNotes || '',
|
|
specialInstructions: data.specialInstructions || '',
|
|
|
|
// Notifications
|
|
notifyCustomerOnStatusChange: data.notifyCustomerOnStatusChange ?? true,
|
|
notifyCustomerOnDelivery: data.notifyCustomerOnDelivery ?? true,
|
|
customerNotificationEmail: data.customerNotificationEmail || '',
|
|
customerNotificationPhone: data.customerNotificationPhone || '',
|
|
|
|
// Quality & requirements
|
|
qualityCheckRequired: data.qualityCheckRequired ?? false,
|
|
qualityCheckStatus: data.qualityCheckStatus || '',
|
|
packagingInstructions: data.packagingInstructions || '',
|
|
labelingRequirements: data.labelingRequirements || '',
|
|
|
|
// Advanced options
|
|
isRecurring: data.isRecurring ?? false,
|
|
recurringSchedule: data.recurringSchedule || '',
|
|
parentOrderId: data.parentOrderId || '',
|
|
relatedOrderIds: data.relatedOrderIds || '',
|
|
tags: data.tags || '',
|
|
metadata: data.metadata || '',
|
|
});
|
|
|
|
// Auto-generate order number if not provided
|
|
useEffect(() => {
|
|
if (!orderData.orderNumber) {
|
|
const orderNum = `ORD-${Date.now().toString().slice(-8)}`;
|
|
const newData = { ...orderData, orderNumber: orderNum };
|
|
setOrderData(newData);
|
|
onDataChange({ ...data, ...newData });
|
|
}
|
|
}, [orderData.orderNumber]);
|
|
|
|
// Update parent whenever order data changes
|
|
const handleOrderDataChange = (newOrderData: any) => {
|
|
setOrderData(newOrderData);
|
|
onDataChange({ ...data, ...newOrderData });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
<Truck className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
|
Delivery & Payment Details
|
|
</h3>
|
|
<p className="text-sm text-[var(--text-secondary)]">
|
|
Configure delivery, payment, and order details
|
|
</p>
|
|
</div>
|
|
|
|
{/* Required Fields */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<Calendar className="w-4 h-4 inline mr-1.5" />
|
|
Requested Delivery Date *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={orderData.requestedDeliveryDate}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, requestedDeliveryDate: e.target.value })}
|
|
min={new Date().toISOString().split('T')[0]}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Order Number
|
|
<Tooltip content="Auto-generated order reference number">
|
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
</Tooltip>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.orderNumber}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, orderNumber: e.target.value })}
|
|
placeholder="ORD-12345678"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Basic Order Info */}
|
|
<div className="border-t border-[var(--border-primary)] pt-4">
|
|
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Basic Order Information</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Order Type
|
|
</label>
|
|
<select
|
|
value={orderData.orderType}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, orderType: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="standard">Standard</option>
|
|
<option value="custom">Custom</option>
|
|
<option value="bulk">Bulk</option>
|
|
<option value="urgent">Urgent</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Priority
|
|
</label>
|
|
<select
|
|
value={orderData.priority}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, priority: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="low">Low</option>
|
|
<option value="normal">Normal</option>
|
|
<option value="high">High</option>
|
|
<option value="urgent">Urgent</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Status
|
|
</label>
|
|
<select
|
|
value={orderData.status}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, status: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="pending">Pending</option>
|
|
<option value="confirmed">Confirmed</option>
|
|
<option value="in_production">In Production</option>
|
|
<option value="ready">Ready</option>
|
|
<option value="delivered">Delivered</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delivery Details */}
|
|
<div className="border-t border-[var(--border-primary)] pt-4">
|
|
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Delivery Details</h4>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<Truck className="w-4 h-4 inline mr-1.5" />
|
|
Delivery Method
|
|
</label>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'pickup' })}
|
|
className={`p-3 rounded-lg border-2 transition-all ${
|
|
orderData.deliveryMethod === 'pickup'
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)]'
|
|
}`}
|
|
>
|
|
<p className="font-semibold text-sm">Pickup</p>
|
|
<p className="text-xs text-[var(--text-tertiary)]">Customer pickup</p>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'delivery' })}
|
|
className={`p-3 rounded-lg border-2 transition-all ${
|
|
orderData.deliveryMethod === 'delivery'
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)]'
|
|
}`}
|
|
>
|
|
<p className="font-semibold text-sm">Delivery</p>
|
|
<p className="text-xs text-[var(--text-tertiary)]">Home delivery</p>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleOrderDataChange({ ...orderData, deliveryMethod: 'shipping' })}
|
|
className={`p-3 rounded-lg border-2 transition-all ${
|
|
orderData.deliveryMethod === 'shipping'
|
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
|
: 'border-[var(--border-secondary)]'
|
|
}`}
|
|
>
|
|
<p className="font-semibold text-sm">Shipping</p>
|
|
<p className="text-xs text-[var(--text-tertiary)]">Courier service</p>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{(orderData.deliveryMethod === 'delivery' || orderData.deliveryMethod === 'shipping') && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<MapPin className="w-4 h-4 inline mr-1.5" />
|
|
Delivery Address *
|
|
</label>
|
|
<textarea
|
|
value={orderData.deliveryAddress}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryAddress: e.target.value })}
|
|
placeholder="Street, number, floor, postal code, city..."
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Delivery Contact Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.deliveryContactName}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactName: e.target.value })}
|
|
placeholder="Contact person"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Delivery Contact Phone
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={orderData.deliveryContactPhone}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryContactPhone: e.target.value })}
|
|
placeholder="+34 123 456 789"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Payment Details */}
|
|
<div className="border-t border-[var(--border-primary)] pt-4">
|
|
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Payment Details</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
<CreditCard className="w-4 h-4 inline mr-1.5" />
|
|
Payment Method
|
|
</label>
|
|
<select
|
|
value={orderData.paymentMethod}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, paymentMethod: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="cash">Cash</option>
|
|
<option value="card">Card</option>
|
|
<option value="bank_transfer">Bank Transfer</option>
|
|
<option value="invoice">Invoice</option>
|
|
<option value="account">Account</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Payment Terms
|
|
</label>
|
|
<select
|
|
value={orderData.paymentTerms}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, paymentTerms: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="immediate">Immediate</option>
|
|
<option value="net_30">Net 30</option>
|
|
<option value="net_60">Net 60</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Payment Status
|
|
</label>
|
|
<select
|
|
value={orderData.paymentStatus}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, paymentStatus: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="pending">Pending</option>
|
|
<option value="partial">Partial</option>
|
|
<option value="paid">Paid</option>
|
|
<option value="overdue">Overdue</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Payment Due Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={orderData.paymentDueDate}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, paymentDueDate: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Summary */}
|
|
<div className="p-4 bg-[var(--bg-secondary)]/50 rounded-lg border border-[var(--border-secondary)]">
|
|
<h4 className="font-semibold text-[var(--text-primary)] mb-3">Order Summary</h4>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-[var(--text-secondary)]">Customer:</span>
|
|
<span className="font-medium">{data.customer?.name || 'New Customer'}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[var(--text-secondary)]">Products:</span>
|
|
<span className="font-medium">{data.orderItems?.length || 0} items</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-[var(--text-secondary)]">Total:</span>
|
|
<span className="font-semibold text-lg text-[var(--color-primary)]">
|
|
€{data.totalAmount?.toFixed(2) || '0.00'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Advanced Options */}
|
|
<AdvancedOptionsSection
|
|
title="Advanced Options"
|
|
description="Optional fields for comprehensive order management"
|
|
>
|
|
{/* Pricing Details */}
|
|
<div className="space-y-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Pricing Details
|
|
</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Discount (%)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={orderData.discountPercentage}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, discountPercentage: e.target.value })}
|
|
placeholder="0"
|
|
step="0.01"
|
|
min="0"
|
|
max="100"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Delivery Fee (€)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={orderData.deliveryFee}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryFee: e.target.value })}
|
|
placeholder="0.00"
|
|
step="0.01"
|
|
min="0"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Production & Scheduling */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Production & Scheduling
|
|
</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Production Start Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={orderData.productionStartDate}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, productionStartDate: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Production Due Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={orderData.productionDueDate}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, productionDueDate: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Production Batch Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.productionBatchNumber}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, productionBatchNumber: e.target.value })}
|
|
placeholder="BATCH-001"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Delivery Time Window
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.deliveryTimeWindow}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryTimeWindow: e.target.value })}
|
|
placeholder="E.g., 9:00 AM - 11:00 AM"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Production Notes
|
|
</label>
|
|
<textarea
|
|
value={orderData.productionNotes}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, productionNotes: e.target.value })}
|
|
placeholder="Special production requirements or notes"
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Fulfillment & Tracking */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Fulfillment & Tracking
|
|
</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Shipping Tracking Number
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.shippingTrackingNumber}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, shippingTrackingNumber: e.target.value })}
|
|
placeholder="Tracking number"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Shipping Carrier
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.shippingCarrier}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, shippingCarrier: e.target.value })}
|
|
placeholder="E.g., DHL, UPS, FedEx"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Pickup Location
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.pickupLocation}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, pickupLocation: e.target.value })}
|
|
placeholder="Store location for pickup"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Actual Delivery Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={orderData.actualDeliveryDate}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, actualDeliveryDate: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Source & Channel */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Source & Channel
|
|
</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Order Source
|
|
</label>
|
|
<select
|
|
value={orderData.orderSource}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, orderSource: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="manual">Manual</option>
|
|
<option value="phone">Phone</option>
|
|
<option value="email">Email</option>
|
|
<option value="website">Website</option>
|
|
<option value="app">Mobile App</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Sales Channel
|
|
</label>
|
|
<select
|
|
value={orderData.salesChannel}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, salesChannel: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="direct">Direct</option>
|
|
<option value="wholesale">Wholesale</option>
|
|
<option value="retail">Retail</option>
|
|
<option value="online">Online</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Sales Representative ID
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.salesRepId}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, salesRepId: e.target.value })}
|
|
placeholder="Sales rep ID or name"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Communication & Notes */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Communication & Notes
|
|
</h5>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Customer Purchase Order #
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.customerPurchaseOrder}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, customerPurchaseOrder: e.target.value })}
|
|
placeholder="Customer's PO number"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Delivery Instructions
|
|
</label>
|
|
<textarea
|
|
value={orderData.deliveryInstructions}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, deliveryInstructions: e.target.value })}
|
|
placeholder="Special delivery instructions"
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Special Instructions
|
|
</label>
|
|
<textarea
|
|
value={orderData.specialInstructions}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, specialInstructions: e.target.value })}
|
|
placeholder="Any special requirements or instructions"
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Internal Notes
|
|
</label>
|
|
<textarea
|
|
value={orderData.internalNotes}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, internalNotes: e.target.value })}
|
|
placeholder="Internal notes (not visible to customer)"
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Customer Notes
|
|
</label>
|
|
<textarea
|
|
value={orderData.customerNotes}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotes: e.target.value })}
|
|
placeholder="Notes from/for the customer"
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notifications */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Notifications
|
|
</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={orderData.notifyCustomerOnStatusChange}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnStatusChange: e.target.checked })}
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
/>
|
|
<label className="text-sm text-[var(--text-secondary)]">
|
|
Notify on Status Change
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={orderData.notifyCustomerOnDelivery}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, notifyCustomerOnDelivery: e.target.checked })}
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
/>
|
|
<label className="text-sm text-[var(--text-secondary)]">
|
|
Notify on Delivery
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Notification Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={orderData.customerNotificationEmail}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationEmail: e.target.value })}
|
|
placeholder="customer@email.com"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Notification Phone
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={orderData.customerNotificationPhone}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, customerNotificationPhone: e.target.value })}
|
|
placeholder="+34 123 456 789"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quality & Requirements */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Quality & Requirements
|
|
</h5>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={orderData.qualityCheckRequired}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckRequired: e.target.checked })}
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
/>
|
|
<label className="text-sm text-[var(--text-secondary)]">
|
|
Quality Check Required
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Quality Check Status
|
|
</label>
|
|
<select
|
|
value={orderData.qualityCheckStatus}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, qualityCheckStatus: e.target.value })}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
>
|
|
<option value="">Not Started</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="passed">Passed</option>
|
|
<option value="failed">Failed</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Packaging Instructions
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.packagingInstructions}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, packagingInstructions: e.target.value })}
|
|
placeholder="Special packaging requirements"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Labeling Requirements
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.labelingRequirements}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, labelingRequirements: e.target.value })}
|
|
placeholder="Custom label requirements"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Additional Options */}
|
|
<div className="space-y-4 border-t border-[var(--border-primary)] pt-4">
|
|
<h5 className="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider">
|
|
Additional Options
|
|
</h5>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={orderData.isRecurring}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, isRecurring: e.target.checked })}
|
|
className="rounded border-[var(--border-secondary)] text-[var(--color-primary)] focus:ring-[var(--color-primary)]"
|
|
/>
|
|
<label className="text-sm text-[var(--text-secondary)]">
|
|
Recurring Order
|
|
</label>
|
|
</div>
|
|
|
|
{orderData.isRecurring && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Recurring Schedule
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.recurringSchedule}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, recurringSchedule: e.target.value })}
|
|
placeholder="E.g., Weekly on Mondays, Every 2 weeks"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Tags
|
|
<Tooltip content="Comma-separated tags for easier search and filtering">
|
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
</Tooltip>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={orderData.tags}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, tags: e.target.value })}
|
|
placeholder="urgent, vip, wholesale"
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
|
Metadata (JSON)
|
|
<Tooltip content="Additional custom data in JSON format">
|
|
<Info className="inline w-4 h-4 ml-1 text-[var(--text-tertiary)]" />
|
|
</Tooltip>
|
|
</label>
|
|
<textarea
|
|
value={orderData.metadata}
|
|
onChange={(e) => handleOrderDataChange({ ...orderData, metadata: e.target.value })}
|
|
placeholder='{"custom_field": "value"}'
|
|
rows={2}
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] font-mono text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdvancedOptionsSection>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const CustomerOrderWizardSteps = (
|
|
data: Record<string, any>,
|
|
setData: (data: Record<string, any>) => void
|
|
): WizardStep[] => [
|
|
{
|
|
id: 'customer-selection',
|
|
title: 'Select Customer',
|
|
component: (props) => <CustomerSelectionStep {...props} data={data} onDataChange={setData} />,
|
|
validate: () => {
|
|
return !!(data.customer || (data.showNewCustomerForm && data.newCustomerName && data.newCustomerPhone));
|
|
},
|
|
},
|
|
{
|
|
id: 'order-items',
|
|
title: 'Order Items',
|
|
component: (props) => <OrderItemsStep {...props} data={data} onDataChange={setData} />,
|
|
validate: () => {
|
|
return !!(data.orderItems && data.orderItems.length > 0);
|
|
},
|
|
},
|
|
{
|
|
id: 'delivery-payment',
|
|
title: 'Delivery & Payment',
|
|
component: (props) => <DeliveryPaymentStep {...props} data={data} onDataChange={setData} />,
|
|
validate: () => {
|
|
return !!(
|
|
data.requestedDeliveryDate &&
|
|
(data.deliveryMethod === 'pickup' || data.deliveryAddress)
|
|
);
|
|
},
|
|
},
|
|
];
|