feat(F-080): completed feature

This commit is contained in:
chattie
2026-08-20 05:59:47 +02:00
parent 60206d88eb
commit 506dfd0cce
10 changed files with 209 additions and 42 deletions

View File

@@ -22,20 +22,17 @@ function calcMarginBruto(grossCents: number, costCents: number): number {
return Math.round(((grossCents - costCents) / grossCents) * 100);
}
interface PricingSectionProps {
productId: string;
}
export function PricingSection({ productId }: { productId: string }) {
const [variants, setVariants] = useState<ProductVariant[]>([]);
const [loadingVariants, setLoadingVariants] = useState(true);
const [loadingPrices, setLoadingPrices] = useState(true);
const [prices, setPrices] = useState<Record<string, VariantPrice>>({});
const [, setLoadingPrices] = useState(true);
const [, setPrices] = useState<Record<string, VariantPrice>>({});
const [saving, setSaving] = useState<string | null>(null);
const [msg, setMsg] = useState<Record<string, string>>({});
// Edit state per variant (valores en EUROS como string, p.ej. "12.00")
const [net, setNet] = useState<Record<string, string>>({});
const [gross, setGross] = useState<Record<string, string>>({});
const [offer, setOffer] = useState<Record<string, string>>({});
const [cost, setCost] = useState<Record<string, string>>({});
const [activeTaxRates, setActiveTaxRates] = useState<TaxRate[]>([]);
@@ -57,14 +54,19 @@ export function PricingSection({ productId }: { productId: string }) {
for (const v of variants) {
pricingApi.getVariantPrice(v.id)
.then((p) => {
const initialVr = p.vatRate;
const initialRateP = activeTaxRates.find(r => r.appliesTo === initialVr)?.ratePercent ?? 21;
const initialGrossCents = Math.round(p.netUnitAmountCents * (1 + initialRateP / 100));
setPrices(prev => ({ ...prev, [v.id]: p }));
setNet(prev => ({ ...prev, [v.id]: centsToEur(p.netUnitAmountCents) }));
setGross(prev => ({ ...prev, [v.id]: centsToEur(initialGrossCents) }));
setOffer(prev => ({ ...prev, [v.id]: p.offerCents !== null ? centsToEur(p.offerCents) : '' }));
setCost(prev => ({ ...prev, [v.id]: p.costCents !== null ? centsToEur(p.costCents) : '' }));
setVatRate(prev => ({ ...prev, [v.id]: p.vatRate }));
setVatRate(prev => ({ ...prev, [v.id]: initialVr }));
})
.catch(() => {
setNet(prev => ({ ...prev, [v.id]: '0.00' }));
setGross(prev => ({ ...prev, [v.id]: '0.00' }));
setOffer(prev => ({ ...prev, [v.id]: '' }));
setCost(prev => ({ ...prev, [v.id]: '' }));
setVatRate(prev => ({ ...prev, [v.id]: (activeTaxRates[0]?.appliesTo ?? 'general') as 'general' | 'reduced' | 'super-reduced' }));
@@ -84,7 +86,10 @@ export function PricingSection({ productId }: { productId: string }) {
}, []);
const savePrice = async (variantId: string) => {
const netCents = eurToCents(net[variantId] ?? '0');
const grossCents = eurToCents(gross[variantId] ?? '0');
const vr = vatRate[variantId] ?? 'general';
const rateP = activeTaxRates.find(r => r.appliesTo === vr)?.ratePercent ?? 21;
const netCents = Math.round(grossCents / (1 + rateP / 100));
const offerCentsVal = offer[variantId] ? eurToCents(offer[variantId]) : null;
const costCentsVal = cost[variantId] ? eurToCents(cost[variantId]) : null;
if (netCents < 0) return;
@@ -93,7 +98,7 @@ export function PricingSection({ productId }: { productId: string }) {
setSaving(variantId);
setMsg(prev => ({ ...prev, [variantId]: '' }));
try {
const updated = await pricingApi.setVariantPrice(variantId, netCents, vatRate[variantId], offerCentsVal, costCentsVal);
const updated = await pricingApi.setVariantPrice(variantId, netCents, vr, offerCentsVal, costCentsVal);
setPrices(prev => ({ ...prev, [variantId]: updated }));
setMsg(prev => ({ ...prev, [variantId]: '✓' }));
setTimeout(() => setMsg(prev => ({ ...prev, [variantId]: '' })), 3000);
@@ -137,8 +142,7 @@ export function PricingSection({ productId }: { productId: string }) {
const vr = vatRate[v.id] ?? 'general';
const activeRate = activeTaxRates.find(r => r.appliesTo === vr);
const ratePercent = activeRate?.ratePercent ?? 21;
const grossCents = Math.round(netCents * (1 + ratePercent / 100));
const grossEur = centsToEur(grossCents);
const grossCents = eurToCents(gross[v.id] ?? '0');
const marginBruto = calcMarginBruto(grossCents, costCents);
const editing = saving === v.id;
@@ -151,7 +155,7 @@ export function PricingSection({ productId }: { productId: string }) {
<div className="flex items-center gap-1">
<span className="text-gray-400 text-xs"></span>
<input
type="number" min={0} step="0.01" inputMode="decimal"
type="text" inputMode="decimal"
value={cost[v.id] ?? ''}
disabled={editing}
onChange={e => setCost(prev => ({ ...prev, [v.id]: e.target.value }))}
@@ -166,11 +170,13 @@ export function PricingSection({ productId }: { productId: string }) {
<div className="flex items-center gap-1">
<span className="text-gray-400 text-xs"></span>
<input
type="number" min={0} step="0.01" inputMode="decimal"
value={grossEur}
type="text" inputMode="decimal"
value={gross[v.id] ?? ''}
disabled={editing}
onChange={e => {
const grossInputCents = eurToCents(e.target.value);
const raw = e.target.value;
setGross(prev => ({ ...prev, [v.id]: raw }));
const grossInputCents = eurToCents(raw);
const newNetCents = Math.round(grossInputCents / (1 + ratePercent / 100));
setNet(prev => ({ ...prev, [v.id]: centsToEur(newNetCents) }));
}}
@@ -184,7 +190,7 @@ export function PricingSection({ productId }: { productId: string }) {
<div className="flex items-center gap-1">
<span className="text-gray-400 text-xs"></span>
<input
type="number" min={0} step="0.01" inputMode="decimal"
type="text" inputMode="decimal"
value={offer[v.id] ?? ''}
disabled={editing}
onChange={e => setOffer(prev => ({ ...prev, [v.id]: e.target.value }))}
@@ -199,7 +205,13 @@ export function PricingSection({ productId }: { productId: string }) {
<select
value={vatRate[v.id] ?? 'general'}
disabled={editing}
onChange={e => setVatRate(prev => ({ ...prev, [v.id]: e.target.value as 'general' | 'reduced' | 'super-reduced' }))}
onChange={e => {
const newVr = e.target.value as 'general' | 'reduced' | 'super-reduced';
setVatRate(prev => ({ ...prev, [v.id]: newVr }));
const newRateP = activeTaxRates.find(r => r.appliesTo === newVr)?.ratePercent ?? 21;
const newGrossCents = Math.round(netCents * (1 + newRateP / 100));
setGross(prev => ({ ...prev, [v.id]: centsToEur(newGrossCents) }));
}}
className="px-2 py-1 border border-gray-300 rounded-lg text-xs focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
>
{activeTaxRates.length === 0 && (
@@ -232,10 +244,16 @@ export function PricingSection({ productId }: { productId: string }) {
<div className="flex items-center gap-1">
<span className="text-gray-400 text-xs"></span>
<input
type="number" min={0} step="0.01" inputMode="decimal"
type="text" inputMode="decimal"
value={net[v.id] ?? ''}
disabled={editing}
onChange={e => setNet(prev => ({ ...prev, [v.id]: e.target.value }))}
onChange={e => {
const raw = e.target.value;
setNet(prev => ({ ...prev, [v.id]: raw }));
const netCentsIn = eurToCents(raw);
const newGrossCents = Math.round(netCentsIn * (1 + ratePercent / 100));
setGross(prev => ({ ...prev, [v.id]: centsToEur(newGrossCents) }));
}}
className="w-24 px-2 py-1 border border-gray-300 rounded-lg text-xs focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
/>
</div>

File diff suppressed because one or more lines are too long