photum/frontend/components/EventLogistics.tsx

359 lines
16 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Plus, Trash, User, Truck, Car, Send } from "lucide-react";
import { useAuth } from "../contexts/AuthContext";
import { listCarros, createCarro, deleteCarro, addPassenger, removePassenger, listPassengers, listCarros as fetchCarrosApi, notifyLogistics } from "../services/apiService";
import { useData } from "../contexts/DataContext";
import { UserRole } from "../types";
interface EventLogisticsProps {
agendaId: string;
token?: string;
isEditable?: boolean;
assignedProfessionals?: string[];
}
interface Carro {
id: string;
driver_id: string;
driver_name: string;
driver_avatar: string;
arrival_time: string;
notes: string;
passengers: any[]; // We will fetch and attach
}
interface PassengerWithOrder {
id: string;
profissional_id: string;
name: string;
order: number;
}
const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, isEditable: propsIsEditable, assignedProfessionals }) => {
const { token, user } = useAuth();
const { professionals, events } = useData();
const eventData = events.find(e => e.id === agendaId);
const [carros, setCarros] = useState<Carro[]>([]);
const [loading, setLoading] = useState(false);
const [passengerOrders, setPassengerOrders] = useState<Record<string, Record<string, number>>>({});
// New Car State
const [driverId, setDriverId] = useState("");
const [arrivalTime, setArrivalTime] = useState("07:00");
const [notes, setNotes] = useState("");
const isEditable = propsIsEditable !== undefined ? propsIsEditable : (user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER);
// Carregar ordens do localStorage ao montar o componente
useEffect(() => {
const savedOrders = localStorage.getItem(`passengerOrders_${agendaId}`);
if (savedOrders) {
try {
setPassengerOrders(JSON.parse(savedOrders));
} catch (e) {
console.error("Erro ao carregar ordens salvas:", e);
}
}
}, [agendaId]);
// Salvar ordens no localStorage quando mudarem
useEffect(() => {
if (Object.keys(passengerOrders).length > 0) {
localStorage.setItem(`passengerOrders_${agendaId}`, JSON.stringify(passengerOrders));
}
}, [passengerOrders, agendaId]);
useEffect(() => {
if (agendaId && token) {
loadCarros();
}
}, [agendaId, token]);
const loadCarros = async () => {
setLoading(true);
const res = await fetchCarrosApi(agendaId, token!);
if (res.data) {
// For each car, fetch passengers
const carsWithPassengers = await Promise.all(res.data.map(async (car: any) => {
const passRes = await listPassengers(car.id, token!);
return { ...car, passengers: passRes.data || [] };
}));
setCarros(carsWithPassengers);
// Inicializar ordens dos passageiros se não existirem
const newOrders = { ...passengerOrders };
carsWithPassengers.forEach((car: any) => {
if (!newOrders[car.id]) {
newOrders[car.id] = {};
}
car.passengers.forEach((pass: any, index: number) => {
if (newOrders[car.id][pass.profissional_id] === undefined) {
newOrders[car.id][pass.profissional_id] = index + 1;
}
});
});
setPassengerOrders(newOrders);
}
setLoading(false);
};
const handleAddCarro = async () => {
// Driver ID is optional (could be external), but for now select from professionals
const input = {
agenda_id: agendaId,
motorista_id: driverId || undefined,
horario_chegada: arrivalTime,
observacoes: notes
};
const res = await createCarro(input, token!);
if (res.data) {
loadCarros();
setDriverId("");
setNotes("");
} else {
alert("Erro ao criar carro: " + res.error);
}
};
const handleDeleteCarro = async (id: string) => {
if (confirm("Remover este carro e passageiros?")) {
await deleteCarro(id, token!);
loadCarros();
}
};
const handleAddPassenger = async (carId: string, profId: string) => {
if (!profId) return;
const res = await addPassenger(carId, profId, token!);
if (!res.error) {
// Atribuir próxima ordem disponível
const currentCar = carros.find(c => c.id === carId);
const currentPassengers = currentCar?.passengers || [];
const nextOrder = currentPassengers.length + 1;
setPassengerOrders(prev => ({
...prev,
[carId]: {
...prev[carId],
[profId]: nextOrder
}
}));
loadCarros();
} else {
alert("Erro ao adicionar passageiro: " + res.error);
}
};
const handleRemovePassenger = async (carId: string, profId: string) => {
const res = await removePassenger(carId, profId, token!);
if (!res.error) {
// Remover ordem do passageiro
setPassengerOrders(prev => {
const newOrders = { ...prev };
if (newOrders[carId]) {
delete newOrders[carId][profId];
}
return newOrders;
});
loadCarros();
}
};
const handleChangePassengerOrder = (carId: string, profId: string, newOrder: number) => {
setPassengerOrders(prev => ({
...prev,
[carId]: {
...prev[carId],
[profId]: newOrder
}
}));
};
const getSortedPassengers = (carId: string, passengers: any[]) => {
return [...passengers].sort((a, b) => {
const orderA = passengerOrders[carId]?.[a.profissional_id] || 999;
const orderB = passengerOrders[carId]?.[b.profissional_id] || 999;
return orderA - orderB;
});
};
const handleNotifyLogistics = async () => {
if (!confirm("Confirmar logística e enviar notificações para TODA a equipe?")) return;
const res = await notifyLogistics(token!, agendaId, passengerOrders);
if (res.error) {
alert("Erro ao enviar notificações: " + res.error);
} else {
alert("Notificações de logística enviadas com sucesso!");
window.location.reload();
}
};
return (
<div className="bg-white p-4 rounded-lg shadow space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold text-gray-800 flex items-center">
<Truck className="w-5 h-5 mr-2 text-orange-500" />
Logística de Transporte
</h3>
{isEditable && eventData?.logisticaNotificacaoEnviadaEm && (
<div className="text-sm text-green-700 bg-green-50 px-2 py-1 rounded border border-green-200">
Notificação enviada em: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()}
</div>
)}
</div>
{/* Add Car Form - Only for Admins */}
{isEditable && (
<div className="bg-gray-50 p-3 rounded-md flex flex-wrap gap-2 items-end">
<div className="flex-1 min-w-[200px]">
<label className="text-xs text-gray-500">Motorista (Opcional)</label>
<select
className="w-full p-2 rounded border bg-white"
value={driverId}
onChange={e => setDriverId(e.target.value)}
>
<option value="">Selecione ou deixe vazio...</option>
{professionals
.filter(p => {
const hasCar = p.carro_disponivel;
const isAssigned = !assignedProfessionals || assignedProfessionals.includes(p.id);
return hasCar && isAssigned;
})
.map(p => (
<option key={p.id} value={p.id}>{p.nomeEventos || p.nome}</option>
))}
</select>
</div>
<div className="w-24">
<label className="text-xs text-gray-500">Chegada</label>
<input
type="time"
className="w-full p-2 rounded border bg-white"
value={arrivalTime}
onChange={e => setArrivalTime(e.target.value)}
/>
</div>
<button
onClick={handleAddCarro}
className="bg-orange-600 hover:bg-orange-700 text-white p-2 rounded flex items-center"
>
<Plus size={20} /> <span className="ml-1 text-sm">Carro</span>
</button>
</div>
)}
{/* Cars List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{loading ? <p>Carregando...</p> : carros.map(car => (
<div key={car.id} className="border rounded-lg p-3 bg-gray-50">
<div className="flex justify-between items-start mb-2 border-b pb-2">
<div className="flex items-center gap-2">
<div className="bg-orange-100 p-1.5 rounded-full">
<Car className="w-4 h-4 text-orange-600" />
</div>
<div>
<p className="font-bold text-sm text-gray-800">
{car.driver_name || "Motorista não definido"}
</p>
<p className="text-xs text-gray-500">Chegada: {car.arrival_time}</p>
</div>
</div>
{isEditable && (
<button onClick={() => handleDeleteCarro(car.id)} className="text-gray-400 hover:text-red-500">
<Trash size={14} />
</button>
)}
</div>
{/* Passengers */}
<div className="space-y-1 mb-3">
<p className="text-xs font-semibold text-gray-500 uppercase">Passageiros</p>
{car.passengers.length === 0 && <p className="text-xs italic text-gray-400">Vazio</p>}
{getSortedPassengers(car.id, car.passengers).map((p: any, index: number) => (
<div key={p.id} className="flex justify-between items-center text-sm bg-white p-1.5 rounded px-2 border">
{isEditable ? (
<div className="flex items-center gap-2 flex-1">
<select
className="w-16 text-xs p-1 rounded border bg-gray-50"
value={passengerOrders[car.id]?.[p.profissional_id] || index + 1}
onChange={(e) => handleChangePassengerOrder(car.id, p.profissional_id, parseInt(e.target.value))}
>
{Array.from({ length: car.passengers.length }, (_, i) => i + 1).map(num => (
<option key={num} value={num}>{num}º</option>
))}
</select>
<span className="truncate flex-1">{p.name || "Desconhecido"}</span>
</div>
) : (
<span className="truncate">
{(passengerOrders[car.id]?.[p.profissional_id] || index + 1)}º - {p.name || "Desconhecido"}
</span>
)}
{isEditable && (
<button onClick={() => handleRemovePassenger(car.id, p.profissional_id)} className="text-red-400 hover:text-red-600 ml-2">
<Trash size={12} />
</button>
)}
</div>
))}
</div>
{/* Add Passenger - Only for Admins */}
{isEditable && (
<select
className="w-full text-xs p-1 rounded border bg-white"
onChange={(e) => {
if (e.target.value) handleAddPassenger(car.id, e.target.value);
e.target.value = "";
}}
>
<option value="">+ Adicionar Passageiro</option>
{professionals
.filter(p => {
// Filter 1: Must be assigned to event (if restriction list provided)
// If assignedProfessionals prop is missing or empty, maybe we should show all?
// User asked to RESTRICT. So if provided, WE RESTRICT.
const isAssigned = !assignedProfessionals || assignedProfessionals.includes(p.id);
// Filter 2: Must not be already in this car
const isInCar = car.passengers.some((pass: any) => pass.profissional_id === p.id);
return isAssigned && !isInCar;
})
.map(p => (
<option key={p.id} value={p.id}>{p.nomeEventos || p.nome}</option>
))}
</select>
)}
</div>
))}
</div>
{/* Notification Button moved to bottom */}
{isEditable && (
<div className="flex flex-col items-center pt-4 border-t border-gray-100">
<button
onClick={handleNotifyLogistics}
disabled={!!eventData?.logisticaNotificacaoEnviadaEm}
className={`px-6 py-2.5 rounded-md flex items-center text-sm font-medium shadow-sm transition-colors w-full justify-center ${
eventData?.logisticaNotificacaoEnviadaEm
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-green-600 hover:bg-green-700 text-white"
}`}
title={eventData?.logisticaNotificacaoEnviadaEm ? "Notificação já enviada" : "Confirmar logística e notificar equipe"}
>
<Send className="w-5 h-5 mr-2" />
{eventData?.logisticaNotificacaoEnviadaEm ? "Notificação Enviada" : "Finalizar Logística e Notificar Equipe"}
</button>
{eventData?.logisticaNotificacaoEnviadaEm && (
<p className="mt-2 text-xs text-gray-500">
Última notificação: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()}
</p>
)}
</div>
)}
</div>
);
};
export default EventLogistics;