// ReturnWizard («Devolver mercancía») — un solo componente para POS, Pedido, // Factura, RMA y OT (benchmark §7, DEV-2). No conoce endpoints: recibe los // renglones devolvibles y entrega un payload por `onSubmit`. import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, Input } from '@asteby/metacore-ui' import { useFormatter } from './format' import { RefundDestination } from './refund-destination' import { DEFAULT_REFUND_DESTINATIONS, type RefundAllocation, type RefundDestinationOption, } from './refund' import { RETURN_CONDITION_DESTINATIONS, computeReturnTotals, creditNoteRelation, defaultStockDestination, returnSteps, returnableQty, serializeReturn, validateReturnChoices, validateReturnRefund, type ReturnChoices, type ReturnCondition, type ReturnStep, type ReturnStockDestination, type ReturnSubmitPayload, type ReturnableLine, } from './return' export interface ReturnWizardProps { lines: ReturnableLine[] /** ¿Hay un addon de almacén que confirma la recepción? Sin él, «Recibir» es un paso. */ warehouseConnected?: boolean /** La venta está facturada: habilita la NC CFDI con relación 01/03 automática. */ invoiced?: boolean /** Destinos de reembolso; `credit_note` se deshabilita solo si no está facturada. */ refundOptions?: RefundDestinationOption[] reasons?: { value: string; label: string }[] currency?: string submitting?: boolean /** Errores del servidor (`"."`). */ serverErrors?: Record onSubmit: (payload: ReturnSubmitPayload) => void | Promise onCancel?: () => void } const DEFAULT_REASONS = [ { value: 'not_wanted', label: 'No lo quiso' }, { value: 'defective', label: 'Defectuoso' }, { value: 'wrong_item', label: 'Producto equivocado' }, { value: 'warranty', label: 'Garantía' }, ] const STOCK_LABEL: Record = { stock: 'Regresa a stock', scrap: 'Merma', warranty: 'Garantía con proveedor' } const STEP_LABEL: Record = { lines: 'Renglones', condition: 'Condición', receive: 'Recibir', refund: 'Reembolso' } export function ReturnWizard({ lines, warehouseConnected = false, invoiced = false, refundOptions = DEFAULT_REFUND_DESTINATIONS, reasons = DEFAULT_REASONS, currency, submitting, serverErrors, onSubmit, onCancel, }: ReturnWizardProps) { const { t } = useTranslation() const fmt = useFormatter({ currency }) const steps = useMemo(() => returnSteps({ warehouseConnected }), [warehouseConnected]) const [stepIdx, setStepIdx] = useState(0) const [choices, setChoices] = useState({}) const [reason, setReason] = useState('') const [notes, setNotes] = useState('') const [received, setReceived] = useState(false) const [refund, setRefund] = useState([]) const step = steps[stepIdx] const totals = useMemo(() => computeReturnTotals(lines, choices), [lines, choices]) const validation = useMemo(() => validateReturnChoices(lines, choices, reason), [lines, choices, reason]) const relation = useMemo(() => creditNoteRelation(lines, choices), [lines, choices]) const options = useMemo( () => refundOptions.map((o) => o.kind === 'credit_note' && !invoiced ? { ...o, available: false, disabledReason: 'La venta no está facturada' } : o.kind === 'credit_note' ? { ...o, label: `${o.label} · relación ${relation}` } : o, ), [refundOptions, invoiced, relation], ) const refundValidation = useMemo(() => validateReturnRefund(totals.total, refund, options), [totals.total, refund, options]) const errors = { ...validation.errors, ...serverErrors } const setChoice = (key: string, patch: Partial) => setChoices((prev) => { const cur = prev[key] ?? { quantity: 0, condition: 'sellable' as ReturnCondition, destination: 'stock' as ReturnStockDestination } const next = { ...cur, ...patch } if (patch.condition && !patch.destination) next.destination = defaultStockDestination(patch.condition) return { ...prev, [key]: next } }) const canNext = step === 'lines' ? validation.valid : step === 'condition' ? validation.valid : step === 'receive' ? received : refundValidation.valid const submit = () => onSubmit(serializeReturn({ lines, choices, reason, notes, received: warehouseConnected ? false : received, refund })) return (
    {steps.map((s, i) => (
  1. {i + 1}. {t(`returns.step.${s}`, { defaultValue: STEP_LABEL[s] })}
  2. ))} {warehouseConnected && (
  3. {t('returns.step.warehouse', { defaultValue: 'Almacén confirma la recepción' })}
  4. )}
{(step === 'lines' || step === 'condition') && (
{lines.map((l) => { const c = choices[l.key] const max = returnableQty(l) return (

{l.description}

{t('returns.sold', { defaultValue: 'Vendido' })}: {l.sold} · {t('returns.returnable', { defaultValue: 'Por devolver' })}: {max} · {fmt.money(l.unit_price)}

{step === 'lines' ? (
setChoice(l.key, { quantity: Number(e.target.value.replace(',', '.')) || 0 })} /> {errors[`${l.key}.quantity`] &&

{errors[`${l.key}.quantity`]}

}
) : ( (c?.quantity ?? 0) > 0 && (
{(['sellable', 'defective'] as ReturnCondition[]).map((cond) => ( ))} {RETURN_CONDITION_DESTINATIONS[c!.condition].length > 1 && RETURN_CONDITION_DESTINATIONS[c!.condition].map((d) => ( ))} {RETURN_CONDITION_DESTINATIONS[c!.condition].length === 1 && ( {STOCK_LABEL[c!.destination]} )} {errors[`${l.key}.destination`] &&

{errors[`${l.key}.destination`]}

}
) )}
) })} {step === 'lines' && (
setNotes(e.target.value)} placeholder={t('returns.notes', { defaultValue: 'Notas (opcional)' })} />
)} {validation.form && stepIdx === 0 && Object.keys(choices).length > 0 &&

{validation.form}

}
)} {step === 'receive' && ( )} {step === 'refund' && ( )}
{t('returns.total', { defaultValue: 'Total a devolver' })}: {fmt.money(totals.total)}
{onCancel && stepIdx === 0 && } {stepIdx > 0 && } {stepIdx < steps.length - 1 ? ( ) : ( )}
) }