testes/src/features/prafrot/views/StatusView.jsx

350 lines
16 KiB
JavaScript

import React, { useEffect, useState, useMemo } from 'react';
import { useStatus } from '../hooks/useStatus';
import { useVehicles } from '../hooks/useVehicles';
import { useDrivers } from '../hooks/useDrivers';
import { useFleetLists } from '../hooks/useFleetLists';
import AutocompleteInput from '../components/AutocompleteInput';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-lg font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
// --- Inline Status Editor ---
const StatusCell = ({ currentStatus, id, options, onUpdate }) => {
const [isEditing, setIsEditing] = useState(false);
const [tempValue, setTempValue] = useState(currentStatus);
useEffect(() => {
setTempValue(currentStatus);
}, [currentStatus]);
const handleBlur = () => {
setIsEditing(false);
if (tempValue !== currentStatus) {
onUpdate(id, tempValue);
}
};
const handleChange = (e) => {
setTempValue(e.target.value);
};
if (isEditing) {
return (
<select
autoFocus
value={tempValue}
onChange={handleChange}
onBlur={handleBlur}
className="w-full bg-slate-50 dark:bg-[#141414] border border-orange-500 rounded px-1 py-0.5 text-[10px] uppercase font-bold text-slate-700 dark:text-slate-200 focus:outline-none"
onClick={(e) => e.stopPropagation()}
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
}
return (
<div
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border cursor-pointer hover:opacity-80 transition-opacity ${
currentStatus === 'Disponível' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
currentStatus === 'Em Manutenção' ? 'bg-amber-500/10 text-amber-500 border-amber-500/20' :
'bg-slate-500/10 text-slate-500 border-slate-500/20'
}`}
>
{currentStatus || 'N/A'}
</div>
);
};
export default function StatusView() {
const { statusList, loading, fetchStatus, createStatus, updateStatus, updateStatusInline, updateStatusBatch, deleteStatus } = useStatus();
const { fetchListsConfig, statusFrotaOptions } = useFleetLists();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const initialFormState = {
atuacao: '', base: '', categoria: '', id_rota: '', idstatus_frota: '',
manutencao: '', modelo: '', motorista: '', obs: '', placa: '',
placa_reserva: '', proprietario: '', status_frota: 'Disponível',
tipo_placa: '', uf: '', vecfleet: ''
};
const [formData, setFormData] = useState(initialFormState);
const { vehicles, fetchVehicles } = useVehicles();
const { drivers, fetchDrivers } = useDrivers();
useEffect(() => {
fetchStatus();
fetchVehicles();
fetchDrivers();
fetchListsConfig();
}, []);
const handleVehicleSelect = (vehicle) => {
if (!vehicle) return;
setFormData(prev => ({
...prev,
placa: vehicle.placa || prev.placa,
placa_reserva: vehicle.placa_reserva || prev.placa_reserva,
modelo: vehicle.modelo || vehicle.mod_veiculo || prev.modelo,
categoria: vehicle.categoria || vehicle.cat_veiculo || prev.categoria,
tipo_placa: vehicle.tipo_placa || prev.tipo_placa,
base: vehicle.base || prev.base,
uf: vehicle.uf || prev.uf,
proprietario: vehicle.proprietario || prev.proprietario,
atuacao: vehicle.atuacao || prev.atuacao,
vecfleet: vehicle.vecfleet || prev.vecfleet,
}));
};
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...initialFormState, ...item });
} else {
setEditingItem(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
let success;
if (editingItem) {
success = await updateStatus(editingItem.idstatus_frota, formData);
} else {
success = await createStatus(formData);
}
if (success) setIsModalOpen(false);
};
const [selectedIds, setSelectedIds] = useState([]);
// ... (effects)
const handleStatusUpdate = async (id, newStatus) => {
// Verificação robusta com conversão para String para evitar incompatibilidade de tipos
const isSelected = selectedIds.some(sid => String(sid) === String(id));
if (isSelected && selectedIds.length > 1) {
// Confirmação simples para evitar acidentes apenas se houver mais de um item selecionado
const confirmUpdate = window.confirm(`Você selecionou ${selectedIds.length} itens. Deseja atualizar o status de TODOS eles para "${newStatus}"?`);
if (confirmUpdate) {
const success = await updateStatusBatch(selectedIds, newStatus);
if (success) {
setSelectedIds([]); // Limpa a seleção após atualizar com sucesso
}
}
} else {
// Agora o Single Update também utiliza o endpoint em lote
await updateStatusBatch([id], newStatus);
}
};
const filteredData = useMemo(() => Array.isArray(statusList) ? statusList.filter(item =>
item.placa?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.motorista?.toLowerCase().includes(searchTerm.toLowerCase())
) : [], [statusList, searchTerm]);
return (
<div className="space-y-6">
{/* ... Header ... */}
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Status da Frota</h1>
<p className="text-slate-500 text-sm">Histórico de movimentação e estados dos veículos.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
{/* ... Search ... */}
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Nova Alteração Status
</DarkButton>
</div>
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
loading={loading}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
rowKey="idstatus_frota"
columns={[
{ header: 'ID', field: 'idstatus_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS', field: 'status_frota', width: '150px', render: (row) => (
<StatusCell
currentStatus={row.status_frota}
id={row.idstatus_frota}
options={statusFrotaOptions}
onUpdate={handleStatusUpdate}
/>
)},
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
{ header: 'PLACA RESERVA', field: 'placa_reserva', width: '110px' },
{ header: 'MODELO', field: 'modelo', width: '120px' },
{ header: 'CATEGORIA', field: 'categoria', width: '100px' },
{ header: 'TIPO PLACA', field: 'tipo_placa', width: '100px' },
{ header: 'BASE', field: 'base', width: '100px' },
{ header: 'UF', field: 'uf', width: '60px' },
{ header: 'PROPRIETÁRIO', field: 'proprietario', width: '140px' },
{ header: 'ATUAÇÃO', field: 'atuacao', width: '140px' },
{ header: 'MANUTENÇÃO', field: 'manutencao', width: '150px' },
{ header: 'ID ROTA', field: 'id_rota', width: '100px' },
{ header: 'VECFLEET', field: 'vecfleet', width: '100px' },
{ header: 'OBS', field: 'obs', width: '200px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'Buscar placa...' },
{ field: 'base', label: 'Base', type: 'select' },
{ field: 'status_frota', label: 'Status', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteStatus(item.idstatus_frota)}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-4xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden shadow-2xl">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a]">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold">
{editingItem ? 'Editar Registro de Status' : 'Nova Alteração de Status'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-stone-500">
Atualize o estado atual do veículo na frota.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-4 max-h-[70vh] overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-3 gap-4 pb-4">
<AutocompleteInput
label="Placa"
value={formData.placa}
onChange={v => setFormData({...formData, placa: v})}
options={vehicles}
displayKey="placa"
valueKey="placa"
searchKeys={['placa']}
onSelect={handleVehicleSelect}
required
/>
<DarkInput label="Placa Reserva" value={formData.placa_reserva} onChange={e => setFormData({...formData, placa_reserva: e.target.value})} />
<DarkSelect label="Novo Status" options={statusFrotaOptions} value={formData.status_frota} onChange={v => setFormData({...formData, status_frota: v})} />
</div>
<div className="grid grid-cols-3 gap-4 pb-4">
<AutocompleteInput
label="Motorista"
value={formData.motorista}
onChange={v => setFormData({...formData, motorista: v})}
options={drivers}
displayKey="NOME_FAVORECIDO"
valueKey="NOME_FAVORECIDO"
placeholder="Buscar motorista..."
/>
<DarkInput label="Base" value={formData.base} onChange={e => setFormData({...formData, base: e.target.value})} />
<DarkInput label="ID Rota" value={formData.id_rota} onChange={e => setFormData({...formData, id_rota: e.target.value})} />
</div>
<div className="grid grid-cols-4 gap-4 pb-4">
<DarkInput label="Tipo Placa" value={formData.tipo_placa} onChange={e => setFormData({...formData, tipo_placa: e.target.value})} />
<DarkInput label="Categoria" value={formData.categoria} onChange={e => setFormData({...formData, categoria: e.target.value})} />
<DarkInput label="Modelo" value={formData.modelo} onChange={e => setFormData({...formData, modelo: e.target.value})} />
<DarkInput label="UF" value={formData.uf} onChange={e => setFormData({...formData, uf: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4 pb-4">
<DarkInput label="Proprietário" value={formData.proprietario} onChange={e => setFormData({...formData, proprietario: e.target.value})} />
<DarkInput label="Atuação" value={formData.atuacao} onChange={e => setFormData({...formData, atuacao: e.target.value})} />
<DarkInput label="Vecfleet" value={formData.vecfleet} onChange={e => setFormData({...formData, vecfleet: e.target.value})} />
</div>
<div className="grid grid-cols-1 gap-4 pb-4">
<DarkInput label="Manutenção (Info)" value={formData.manutencao} onChange={e => setFormData({...formData, manutencao: e.target.value})} />
</div>
<div className="gap-4 pb-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações / Motivo</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[80px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
</div>
<DialogFooter className="sticky bottom-0 bg-white dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit">Atualizar Registro</DarkButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}