- Backend: - Adicionado endpoint para extrato financeiro do profissional (/meus-pagamentos). - Atualizada query SQL para incluir nome da empresa e curso nos detalhes da transação. - Adicionado retorno de valores (Free, Extra, Descrição) na API. - Frontend: - Nova página "Meus Pagamentos" com modal de detalhes da transação. - Removido componente antigo PhotographerFinance. - Ajustado filtro de motoristas na Logística para exibir apenas profissionais atribuídos e com carro. - Corrigida exibição da função do profissional na Escala (mostra a função atribuída no evento, ex: Cinegrafista). - Melhoria no botão de voltar na tela de detalhes do evento.
216 lines
9.7 KiB
TypeScript
216 lines
9.7 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Plus, Trash, User, Truck, Car } from "lucide-react";
|
|
import { useAuth } from "../contexts/AuthContext";
|
|
import { listCarros, createCarro, deleteCarro, addPassenger, removePassenger, listPassengers, listCarros as fetchCarrosApi } from "../services/apiService";
|
|
import { useData } from "../contexts/DataContext";
|
|
import { UserRole } from "../types";
|
|
|
|
interface EventLogisticsProps {
|
|
agendaId: string;
|
|
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
|
|
}
|
|
|
|
const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfessionals }) => {
|
|
const { token, user } = useAuth();
|
|
const { professionals } = useData();
|
|
const [carros, setCarros] = useState<Carro[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// New Car State
|
|
const [driverId, setDriverId] = useState("");
|
|
const [arrivalTime, setArrivalTime] = useState("07:00");
|
|
const [notes, setNotes] = useState("");
|
|
|
|
const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER;
|
|
|
|
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);
|
|
}
|
|
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) {
|
|
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) {
|
|
loadCarros();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white p-4 rounded-lg shadow space-y-4">
|
|
<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>
|
|
|
|
{/* 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>}
|
|
{car.passengers.map((p: any) => (
|
|
<div key={p.id} className="flex justify-between items-center text-sm bg-white p-1 rounded px-2 border">
|
|
<span className="truncate">{p.name || "Desconhecido"}</span>
|
|
{isEditable && (
|
|
<button onClick={() => handleRemovePassenger(car.id, p.profissional_id)} className="text-red-400 hover:text-red-600">
|
|
<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>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EventLogistics;
|