Files
bakery-ia/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx
2025-11-18 07:17:17 +01:00

532 lines
26 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Building2, CheckCircle2, Loader2 } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store';
import { suppliersService } from '../../../../api/services/suppliers';
import { showToast } from '../../../../utils/toast';
import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
const SupplierDetailsStep: React.FC<WizardStepProps> = ({ dataRef, onDataChange, onComplete }) => {
const { t } = useTranslation('wizards');
// New architecture: access data from dataRef.current
const data = dataRef?.current || {};
const { currentTenant } = useTenant();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleFieldChange = (field: string, value: any) => {
onDataChange?.({ ...data, [field]: value });
};
useEffect(() => {
if (!data.supplierCode && data.name) {
const code = `SUP-${data.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
onDataChange?.({ ...data, supplierCode: code });
}
}, [data.name]);
const handleCreateSupplier = async () => {
const i18next = (await import('i18next')).default;
if (!currentTenant?.id) {
const errorMsg = i18next.t('wizards:supplier.messages.errorObtainingTenantInfo');
setError(errorMsg);
return;
}
setLoading(true);
setError(null);
try {
const payload = {
name: data.name,
supplier_type: data.supplierType,
status: data.status,
payment_terms: data.paymentTerms,
currency: data.currency,
standard_lead_time: data.standardLeadTime,
supplier_code: data.supplierCode || undefined,
tax_id: data.taxId || undefined,
registration_number: data.registrationNumber || undefined,
contact_person: data.contactPerson || undefined,
email: data.email || undefined,
phone: data.phone || undefined,
mobile: data.mobile || undefined,
website: data.website || undefined,
address_line1: data.addressLine1 || undefined,
address_line2: data.addressLine2 || undefined,
city: data.city || undefined,
state_province: data.stateProvince || undefined,
postal_code: data.postalCode || undefined,
country: data.country || undefined,
credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined,
minimum_order_amount: data.minimumOrderAmount ? parseFloat(data.minimumOrderAmount) : undefined,
delivery_area: data.deliveryArea || undefined,
is_preferred_supplier: data.isPreferredSupplier,
auto_approve_enabled: data.autoApproveEnabled,
notes: data.notes || undefined,
certifications: data.certifications ? JSON.parse(`{"items": ${JSON.stringify(data.certifications.split(',').map(c => c.trim()))}}`) : undefined,
specializations: data.specializations ? JSON.parse(`{"items": ${JSON.stringify(data.specializations.split(',').map(s => s.trim()))}}`) : undefined,
created_by: currentTenant.id,
updated_by: currentTenant.id,
};
await suppliersService.createSupplier(currentTenant.id, payload);
showToast.success(i18next.t('wizards:supplier.messages.supplierCreatedSuccessfully'));
// Let the wizard handle completion via the Next/Complete button
} catch (err: any) {
console.error('Error creating supplier:', err);
const errorMessage = err.response?.data?.detail || i18next.t('wizards:supplier.messages.errorCreatingSupplier');
setError(errorMessage);
showToast.error(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Building2 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">{t('supplier.supplierDetails')}</h3>
<p className="text-sm text-[var(--text-secondary)]">{t('supplier.subtitle')}</p>
</div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
{/* Required Fields */}
<div className="space-y-4">
<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">
{t('supplier.fields.name')} *
</label>
<input
type="text"
value={data.name}
onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder={t('supplier.fields.namePlaceholder')}
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 inline-flex items-center gap-2">
{t('supplier.fields.supplierType')} *
<Tooltip content={t('supplier.fields.supplierTypeTooltip')}>
<span />
</Tooltip>
</label>
<select
value={data.supplierType}
onChange={(e) => handleFieldChange('supplierType', 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="ingredients">{t('supplier.supplierTypes.ingredients')}</option>
<option value="packaging">{t('supplier.supplierTypes.packaging')}</option>
<option value="equipment">{t('supplier.supplierTypes.equipment')}</option>
<option value="services">{t('supplier.supplierTypes.services')}</option>
<option value="utilities">{t('supplier.supplierTypes.utilities')}</option>
<option value="multi">{t('supplier.supplierTypes.multi')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('supplier.fields.status')} *
</label>
<select
value={data.status}
onChange={(e) => handleFieldChange('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="active">{t('supplier.statuses.active')}</option>
<option value="inactive">{t('supplier.statuses.inactive')}</option>
<option value="pending_approval">{t('supplier.statuses.pending_approval')}</option>
<option value="suspended">{t('supplier.statuses.suspended')}</option>
<option value="blacklisted">{t('supplier.statuses.blacklisted')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('supplier.fields.paymentTerms')} *
</label>
<select
value={data.paymentTerms}
onChange={(e) => handleFieldChange('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="cod">{t('supplier.paymentTerms.cod')}</option>
<option value="net_15">{t('supplier.paymentTerms.net_15')}</option>
<option value="net_30">{t('supplier.paymentTerms.net_30')}</option>
<option value="net_45">{t('supplier.paymentTerms.net_45')}</option>
<option value="net_60">{t('supplier.paymentTerms.net_60')}</option>
<option value="prepaid">{t('supplier.paymentTerms.prepaid')}</option>
<option value="credit_terms">{t('supplier.paymentTerms.credit_terms')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('supplier.fields.currency')} *
</label>
<input
type="text"
value={data.currency}
onChange={(e) => handleFieldChange('currency', e.target.value)}
placeholder={t('supplier.fields.currencyPlaceholder')}
maxLength={3}
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 inline-flex items-center gap-2">
{t('supplier.fields.leadTime')} *
<Tooltip content={t('supplier.fields.leadTimeTooltip')}>
<span />
</Tooltip>
</label>
<input
type="number"
value={data.standardLeadTime}
onChange={(e) => handleFieldChange('standardLeadTime', parseInt(e.target.value) || 0)}
placeholder="3"
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 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">
{t('supplier.fields.contactPerson')}
</label>
<input
type="text"
value={data.contactPerson}
onChange={(e) => handleFieldChange('contactPerson', e.target.value)}
placeholder={t('supplier.fields.contactPersonPlaceholder')}
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">
{t('supplier.fields.email')}
</label>
<input
type="email"
value={data.email}
onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder={t('supplier.fields.emailPlaceholder')}
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">
{t('supplier.fields.phone')}
</label>
<input
type="tel"
value={data.phone}
onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder={t('supplier.fields.phonePlaceholder')}
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>
{/* Advanced Options */}
<AdvancedOptionsSection
title={t('supplier.advancedOptionsTitle')}
description={t('supplier.advancedOptionsDescription')}
>
<div className="space-y-4">
<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">
{t('supplier.fields.supplierCode')}
</label>
<input
type="text"
value={data.supplierCode}
onChange={(e) => handleFieldChange('supplierCode', e.target.value)}
placeholder={t('supplier.fields.supplierCodePlaceholder')}
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">
{t('supplier.fields.mobile')}
</label>
<input
type="tel"
value={data.mobile}
onChange={(e) => handleFieldChange('mobile', e.target.value)}
placeholder={t('supplier.fields.mobilePlaceholder')}
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">
{t('supplier.fields.taxId')}
</label>
<input
type="text"
value={data.taxId}
onChange={(e) => handleFieldChange('taxId', e.target.value)}
placeholder={t('supplier.fields.taxIdPlaceholder')}
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">
{t('supplier.fields.registrationNumber')}
</label>
<input
type="text"
value={data.registrationNumber}
onChange={(e) => handleFieldChange('registrationNumber', e.target.value)}
placeholder={t('supplier.fields.registrationNumberPlaceholder')}
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">
{t('supplier.fields.website')}
</label>
<input
type="url"
value={data.website}
onChange={(e) => handleFieldChange('website', e.target.value)}
placeholder={t('supplier.fields.websitePlaceholder')}
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">
{t('supplier.fields.addressLine1')}
</label>
<input
type="text"
value={data.addressLine1}
onChange={(e) => handleFieldChange('addressLine1', e.target.value)}
placeholder={t('supplier.fields.addressLine1Placeholder')}
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">
{t('supplier.fields.addressLine2')}
</label>
<input
type="text"
value={data.addressLine2}
onChange={(e) => handleFieldChange('addressLine2', e.target.value)}
placeholder={t('supplier.fields.addressLine2Placeholder')}
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">
{t('supplier.fields.city')}
</label>
<input
type="text"
value={data.city}
onChange={(e) => handleFieldChange('city', e.target.value)}
placeholder={t('supplier.fields.cityPlaceholder')}
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">
{t('supplier.fields.state')}
</label>
<input
type="text"
value={data.stateProvince}
onChange={(e) => handleFieldChange('stateProvince', e.target.value)}
placeholder={t('supplier.fields.statePlaceholder')}
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">
{t('supplier.fields.postalCode')}
</label>
<input
type="text"
value={data.postalCode}
onChange={(e) => handleFieldChange('postalCode', e.target.value)}
placeholder={t('supplier.fields.postalCodePlaceholder')}
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">
{t('supplier.fields.country')}
</label>
<input
type="text"
value={data.country}
onChange={(e) => handleFieldChange('country', e.target.value)}
placeholder={t('supplier.fields.countryPlaceholder')}
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">
{t('supplier.fields.creditLimit')}
</label>
<input
type="number"
value={data.creditLimit}
onChange={(e) => handleFieldChange('creditLimit', e.target.value)}
placeholder={t('supplier.fields.creditLimitPlaceholder')}
min="0"
step="0.01"
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">
{t('supplier.fields.minOrderAmount')}
</label>
<input
type="number"
value={data.minimumOrderAmount}
onChange={(e) => handleFieldChange('minimumOrderAmount', e.target.value)}
placeholder={t('supplier.fields.minOrderAmountPlaceholder')}
min="0"
step="0.01"
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">
{t('supplier.fields.deliveryArea')}
</label>
<input
type="text"
value={data.deliveryArea}
onChange={(e) => handleFieldChange('deliveryArea', e.target.value)}
placeholder={t('supplier.fields.deliveryAreaPlaceholder')}
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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="isPreferredSupplier"
checked={data.isPreferredSupplier}
onChange={(e) => handleFieldChange('isPreferredSupplier', e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
{t('supplier.fields.preferredSupplier')}
</label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="autoApproveEnabled"
checked={data.autoApproveEnabled}
onChange={(e) => handleFieldChange('autoApproveEnabled', e.target.checked)}
className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
/>
<label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]">
{t('supplier.fields.autoApproveOrders')}
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('supplier.fields.certifications')}
</label>
<input
type="text"
value={data.certifications}
onChange={(e) => handleFieldChange('certifications', e.target.value)}
placeholder={t('supplier.fields.certificationsPlaceholder')}
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">
{t('supplier.fields.specializations')}
</label>
<input
type="text"
value={data.specializations}
onChange={(e) => handleFieldChange('specializations', e.target.value)}
placeholder={t('supplier.fields.specializationsPlaceholder')}
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">
{t('supplier.fields.notes')}
</label>
<textarea
value={data.notes}
onChange={(e) => handleFieldChange('notes', e.target.value)}
placeholder={t('supplier.fields.notesPlaceholder')}
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)]"
rows={3}
/>
</div>
</div>
</AdvancedOptionsSection>
</div>
);
};
export const SupplierWizardSteps = (
dataRef: React.MutableRefObject<Record<string, any>>,
setData: (data: Record<string, any>) => void
): WizardStep[] => {
// New architecture: return direct component reference instead of arrow function
// dataRef and onDataChange are now passed through WizardModal props
return [
{
id: 'supplier-details',
title: 'wizards:supplier.steps.supplierDetails',
description: 'wizards:supplier.steps.supplierDetailsDescription',
component: SupplierDetailsStep,
},
];
};