feat(F-113): completed feature

This commit is contained in:
chattie
2026-08-21 12:27:13 +02:00
parent 027cacd871
commit c05c0b0582
25 changed files with 581 additions and 74 deletions

View File

@@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import type { Order, OrderState, Product, OrderHistoryEvent } from '@/types';
import { ordersApi, productsApi } from '@/lib/api-client';
import { ordersApi, productsApi, settingsApi } from '@/lib/api-client';
const STATE_LABELS: Record<OrderState, string> = {
PENDING: 'Pendiente',
@@ -74,6 +74,8 @@ export default function OrderDetailPage() {
const [showConfirm, setShowConfirm] = useState<OrderState | null>(null);
const [confirmReason, setConfirmReason] = useState('');
const [trackingInput, setTrackingInput] = useState('');
const [courierInput, setCourierInput] = useState('');
const [couriers, setCouriers] = useState<string[]>([]);
const [notification, setNotification] = useState<{ ok: boolean; message: string } | null>(null);
// Edición de artículos
@@ -90,6 +92,7 @@ export default function OrderDetailPage() {
// Envío e historial (F-111)
const [history, setHistory] = useState<OrderHistoryEvent[]>([]);
const [shippingTracking, setShippingTracking] = useState('');
const [shippingCourier, setShippingCourier] = useState('');
const [shippingNote, setShippingNote] = useState('');
const [savingShipping, setSavingShipping] = useState(false);
const [shippingMsg, setShippingMsg] = useState<{ ok: boolean; text: string } | null>(null);
@@ -110,6 +113,7 @@ export default function OrderDetailPage() {
const data = await ordersApi.get(id);
setOrder(data);
setShippingTracking(data.trackingNumber ?? '');
setShippingCourier(data.courier ?? '');
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar');
} finally {
@@ -119,16 +123,30 @@ export default function OrderDetailPage() {
useEffect(() => { load(); loadHistory(); }, [load, loadHistory]);
useEffect(() => {
settingsApi
.get()
.then((settings) => setCouriers(settings.couriers ?? []))
.catch(() => setCouriers([]));
}, []);
const handleTransition = async (nextState: OrderState) => {
if (nextState === 'SHIPPED' && !trackingInput.trim()) return;
if (nextState === 'SHIPPED' && (!trackingInput.trim() || !courierInput.trim())) return;
setTransitioning(true);
try {
const updated = await ordersApi.transition(id, nextState, trackingInput);
const updated = await ordersApi.transition(
id,
nextState,
nextState === 'SHIPPED' ? trackingInput : undefined,
nextState === 'SHIPPED' ? courierInput : undefined,
);
setOrder(updated);
setShippingTracking(updated.trackingNumber ?? '');
setShippingCourier(updated.courier ?? '');
setShowConfirm(null);
setConfirmReason('');
setTrackingInput('');
setCourierInput('');
loadHistory();
if (updated.notified) {
setNotification({ ok: true, message: 'Cliente notificado por email.' });
@@ -256,9 +274,10 @@ export default function OrderDetailPage() {
setSavingShipping(true);
setShippingMsg(null);
try {
const updated = await ordersApi.updateShipping(id, tracking, shippingNote);
const updated = await ordersApi.updateShipping(id, tracking, shippingNote, shippingCourier);
setOrder(updated);
setShippingTracking(updated.trackingNumber ?? tracking);
setShippingCourier(updated.courier ?? '');
setShippingNote('');
setShippingMsg({ ok: true, text: 'Envío actualizado y registrado en el historial' });
loadHistory();
@@ -342,7 +361,11 @@ export default function OrderDetailPage() {
{allowed.map((next) => (
<button
key={next}
onClick={() => { setShowConfirm(next); setNotification(null); }}
onClick={() => {
setShowConfirm(next);
setNotification(null);
if (next === 'SHIPPED') setCourierInput((prev) => prev || couriers[0] || '');
}}
disabled={transitioning}
className="px-4 py-2 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors"
>
@@ -492,6 +515,22 @@ export default function OrderDetailPage() {
{/* Envío */}
<div className="bg-white border border-gray-200 rounded-xl p-6">
<h2 className="font-bold text-gray-900 mb-4">Envío</h2>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">
Transportista
</label>
<select
value={shippingCourier}
onChange={(e) => setShippingCourier(e.target.value)}
className="mb-2 w-full px-3 py-2 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none bg-white"
>
<option value="">Sin transportista</option>
{couriers.map((courier) => (
<option key={courier} value={courier}>{courier}</option>
))}
{shippingCourier && !couriers.includes(shippingCourier) && (
<option value={shippingCourier}>{shippingCourier}</option>
)}
</select>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">
Número de seguimiento
</label>
@@ -600,6 +639,24 @@ export default function OrderDetailPage() {
</p>
{showConfirm === 'SHIPPED' && (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
Transportista <span className="text-red-500">*</span>
</label>
<select
value={courierInput}
onChange={(e) => setCourierInput(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none bg-white mb-3"
>
<option value="">Selecciona transportista</option>
{couriers.map((courier) => (
<option key={courier} value={courier}>{courier}</option>
))}
</select>
{couriers.length === 0 && (
<p className="text-xs text-amber-600 mb-2">
No hay transportistas configurados. Añádelos en Ajustes Transportistas.
</p>
)}
<label className="block text-sm font-medium text-gray-700 mb-1">
Número de seguimiento <span className="text-red-500">*</span>
</label>
@@ -624,14 +681,14 @@ export default function OrderDetailPage() {
)}
<div className="flex gap-3 justify-end">
<button
onClick={() => { setShowConfirm(null); setConfirmReason(''); setTrackingInput(''); }}
onClick={() => { setShowConfirm(null); setConfirmReason(''); setTrackingInput(''); setCourierInput(''); }}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900 transition-colors"
>
Cancelar
</button>
<button
onClick={() => handleTransition(showConfirm)}
disabled={transitioning || (showConfirm === 'SHIPPED' && !trackingInput.trim())}
disabled={transitioning || (showConfirm === 'SHIPPED' && (!trackingInput.trim() || !courierInput.trim()))}
className="px-4 py-2 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-colors"
>
{transitioning ? 'Guardando...' : 'Confirmar'}