326 lines
13 KiB
TypeScript
326 lines
13 KiB
TypeScript
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import {
|
||
|
|
shippingService,
|
||
|
|
DeliveryScheduleItem,
|
||
|
|
} from "../src/services/shipping.service";
|
||
|
|
|
||
|
|
export interface DeliveryDay {
|
||
|
|
date: string;
|
||
|
|
day: string;
|
||
|
|
cap: number;
|
||
|
|
sales: number;
|
||
|
|
capDisp: number;
|
||
|
|
available: "Sim" | "Não";
|
||
|
|
}
|
||
|
|
|
||
|
|
interface BaldinhoProps {
|
||
|
|
selectedDeliveryDate: string;
|
||
|
|
onDateChange: (date: string) => void;
|
||
|
|
deliveryDays?: DeliveryDay[]; // Opcional agora, pois será carregado dinamicamente
|
||
|
|
}
|
||
|
|
|
||
|
|
const Baldinho: React.FC<BaldinhoProps> = ({
|
||
|
|
selectedDeliveryDate,
|
||
|
|
onDateChange,
|
||
|
|
deliveryDays: deliveryDaysProp,
|
||
|
|
}) => {
|
||
|
|
const [deliveryDays, setDeliveryDays] = useState<DeliveryDay[]>(
|
||
|
|
deliveryDaysProp || []
|
||
|
|
);
|
||
|
|
const [loading, setLoading] = useState<boolean>(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Converte um item da API para o formato do componente
|
||
|
|
*/
|
||
|
|
const mapApiItemToDeliveryDay = (item: DeliveryScheduleItem): DeliveryDay => {
|
||
|
|
const date = new Date(item.dateDelivery);
|
||
|
|
const dayNames = [
|
||
|
|
"domingo",
|
||
|
|
"segunda-feira",
|
||
|
|
"terça-feira",
|
||
|
|
"quarta-feira",
|
||
|
|
"quinta-feira",
|
||
|
|
"sexta-feira",
|
||
|
|
"sábado",
|
||
|
|
];
|
||
|
|
const dayName = dayNames[date.getUTCDay()];
|
||
|
|
|
||
|
|
// Formatar data como DD/MM/YYYY
|
||
|
|
const formattedDate = date.toLocaleDateString("pt-BR", {
|
||
|
|
day: "2-digit",
|
||
|
|
month: "2-digit",
|
||
|
|
year: "numeric",
|
||
|
|
timeZone: "UTC",
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
date: formattedDate,
|
||
|
|
day: dayName,
|
||
|
|
cap: item.deliverySize || 0,
|
||
|
|
sales: item.saleWeigth || 0,
|
||
|
|
capDisp: item.avaliableDelivery || 0,
|
||
|
|
available: item.delivery === "S" ? "Sim" : "Não",
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Carrega os dados do agendamento de entrega da API
|
||
|
|
* Segue o mesmo padrão do Angular: getScheduleDelivery()
|
||
|
|
*/
|
||
|
|
useEffect(() => {
|
||
|
|
const loadDeliverySchedule = async () => {
|
||
|
|
try {
|
||
|
|
setLoading(true);
|
||
|
|
setError(null);
|
||
|
|
|
||
|
|
// Se já tiver dados via props, não carrega da API
|
||
|
|
if (deliveryDaysProp && deliveryDaysProp.length > 0) {
|
||
|
|
setDeliveryDays(deliveryDaysProp);
|
||
|
|
setLoading(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await shippingService.getScheduleDelivery();
|
||
|
|
|
||
|
|
if (
|
||
|
|
response &&
|
||
|
|
response.deliveries &&
|
||
|
|
Array.isArray(response.deliveries)
|
||
|
|
) {
|
||
|
|
const mappedDays = response.deliveries.map(mapApiItemToDeliveryDay);
|
||
|
|
setDeliveryDays(mappedDays);
|
||
|
|
|
||
|
|
// Se não houver data selecionada e houver dias disponíveis, selecionar o primeiro disponível
|
||
|
|
if (!selectedDeliveryDate && mappedDays.length > 0) {
|
||
|
|
const firstAvailable = mappedDays.find(
|
||
|
|
(d) => d.available === "Sim"
|
||
|
|
);
|
||
|
|
if (firstAvailable) {
|
||
|
|
onDateChange(firstAvailable.date);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
setDeliveryDays([]);
|
||
|
|
}
|
||
|
|
} catch (err: any) {
|
||
|
|
console.error("Erro ao carregar agendamento de entrega:", err);
|
||
|
|
setError(err.message || "Erro ao carregar dados de entrega");
|
||
|
|
setDeliveryDays([]);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
loadDeliverySchedule();
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
}, []); // Carrega apenas uma vez ao montar o componente
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/60 border border-slate-100 overflow-hidden mb-12">
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
||
|
|
{/* Lateral: Seleção Atual */}
|
||
|
|
<div className="lg:col-span-4 bg-[#002147] p-6 text-white relative overflow-hidden flex flex-col justify-center">
|
||
|
|
<div className="relative z-10">
|
||
|
|
<div className="w-10 h-10 bg-orange-500 rounded-xl flex items-center justify-center mb-5 shadow-lg shadow-orange-500/20">
|
||
|
|
<svg
|
||
|
|
className="w-5 h-5 text-white"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2.5"
|
||
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<span className="text-orange-400 font-black text-[9px] uppercase tracking-[0.3em] mb-3 block">
|
||
|
|
Fluxo de Logística
|
||
|
|
</span>
|
||
|
|
<h4 className="text-sm font-bold text-blue-100/70 mb-1.5">
|
||
|
|
Agendamento de Entrega:
|
||
|
|
</h4>
|
||
|
|
<div className="flex items-baseline space-x-2">
|
||
|
|
<span className="text-4xl font-black tracking-tighter">
|
||
|
|
{selectedDeliveryDate}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="mt-6 p-4 rounded-xl bg-white/5 border border-white/10 backdrop-blur-sm">
|
||
|
|
<p className="text-xs font-medium text-blue-50 leading-relaxed">
|
||
|
|
Confira a grade ao lado. Dias em{" "}
|
||
|
|
<span className="text-red-400 font-black">VERMELHO</span> estão
|
||
|
|
com capacidade esgotada ou sem operação.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* Elementos decorativos */}
|
||
|
|
<div className="absolute top-[-20%] right-[-10%] w-80 h-80 bg-orange-500/10 rounded-full blur-3xl"></div>
|
||
|
|
<div className="absolute bottom-[-10%] left-[-10%] w-64 h-64 bg-blue-400/10 rounded-full blur-3xl"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Tabela de Disponibilidade */}
|
||
|
|
<div className="lg:col-span-8 p-6">
|
||
|
|
<div className="flex items-center justify-between mb-6">
|
||
|
|
<div>
|
||
|
|
<h4 className="text-lg font-black text-[#002147] tracking-tight mb-0.5">
|
||
|
|
Capacidade Operacional
|
||
|
|
</h4>
|
||
|
|
<p className="text-slate-400 text-xs font-medium">
|
||
|
|
Selecione uma data disponível para continuar o pedido
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-4">
|
||
|
|
<div className="flex items-center text-[9px] font-black uppercase text-slate-400 tracking-widest">
|
||
|
|
<span className="w-2.5 h-2.5 bg-red-500 rounded-full mr-1.5 shadow-sm shadow-red-500/20"></span>{" "}
|
||
|
|
Bloqueado
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center text-[9px] font-black uppercase text-slate-400 tracking-widest">
|
||
|
|
<span className="w-2.5 h-2.5 bg-emerald-500 rounded-full mr-1.5 shadow-sm shadow-emerald-500/20"></span>{" "}
|
||
|
|
Liberado
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="rounded-xl border border-slate-200 overflow-hidden shadow-inner bg-slate-50/30">
|
||
|
|
{loading ? (
|
||
|
|
<div className="flex items-center justify-center py-12">
|
||
|
|
<div className="flex flex-col items-center">
|
||
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500 mb-3"></div>
|
||
|
|
<span className="text-xs text-slate-500 font-medium">
|
||
|
|
Carregando disponibilidade...
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : error ? (
|
||
|
|
<div className="p-6 bg-red-50 border border-red-200 rounded-lg m-4">
|
||
|
|
<p className="text-xs text-red-600 font-medium">{error}</p>
|
||
|
|
</div>
|
||
|
|
) : deliveryDays.length === 0 ? (
|
||
|
|
<div className="flex items-center justify-center py-12">
|
||
|
|
<p className="text-xs text-slate-400 font-medium">
|
||
|
|
Nenhum agendamento disponível
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="max-h-[300px] overflow-auto custom-scrollbar">
|
||
|
|
<table className="w-full text-left text-xs border-collapse">
|
||
|
|
<thead className="bg-white sticky top-0 z-20 border-b border-slate-200">
|
||
|
|
<tr className="text-slate-500 font-black uppercase tracking-widest text-[9px]">
|
||
|
|
<th className="px-5 py-3 border-r border-slate-100">
|
||
|
|
Data
|
||
|
|
</th>
|
||
|
|
<th className="px-5 py-3 border-r border-slate-100">
|
||
|
|
Dia
|
||
|
|
</th>
|
||
|
|
<th className="px-5 py-3 border-r border-slate-100 text-center">
|
||
|
|
Capacidade
|
||
|
|
</th>
|
||
|
|
<th className="px-5 py-3 border-r border-slate-100 text-center">
|
||
|
|
Carga Atual
|
||
|
|
</th>
|
||
|
|
<th className="px-5 py-3">Status</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-slate-100">
|
||
|
|
{deliveryDays.map((d, i) => {
|
||
|
|
const occupancy =
|
||
|
|
d.cap > 0 ? (d.sales / d.cap) * 100 : 100;
|
||
|
|
const isFull = d.available === "Não";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<tr
|
||
|
|
key={i}
|
||
|
|
onClick={() => !isFull && onDateChange(d.date)}
|
||
|
|
className={`transition-all duration-200 group cursor-pointer ${
|
||
|
|
isFull
|
||
|
|
? "bg-red-600 text-white hover:bg-red-700"
|
||
|
|
: selectedDeliveryDate === d.date
|
||
|
|
? "bg-orange-50 text-orange-900 border-l-4 border-l-orange-500"
|
||
|
|
: "bg-white hover:bg-slate-50"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<td className="px-5 py-3.5 font-black border-r border-slate-100/10 text-sm">
|
||
|
|
{d.date}
|
||
|
|
</td>
|
||
|
|
<td className="px-5 py-3.5 font-bold border-r border-slate-100/10 uppercase text-[9px] tracking-widest opacity-80">
|
||
|
|
{d.day}
|
||
|
|
</td>
|
||
|
|
<td className="px-5 py-3.5 border-r border-slate-100/10 text-center font-bold text-xs">
|
||
|
|
{d.cap} Ton
|
||
|
|
</td>
|
||
|
|
<td className="px-5 py-3.5 border-r border-slate-100/10">
|
||
|
|
<div className="flex flex-col items-center">
|
||
|
|
<span className="font-bold text-[10px] mb-1.5">
|
||
|
|
{d.sales.toFixed(3)} Ton
|
||
|
|
</span>
|
||
|
|
<div
|
||
|
|
className={`w-20 h-1 rounded-full overflow-hidden ${
|
||
|
|
isFull
|
||
|
|
? "bg-white/20"
|
||
|
|
: "bg-slate-100 shadow-inner"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={`h-full transition-all duration-1000 ${
|
||
|
|
isFull
|
||
|
|
? "bg-white"
|
||
|
|
: occupancy > 85
|
||
|
|
? "bg-orange-500"
|
||
|
|
: "bg-emerald-500"
|
||
|
|
}`}
|
||
|
|
style={{
|
||
|
|
width: `${Math.min(occupancy, 100)}%`,
|
||
|
|
}}
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td className="px-5 py-3.5">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span
|
||
|
|
className={`px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-[0.1em] shadow-sm ${
|
||
|
|
isFull
|
||
|
|
? "bg-white text-red-600"
|
||
|
|
: "bg-emerald-100 text-emerald-700 border border-emerald-200"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{d.available === "Sim"
|
||
|
|
? "• Disponível"
|
||
|
|
: "• Esgotado"}
|
||
|
|
</span>
|
||
|
|
{!isFull && (
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4 opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all text-orange-500"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="3"
|
||
|
|
d="M9 5l7 7-7 7"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default Baldinho;
|