570 lines
31 KiB
JavaScript
570 lines
31 KiB
JavaScript
import React, { useEffect, useState, useMemo } from 'react';
|
|
import { useMaintenance } from '../hooks/useMaintenance';
|
|
import { useVehicles } from '../hooks/useVehicles';
|
|
import { useWorkshops } from '../hooks/useWorkshops';
|
|
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";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { useFeedback } from '../components/FeedbackNotification';
|
|
|
|
// Reusing styled components locally or moving to a shared file in future
|
|
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-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-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-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-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-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
|
|
const variants = {
|
|
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-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 for Maintenance ---
|
|
const MaintenanceStatusCell = ({ 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-emerald-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 cursor-pointer hover:opacity-80 transition-opacity text-[9px] font-bold uppercase tracking-wider border ${
|
|
currentStatus?.includes('Pendente') ? 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20' :
|
|
currentStatus === 'Aprovado' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
|
|
currentStatus === 'Concluído' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
|
|
'bg-slate-700/30 text-slate-400 border-slate-600/30'
|
|
}`}
|
|
>
|
|
{currentStatus || 'Pendente'}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
import { Wrench, CheckCircle, Truck } from 'lucide-react';
|
|
import { prafrotStatisticsService } from '../services/prafrotStatisticsService';
|
|
|
|
export default function MaintenanceView() {
|
|
const { maintenances, fetchMaintenances, createMaintenance, updateMaintenance, updateMaintenanceBatch, deleteMaintenance } = useMaintenance();
|
|
const { deleteVehicle } = useVehicles();
|
|
const { workshops, fetchWorkshops } = useWorkshops();
|
|
const { fetchListsConfig, statusManutencaoOptions, motivoAtendimentoOptions } = useFleetLists();
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingItem, setEditingItem] = useState(null);
|
|
const [statusStats, setStatusStats] = useState([]);
|
|
const [selectedStatusRecords, setSelectedStatusRecords] = useState(null);
|
|
const [isRecordsModalOpen, setIsRecordsModalOpen] = useState(false);
|
|
|
|
const initialFormState = {
|
|
ano_entrada: '', ano_saida: '', atendimento: '', base_frota: '', cidade: '',
|
|
condicao_pagamento: '', data_agendamento: '', data_finalizacao: '', data_parada_veiculo: '',
|
|
data_retirada: '', data_solicitacao: '', dif_orcamento: '', endereco_prestador: '',
|
|
idmanutencao_frota: '', manutencao: '', mes_entrada: '', mes_saida: '', modelo: '',
|
|
motivo_atendimento: '', obs: '', oficina: '', orcamento_final: '', orcamento_inicial: '',
|
|
pdf_orcamento: '', placa: '', placa_reserva: '', previcao_entrega: '', proprietario: '',
|
|
resp_aprovacao: '', responsavel: '',
|
|
status: 'Pendente', uf: '', validacao_financeiro: ''
|
|
};
|
|
const [formData, setFormData] = useState(initialFormState);
|
|
|
|
const { vehicles, fetchVehicles } = useVehicles();
|
|
|
|
useEffect(() => {
|
|
fetchMaintenances();
|
|
fetchVehicles();
|
|
fetchWorkshops();
|
|
fetchListsConfig();
|
|
|
|
// Fetch Status Stats
|
|
prafrotStatisticsService.getPlacasPorStatus().then(data => {
|
|
if (Array.isArray(data)) setStatusStats(data);
|
|
});
|
|
}, []);
|
|
|
|
const getStatusData = (status) => {
|
|
return statusStats.find(item => {
|
|
const itemStatus = (item.status || item.status_frota || '');
|
|
return itemStatus.trim().toLowerCase() === status?.trim().toLowerCase();
|
|
});
|
|
};
|
|
|
|
const handleStatusClick = (status) => {
|
|
const data = getStatusData(status);
|
|
if (data && data.registros) {
|
|
setSelectedStatusRecords({
|
|
title: status,
|
|
records: JSON.parse(data.registros).filter(r => r !== null)
|
|
});
|
|
setIsRecordsModalOpen(true);
|
|
}
|
|
};
|
|
|
|
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,
|
|
base_frota: vehicle.base || prev.base_frota,
|
|
uf: vehicle.uf || prev.uf,
|
|
proprietario: vehicle.proprietario || prev.proprietario,
|
|
cidade: vehicle.cidade || prev.cidade,
|
|
}));
|
|
};
|
|
|
|
const handleWorkshopSelect = (workshop) => {
|
|
if (!workshop) return;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
oficina: workshop.nome_reduzido || prev.oficina,
|
|
cidade: workshop.cidade || prev.cidade,
|
|
uf: workshop.uf || prev.uf,
|
|
endereco_prestador: workshop.endereco || prev.endereco_prestador,
|
|
}));
|
|
};
|
|
|
|
const handleOpenModal = (item = null) => {
|
|
if (item) {
|
|
setEditingItem(item);
|
|
setFormData({ ...initialFormState, ...item });
|
|
} else {
|
|
setEditingItem(null);
|
|
setFormData(initialFormState);
|
|
}
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const { notifyFields } = useFeedback();
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
|
|
// Validação de campos obrigatórios mais abrangente para evitar 400 do backend
|
|
const requiredFields = [];
|
|
if (!formData.placa) requiredFields.push('Placa');
|
|
if (!formData.oficina) requiredFields.push('Oficina');
|
|
if (!formData.responsavel) requiredFields.push('Responsável');
|
|
if (!formData.modelo) requiredFields.push('Modelo');
|
|
if (!formData.motivo_atendimento) requiredFields.push('Motivo Atendimento');
|
|
if (!formData.proprietario) requiredFields.push('Proprietário');
|
|
if (!formData.base_frota) requiredFields.push('Base Frota');
|
|
if (!formData.data_parada_veiculo) requiredFields.push('Data de Parada');
|
|
if (!formData.manutencao) requiredFields.push('Descrição da Manutenção');
|
|
|
|
if (requiredFields.length > 0) {
|
|
notifyFields(requiredFields);
|
|
return;
|
|
}
|
|
|
|
// Limpeza do payload: remove strings vazias que podem causar erro no backend
|
|
const payload = {};
|
|
Object.keys(formData).forEach(key => {
|
|
if (formData[key] !== '' && formData[key] !== null) {
|
|
payload[key] = formData[key];
|
|
}
|
|
});
|
|
|
|
// Remove campos que não devem ser enviados na criação/edição direta conforme regras de negócio
|
|
delete payload.mes_entrada;
|
|
delete payload.ano_entrada;
|
|
delete payload.mes_saida;
|
|
delete payload.ano_saida;
|
|
|
|
let success;
|
|
if (editingItem) {
|
|
success = await updateMaintenance(editingItem.idmanutencao_frota, payload);
|
|
} else {
|
|
success = await createMaintenance(payload);
|
|
}
|
|
if (success) setIsModalOpen(false);
|
|
};
|
|
|
|
const [selectedIds, setSelectedIds] = useState([]);
|
|
|
|
const handleStatusUpdate = async (id, newStatus) => {
|
|
// Se o item que está sendo editado faz parte da seleção, aplica em massa
|
|
if (selectedIds.includes(id)) {
|
|
// Confirmação simples
|
|
const confirmUpdate = window.confirm(`Você selecionou ${selectedIds.length} itens. Deseja atualizar o status de TODOS eles para "${newStatus}"?`);
|
|
|
|
if (confirmUpdate) {
|
|
const success = await updateMaintenanceBatch(selectedIds, newStatus);
|
|
if (success) {
|
|
setSelectedIds([]); // Limpa a seleção
|
|
}
|
|
}
|
|
} else {
|
|
// Single Update (using batch route as requested)
|
|
await updateMaintenanceBatch([id], newStatus);
|
|
}
|
|
};
|
|
|
|
// Filter logic
|
|
const filteredData = useMemo(() => Array.isArray(maintenances) ? maintenances.filter(item =>
|
|
item.placa?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.oficina?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
String(item.idmanutencao_frota || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
) : [], [maintenances, searchTerm]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-black text-slate-800 dark:text-white tracking-tight">Manutenção de Frota</h1>
|
|
<p className="text-slate-500 text-sm">Controle de oficinas, orçamentos e agendamentos.</p>
|
|
</div>
|
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
|
<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-xl text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
|
|
placeholder="Buscar por ID, placa ou oficina..."
|
|
value={searchTerm}
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<DarkButton onClick={() => handleOpenModal()}>
|
|
<Plus size={18} /> Nova Solicitação
|
|
</DarkButton>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Highlights for Maintenance */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{[
|
|
{ label: 'Oficina - Externo', status: 'Em Oficina - Externo', color: 'bg-orange-500/10 text-orange-600', icon: <Wrench size={20} />, hover: 'hover:border-orange-500/30' },
|
|
{ label: 'Oficina - Rentals', status: 'Em Oficina - Rentals', color: 'bg-purple-500/10 text-purple-600', icon: <Wrench size={20} />, hover: 'hover:border-purple-500/30' },
|
|
{ label: 'Oficina - Trois', status: 'Em Oficina - Trois', color: 'bg-pink-500/10 text-pink-600', icon: <Wrench size={20} />, hover: 'hover:border-pink-500/30' },
|
|
{ label: 'Pra Reboque', status: 'Pra Reboque', color: 'bg-rose-500/10 text-rose-600', icon: <Wrench size={20} />, hover: 'hover:border-rose-500/30' }
|
|
].map((item, idx) => (
|
|
<div
|
|
key={idx}
|
|
onClick={() => handleStatusClick(item.status)}
|
|
className={`bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-xl p-4 shadow-sm hover:shadow-md transition-all flex flex-col items-center justify-center text-center group cursor-pointer ${item.hover}`}
|
|
>
|
|
<div className={`p-2.5 rounded-lg ${item.color} group-hover:scale-110 transition-transform mb-2`}>
|
|
{item.icon}
|
|
</div>
|
|
<span className="text-[10px] font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 leading-tight mb-1">{item.label}</span>
|
|
<div className="text-xl font-black text-slate-800 dark:text-white">
|
|
{getStatusData(item.status)?.total || 0}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
|
|
<ExcelTable
|
|
data={filteredData}
|
|
selectedIds={selectedIds}
|
|
onSelectionChange={setSelectedIds}
|
|
rowKey="idmanutencao_frota"
|
|
columns={[
|
|
{ header: 'NÚMERO DO ATENDIMENTO', field: 'idmanutencao_frota', width: '120px' },
|
|
{ header: 'RESPONSÁVEL', field: 'responsavel', width: '140px' },
|
|
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
|
|
{ header: 'MODELO', field: 'modelo', width: '120px' },
|
|
{ header: 'OBS', field: 'obs', width: '200px' },
|
|
{ header: 'STATUS', field: 'status', width: '160px', render: (row) => (
|
|
<MaintenanceStatusCell
|
|
currentStatus={row.status}
|
|
id={row.idmanutencao_frota}
|
|
options={statusManutencaoOptions}
|
|
onUpdate={handleStatusUpdate}
|
|
/>
|
|
)},
|
|
{ header: 'BASE', field: 'base_frota', width: '120px' },
|
|
{ header: 'DATA DE PARADA', field: 'data_parada_veiculo', width: '120px' },
|
|
{ header: 'DATA DE PREVISÃO', field: 'previcao_entrega', width: '120px' },
|
|
{ header: 'ORÇAMENTO FINAL', field: 'orcamento_final', width: '140px', className: 'font-mono text-emerald-600 dark:text-emerald-400' },
|
|
{ header: 'DOCUMENTO “ORÇAMENTO”', field: 'pdf_orcamento', width: '140px', render: (row) => row.pdf_orcamento ? <a href={row.pdf_orcamento} target="_blank" className="underline text-blue-500 font-bold">PDF</a> : '-' },
|
|
{ header: 'OFICINA', field: 'oficina', width: '180px' },
|
|
]}
|
|
filterDefs={[
|
|
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'Buscar placa...' },
|
|
{ field: 'oficina', label: 'Oficina', type: 'select' },
|
|
{ field: 'status', label: 'Status', type: 'select' },
|
|
{ field: 'motivo_atendimento', label: 'Motivo Atendimento', type: 'select' },
|
|
{ field: 'responsavel', label: 'Responsável', type: 'select' },
|
|
{ field: 'cidade', label: 'Cidade', type: 'select' },
|
|
]}
|
|
onEdit={handleOpenModal}
|
|
onDelete={(item) => deleteMaintenance(item.idmanutencao_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] bg-slate-50 dark:bg-[#1c1c1c]">
|
|
<DialogTitle className="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight">
|
|
{editingItem ? 'Editar Manutenção' : 'Nova Solicitação de Manutenção'}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-slate-500 dark:text-stone-500">
|
|
Registre os detalhes da manutenção, orçamentos e prazos do veículo.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="px-6 py-2 max-h-[75vh] overflow-y-auto custom-scrollbar">
|
|
<Tabs defaultValue="basicos" className="w-full">
|
|
<TabsList className="bg-slate-100 dark:bg-[#141414] border border-slate-200 dark:border-[#2a2a2a] w-full justify-start p-1 h-auto mb-4">
|
|
{['basicos', 'orcamentos', 'datas'].map(tab => (
|
|
<TabsTrigger
|
|
key={tab}
|
|
value={tab}
|
|
className="data-[state=active]:bg-emerald-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
|
|
>
|
|
{tab}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
|
|
<TabsContent value="basicos" className="space-y-4 m-0 pb-4">
|
|
<div className="grid grid-cols-3 gap-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})} />
|
|
<DarkInput label="Modelo" value={formData.modelo} onChange={e => setFormData({...formData, modelo: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<AutocompleteInput
|
|
label="Oficina"
|
|
value={formData.oficina}
|
|
onChange={v => setFormData({...formData, oficina: v})}
|
|
options={workshops}
|
|
displayKey="nome_reduzido"
|
|
valueKey="nome_reduzido"
|
|
searchKeys={['nome_reduzido', 'razao_social', 'cnpj']}
|
|
onSelect={handleWorkshopSelect}
|
|
required
|
|
/>
|
|
<DarkInput label="Responsável" value={formData.responsavel} onChange={e => setFormData({...formData, responsavel: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<DarkSelect
|
|
label="Status manutenção"
|
|
options={statusManutencaoOptions}
|
|
value={formData.status}
|
|
onChange={v => setFormData({...formData, status: v})}
|
|
/>
|
|
<DarkInput label="Atendimento" value={formData.atendimento} onChange={e => setFormData({...formData, atendimento: e.target.value})} />
|
|
<DarkInput label="Base Frota" value={formData.base_frota} onChange={e => setFormData({...formData, base_frota: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<DarkInput label="Cidade" value={formData.cidade} onChange={e => setFormData({...formData, cidade: e.target.value})} />
|
|
<DarkInput label="UF" value={formData.uf} onChange={e => setFormData({...formData, uf: e.target.value})} maxLength={2} />
|
|
</div>
|
|
<DarkInput label="Endereço Prestador" value={formData.endereco_prestador} onChange={e => setFormData({...formData, endereco_prestador: e.target.value})} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="orcamentos" className="space-y-4 m-0 pb-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<DarkInput type="number" label="Orçamento Inicial" value={formData.orcamento_inicial} onChange={e => setFormData({...formData, orcamento_inicial: e.target.value})} />
|
|
<DarkInput type="number" label="Orçamento Final" value={formData.orcamento_final} onChange={e => setFormData({...formData, orcamento_final: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<DarkInput label="Dif. Orçamento" value={formData.dif_orcamento} onChange={e => setFormData({...formData, dif_orcamento: e.target.value})} />
|
|
<DarkInput label="Cond. Pagamento" value={formData.condicao_pagamento} onChange={e => setFormData({...formData, condicao_pagamento: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<DarkInput label="Validação Financeiro" value={formData.validacao_financeiro} onChange={e => setFormData({...formData, validacao_financeiro: e.target.value})} />
|
|
<DarkInput label="Resp. Aprovação" value={formData.resp_aprovacao} onChange={e => setFormData({...formData, resp_aprovacao: e.target.value})} />
|
|
</div>
|
|
<DarkInput label="Link PDF Orçamento" value={formData.pdf_orcamento} onChange={e => setFormData({...formData, pdf_orcamento: e.target.value})} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="datas" className="space-y-4 m-0 pb-4">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<DarkInput type="date" label="Data Solicitação" value={formData.data_solicitacao?.split('T')[0]} onChange={e => setFormData({...formData, data_solicitacao: e.target.value})} />
|
|
<DarkInput type="date" label="Data Agendamento" value={formData.data_agendamento?.split('T')[0]} onChange={e => setFormData({...formData, data_agendamento: e.target.value})} />
|
|
<DarkInput type="date" label="Data Parada" value={formData.data_parada_veiculo?.split('T')[0]} onChange={e => setFormData({...formData, data_parada_veiculo: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<DarkInput type="date" label="Previsão Entrega" value={formData.previcao_entrega?.split('T')[0]} onChange={e => setFormData({...formData, previcao_entrega: e.target.value})} />
|
|
<DarkInput type="date" label="Data Finalização" value={formData.data_finalizacao?.split('T')[0]} onChange={e => setFormData({...formData, data_finalizacao: e.target.value})} />
|
|
<DarkInput type="date" label="Data Retirada" value={formData.data_retirada?.split('T')[0]} onChange={e => setFormData({...formData, data_retirada: e.target.value})} />
|
|
</div>
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{/* Campos de mês e ano comentados para escrita e envio ao back conforme solicitado */}
|
|
{/* <DarkInput label="Mês Ent." value={formData.mes_entrada} onChange={e => setFormData({...formData, mes_entrada: e.target.value})} />
|
|
<DarkInput label="Ano Ent." value={formData.ano_entrada} onChange={e => setFormData({...formData, ano_entrada: e.target.value})} />
|
|
<DarkInput label="Mês Saí." value={formData.mes_saida} onChange={e => setFormData({...formData, mes_saida: e.target.value})} />
|
|
<DarkInput label="Ano Saí." value={formData.ano_saida} onChange={e => setFormData({...formData, ano_saida: e.target.value})} /> */}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Campos adicionais fora das tabs */}
|
|
<div className="space-y-4 pt-4 pb-4 border-t border-slate-200 dark:border-[#2a2a2a]">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<DarkSelect
|
|
label="Motivo Atendimento"
|
|
options={motivoAtendimentoOptions}
|
|
value={formData.motivo_atendimento}
|
|
onChange={v => setFormData({...formData, motivo_atendimento: v})}
|
|
/>
|
|
<DarkInput label="Proprietário" value={formData.proprietario} onChange={e => setFormData({...formData, proprietario: e.target.value})} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4 pb-4">
|
|
<div className="gap-4">
|
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-400 tracking-wider ml-1">Descrição da Manutenção</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-emerald-500 transition-all placeholder:text-slate-400 min-h-[60px]"
|
|
value={formData.manutencao}
|
|
onChange={e => setFormData({...formData, manutencao: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="gap-4">
|
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-400 tracking-wider ml-1">Observações</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-emerald-500 transition-all placeholder:text-slate-400 min-h-[60px]"
|
|
value={formData.obs}
|
|
onChange={e => setFormData({...formData, obs: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<DialogFooter className="bg-slate-50 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" onClick={handleSubmit}>
|
|
{editingItem ? 'Salvar Alterações' : 'Criar Solicitação'}
|
|
</DarkButton>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Records Detail Modal */}
|
|
<Dialog open={isRecordsModalOpen} onOpenChange={setIsRecordsModalOpen}>
|
|
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none">
|
|
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-4 bg-indigo-500/10 rounded-2xl text-indigo-600 shadow-inner">
|
|
<Wrench size={28} />
|
|
</div>
|
|
<div>
|
|
<DialogTitle className="text-2xl font-black text-slate-800 dark:text-white uppercase tracking-tight">
|
|
Veículos: {selectedStatusRecords?.title}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
|
|
Listagem técnica dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-emerald-500 font-bold">"{selectedStatusRecords?.title}"</span>.
|
|
</DialogDescription>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
|
|
<div className="h-full w-full rounded-2xl border border-slate-200 dark:border-[#2a2a2a] overflow-hidden shadow-sm">
|
|
<ExcelTable
|
|
data={selectedStatusRecords?.records || []}
|
|
columns={[
|
|
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
|
|
{ header: 'Modelo', field: 'modelo', width: '150px' },
|
|
{ header: 'Unidade', field: 'base', width: '120px' },
|
|
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
|
{ header: 'Status', field: 'status', width: '140px' },
|
|
{ header: 'Manutenção', field: 'manutencao', width: '120px' },
|
|
{ header: 'Atuação', field: 'atuacao', width: '140px' },
|
|
{ header: 'Proprietário', field: 'proprietario', width: '140px' },
|
|
{ header: 'VecFleet', field: 'vecfleet', width: '140px' },
|
|
{ header: 'OBS', field: 'obs', width: '250px' },
|
|
]}
|
|
filterDefs={[
|
|
{ field: 'placa', label: 'Placa', type: 'text' },
|
|
{ field: 'motorista', label: 'Motorista', type: 'text' },
|
|
{ field: 'base', label: 'Unidade', type: 'select' }
|
|
]}
|
|
onDelete={(item) => deleteVehicle(item.idveiculo_frota)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="p-4 border-t border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#1c1c1c]">
|
|
<DarkButton variant="secondary" onClick={() => setIsRecordsModalOpen(false)}>
|
|
Fechar Listagem
|
|
</DarkButton>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|