webmoney/frontend/src/components/business/ProductSheetModal.jsx

616 lines
26 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { productSheetService } from '../../services/api';
import useFormatters from '../../hooks/useFormatters';
const ProductSheetModal = ({ sheet, settings, onSave, onClose }) => {
const { t } = useTranslation();
const { currency } = useFormatters();
const isEditing = !!sheet;
const [formData, setFormData] = useState({
name: '',
sku: '',
description: '',
category: '',
currency: 'EUR',
business_setting_id: '',
is_active: true,
// Strategic pricing fields
competitor_price: '',
min_price: '',
max_price: '',
premium_multiplier: '1.00',
price_strategy: 'neutral',
psychological_rounding: false,
target_margin_percent: '',
});
const [items, setItems] = useState([]);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const [showStrategicPricing, setShowStrategicPricing] = useState(false);
const itemTypes = [
{ value: 'product_cost', label: t('business.products.itemTypes.productCost') },
{ value: 'packaging', label: t('business.products.itemTypes.packaging') },
{ value: 'label', label: t('business.products.itemTypes.label') },
{ value: 'shipping', label: t('business.products.itemTypes.shipping') },
{ value: 'handling', label: t('business.products.itemTypes.handling') },
{ value: 'other', label: t('business.products.itemTypes.other') },
];
useEffect(() => {
if (sheet) {
setFormData({
name: sheet.name || '',
sku: sheet.sku || '',
description: sheet.description || '',
category: sheet.category || '',
currency: sheet.currency || 'EUR',
business_setting_id: sheet.business_setting_id || '',
is_active: sheet.is_active ?? true,
// Strategic pricing fields
competitor_price: sheet.competitor_price || '',
min_price: sheet.min_price || '',
max_price: sheet.max_price || '',
premium_multiplier: sheet.premium_multiplier || '1.00',
price_strategy: sheet.price_strategy || 'neutral',
psychological_rounding: sheet.psychological_rounding ?? false,
target_margin_percent: sheet.target_margin_percent || '',
});
setItems(sheet.items?.map(item => ({
id: item.id,
name: item.name,
type: item.type,
amount: item.amount,
quantity: item.quantity || 1,
unit: item.unit || '',
})) || []);
// Show strategic pricing if any field is set
if (sheet.competitor_price || sheet.min_price || sheet.max_price ||
sheet.target_margin_percent || sheet.price_strategy !== 'neutral') {
setShowStrategicPricing(true);
}
}
}, [sheet]);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleItemChange = (index, field, value) => {
setItems(prev => prev.map((item, i) =>
i === index ? { ...item, [field]: value } : item
));
};
const addItem = () => {
setItems(prev => [...prev, {
name: '',
type: 'product_cost',
amount: '',
quantity: 1,
unit: '',
}]);
};
const removeItem = (index) => {
setItems(prev => prev.filter((_, i) => i !== index));
};
// Calcular CMV total
const calculateCmv = () => {
return items.reduce((sum, item) => {
const amount = parseFloat(item.amount) || 0;
const quantity = parseFloat(item.quantity) || 1;
return sum + (amount / quantity);
}, 0);
};
// Calcular preço de venda preview
const calculateSalePrice = () => {
if (!formData.business_setting_id) return null;
const setting = settings.find(s => s.id === parseInt(formData.business_setting_id));
if (!setting || !setting.markup_factor) return null;
return calculateCmv() * setting.markup_factor;
};
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
setError(null);
// Validar itens
const validItems = items.filter(item => item.name && item.amount);
try {
const dataToSend = {
...formData,
business_setting_id: formData.business_setting_id || null,
items: validItems.map(item => ({
name: item.name,
type: item.type,
amount: parseFloat(item.amount) || 0,
quantity: parseFloat(item.quantity) || 1,
unit: item.unit || null,
})),
};
let result;
if (isEditing) {
// Para edição, primeiro atualiza a ficha, depois gerencia itens
result = await productSheetService.update(sheet.id, {
name: formData.name,
sku: formData.sku,
description: formData.description,
category: formData.category,
currency: formData.currency,
business_setting_id: formData.business_setting_id || null,
is_active: formData.is_active,
// Strategic pricing fields
competitor_price: formData.competitor_price ? parseFloat(formData.competitor_price) : null,
min_price: formData.min_price ? parseFloat(formData.min_price) : null,
max_price: formData.max_price ? parseFloat(formData.max_price) : null,
premium_multiplier: parseFloat(formData.premium_multiplier) || 1.0,
price_strategy: formData.price_strategy,
psychological_rounding: formData.psychological_rounding,
target_margin_percent: formData.target_margin_percent ? parseFloat(formData.target_margin_percent) : null,
});
// Remover itens que não existem mais
const existingIds = items.filter(i => i.id).map(i => i.id);
for (const oldItem of sheet.items || []) {
if (!existingIds.includes(oldItem.id)) {
await productSheetService.removeItem(sheet.id, oldItem.id);
}
}
// Atualizar ou criar itens
for (const item of validItems) {
if (item.id) {
await productSheetService.updateItem(sheet.id, item.id, {
name: item.name,
type: item.type,
amount: parseFloat(item.amount) || 0,
quantity: parseFloat(item.quantity) || 1,
unit: item.unit || null,
});
} else {
await productSheetService.addItem(sheet.id, {
name: item.name,
type: item.type,
amount: parseFloat(item.amount) || 0,
quantity: parseFloat(item.quantity) || 1,
unit: item.unit || null,
});
}
}
// Recalcular preço
if (formData.business_setting_id) {
result = await productSheetService.recalculatePrice(sheet.id, formData.business_setting_id);
} else {
result = await productSheetService.getById(sheet.id);
}
} else {
result = await productSheetService.create(dataToSend);
}
onSave(result);
} catch (err) {
setError(err.response?.data?.message || err.response?.data?.errors || t('common.error'));
} finally {
setSaving(false);
}
};
const currencies = ['EUR', 'USD', 'BRL', 'GBP'];
const cmvTotal = calculateCmv();
const salePrice = calculateSalePrice();
return (
<div className="modal show d-block" style={{ background: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" style={{ maxHeight: '90vh' }}>
<div className="modal-content" style={{ background: '#1e293b', border: 'none', maxHeight: '90vh' }}>
<div className="modal-header border-0">
<h5 className="modal-title text-white">
<i className="bi bi-box-seam me-2"></i>
{isEditing ? t('business.products.edit') : t('business.products.add')}
</h5>
<button type="button" className="btn-close btn-close-white" onClick={onClose}></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body" style={{ maxHeight: 'calc(90vh - 130px)', overflowY: 'auto' }}>
{error && (
<div className="alert alert-danger">{typeof error === 'string' ? error : JSON.stringify(error)}</div>
)}
<div className="row g-3">
{/* Info básica */}
<div className="col-md-8">
<label className="form-label text-slate-400">{t('business.products.name')}</label>
<input
type="text"
className="form-control bg-dark text-white border-secondary"
name="name"
value={formData.name}
onChange={handleChange}
placeholder={t('business.products.namePlaceholder')}
required
/>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">SKU</label>
<input
type="text"
className="form-control bg-dark text-white border-secondary"
name="sku"
value={formData.sku}
onChange={handleChange}
placeholder={t('business.products.skuPlaceholder')}
/>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">{t('common.currency')}</label>
<select
className="form-select bg-dark text-white border-secondary"
name="currency"
value={formData.currency}
onChange={handleChange}
required
>
{currencies.map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">{t('business.products.category')}</label>
<input
type="text"
className="form-control bg-dark text-white border-secondary"
name="category"
value={formData.category}
onChange={handleChange}
placeholder={t('business.products.categoryPlaceholder')}
/>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">{t('business.products.businessSetting')}</label>
<select
className="form-select bg-dark text-white border-secondary"
name="business_setting_id"
value={formData.business_setting_id}
onChange={handleChange}
>
<option value="">{t('business.products.noSetting')}</option>
{settings.filter(s => s.is_active).map(s => (
<option key={s.id} value={s.id}>
{s.name} (Markup: {parseFloat(s.markup_factor || 0).toFixed(2)})
</option>
))}
</select>
</div>
<div className="col-12">
<label className="form-label text-slate-400">{t('common.description')}</label>
<textarea
className="form-control bg-dark text-white border-secondary"
name="description"
value={formData.description}
onChange={handleChange}
rows="2"
/>
</div>
{/* Strategic Pricing Toggle */}
<div className="col-12">
<hr className="border-secondary my-2" />
<button
type="button"
className="btn btn-sm btn-outline-info w-100"
onClick={() => setShowStrategicPricing(!showStrategicPricing)}
>
<i className={`bi bi-chevron-${showStrategicPricing ? 'up' : 'down'} me-2`}></i>
{t('business.products.strategicPricing')}
<i className="bi bi-graph-up ms-2"></i>
</button>
</div>
{/* Strategic Pricing Fields */}
{showStrategicPricing && (
<>
<div className="col-md-4">
<label className="form-label text-slate-400">
<i className="bi bi-building me-1"></i>
{t('business.products.competitorPrice')}
</label>
<input
type="number"
step="0.01"
min="0"
className="form-control bg-dark text-white border-secondary"
name="competitor_price"
value={formData.competitor_price}
onChange={handleChange}
placeholder="0.00"
/>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">
<i className="bi bi-arrow-down me-1"></i>
{t('business.products.minPrice')}
</label>
<input
type="number"
step="0.01"
min="0"
className="form-control bg-dark text-white border-secondary"
name="min_price"
value={formData.min_price}
onChange={handleChange}
placeholder="0.00"
/>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">
<i className="bi bi-arrow-up me-1"></i>
{t('business.products.maxPrice')}
</label>
<input
type="number"
step="0.01"
min="0"
className="form-control bg-dark text-white border-secondary"
name="max_price"
value={formData.max_price}
onChange={handleChange}
placeholder="0.00"
/>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">
<i className="bi bi-star me-1"></i>
{t('business.products.premiumMultiplier')}
</label>
<input
type="number"
step="0.01"
min="0.1"
max="5"
className="form-control bg-dark text-white border-secondary"
name="premium_multiplier"
value={formData.premium_multiplier}
onChange={handleChange}
/>
<small className="text-slate-500">1.0 = {t('business.products.neutral')}</small>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">
<i className="bi bi-bullseye me-1"></i>
{t('business.products.priceStrategy')}
</label>
<select
className="form-select bg-dark text-white border-secondary"
name="price_strategy"
value={formData.price_strategy}
onChange={handleChange}
>
<option value="aggressive">{t('business.products.strategyAggressive')}</option>
<option value="neutral">{t('business.products.strategyNeutral')}</option>
<option value="premium">{t('business.products.strategyPremium')}</option>
</select>
</div>
<div className="col-md-4">
<label className="form-label text-slate-400">
<i className="bi bi-percent me-1"></i>
{t('business.products.targetMargin')}
</label>
<input
type="number"
step="0.1"
min="0"
max="99"
className="form-control bg-dark text-white border-secondary"
name="target_margin_percent"
value={formData.target_margin_percent}
onChange={handleChange}
placeholder={t('business.products.useGlobal')}
/>
</div>
<div className="col-12">
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="psychological_rounding"
name="psychological_rounding"
checked={formData.psychological_rounding}
onChange={handleChange}
/>
<label className="form-check-label text-slate-400" htmlFor="psychological_rounding">
<i className="bi bi-magic me-1"></i>
{t('business.products.psychologicalRounding')}
<small className="text-slate-500 ms-2">({t('business.products.psychologicalExample')})</small>
</label>
</div>
</div>
</>
)}
{/* Componentes de Custo (CMV) */}
<div className="col-12">
<hr className="border-secondary my-2" />
<div className="d-flex justify-content-between align-items-center mb-3">
<h6 className="text-slate-400 small mb-0">
<i className="bi bi-list-check me-2"></i>
{t('business.products.costComponents')}
</h6>
<button type="button" className="btn btn-sm btn-outline-primary" onClick={addItem}>
<i className="bi bi-plus-lg me-1"></i>
{t('business.products.addComponent')}
</button>
</div>
{items.length === 0 ? (
<div className="text-center py-4 text-slate-500">
<i className="bi bi-inbox fs-3 mb-2 d-block"></i>
{t('business.products.noComponents')}
</div>
) : (
<div className="table-responsive">
<table className="table table-dark table-sm mb-0">
<thead>
<tr>
<th className="text-slate-400" style={{ width: '30%' }}>{t('business.products.componentName')}</th>
<th className="text-slate-400" style={{ width: '20%' }}>{t('common.type')}</th>
<th className="text-slate-400 text-end" style={{ width: '20%' }}>{t('business.products.amount')}</th>
<th className="text-slate-400 text-end" style={{ width: '15%' }}>{t('business.products.quantity')}</th>
<th className="text-slate-400 text-end" style={{ width: '15%' }}>{t('business.products.unitCost')}</th>
<th style={{ width: '40px' }}></th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const unitCost = (parseFloat(item.amount) || 0) / (parseFloat(item.quantity) || 1);
return (
<tr key={index}>
<td>
<input
type="text"
className="form-control form-control-sm bg-dark text-white border-secondary"
value={item.name}
onChange={(e) => handleItemChange(index, 'name', e.target.value)}
placeholder={t('business.products.componentNamePlaceholder')}
/>
</td>
<td>
<select
className="form-select form-select-sm bg-dark text-white border-secondary"
value={item.type}
onChange={(e) => handleItemChange(index, 'type', e.target.value)}
>
{itemTypes.map(type => (
<option key={type.value} value={type.value}>{type.label}</option>
))}
</select>
</td>
<td>
<input
type="number"
step="0.01"
className="form-control form-control-sm bg-dark text-white border-secondary text-end"
value={item.amount}
onChange={(e) => handleItemChange(index, 'amount', e.target.value)}
placeholder="0.00"
/>
</td>
<td>
<input
type="number"
step="0.01"
className="form-control form-control-sm bg-dark text-white border-secondary text-end"
value={item.quantity}
onChange={(e) => handleItemChange(index, 'quantity', e.target.value)}
placeholder="1"
/>
</td>
<td className="text-end text-white align-middle">
{currency(unitCost, formData.currency)}
</td>
<td>
<button
type="button"
className="btn btn-sm btn-outline-danger border-0"
onClick={() => removeItem(index)}
>
<i className="bi bi-trash"></i>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
{/* Preview CMV e Preço de Venda */}
<div className="col-12">
<div className="row g-3 mt-2">
<div className="col-md-6">
<div className="p-3 rounded text-center" style={{ background: 'rgba(239, 68, 68, 0.1)' }}>
<small className="text-slate-500 d-block mb-1">{t('business.products.cmvTotalLabel')}</small>
<h4 className="text-danger mb-0">{currency(cmvTotal, formData.currency)}</h4>
</div>
</div>
<div className="col-md-6">
<div className="p-3 rounded text-center" style={{ background: salePrice ? 'rgba(16, 185, 129, 0.1)' : 'rgba(255,255,255,0.05)' }}>
<small className="text-slate-500 d-block mb-1">{t('business.products.salePrice')}</small>
<h4 className={salePrice ? 'text-success mb-0' : 'text-slate-500 mb-0'}>
{salePrice ? currency(salePrice, formData.currency) : '-'}
</h4>
{!formData.business_setting_id && (
<small className="text-slate-500">{t('business.products.selectSettingForPrice')}</small>
)}
</div>
</div>
</div>
</div>
{/* Status */}
{isEditing && (
<div className="col-12">
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="is_active"
name="is_active"
checked={formData.is_active}
onChange={handleChange}
/>
<label className="form-check-label text-slate-400" htmlFor="is_active">
{t('business.products.isActive')}
</label>
</div>
</div>
)}
</div>
</div>
<div className="modal-footer border-0">
<button type="button" className="btn btn-secondary" onClick={onClose}>
{t('common.cancel')}
</button>
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.saving')}
</>
) : (
<>
<i className="bi bi-check-lg me-2"></i>
{t('common.save')}
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default ProductSheetModal;