Cliente/OestPan #2

Manually merged
daivid.alves merged 2 commits from Cliente/OestPan into frontend_React 2026-03-02 17:15:10 +00:00
54 changed files with 597 additions and 4898 deletions

View File

@ -133,3 +133,6 @@ const AdvancedFiltersModal = ({ isOpen, onClose, onApply, options = {}, initialF
};
export default AdvancedFiltersModal;

View File

@ -1,200 +0,0 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Calendar, Filter, Download, ArrowRight, MapPin, Truck, User, Search, Loader2 } from 'lucide-react';
import { getTripRequests } from '../services/prafrotService';
import { toast } from 'sonner';
export const AnalysisPanel = ({ onClose }) => {
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const fetchHistory = async () => {
setLoading(true);
try {
const allRequests = await getTripRequests();
// Filter for archived or closed requests if needed, otherwise show all and filter by date
const list = Array.isArray(allRequests) ? allRequests : allRequests?.data || [];
setData(list);
} catch (error) {
toast.error('Erro ao buscar histórico');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchHistory();
}, []);
const filteredData = data.filter(item => {
const matchesSearch =
item.nome_completo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.placa_do_cavalo?.toLowerCase().includes(searchTerm.toLowerCase());
// Simplistic date filtering (needs actual date field from API to be robust)
// For now we show all filtered by search
return matchesSearch;
});
const stats = {
total: filteredData.length,
carreta: filteredData.filter(i => i.tipo_de_veiculo === 'Carreta').length,
truck: filteredData.filter(i => i.tipo_de_veiculo === 'Truck').length
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex justify-end bg-black/60 backdrop-blur-sm p-4"
onClick={onClose}
>
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="w-full max-w-2xl bg-[#0d0d0e] border border-white/5 rounded-[40px] shadow-2xl overflow-hidden flex flex-col"
style={{ fontFamily: 'var(--font-main)' }}
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="p-8 border-b border-white/5 bg-gradient-to-r from-emerald-500/10 to-transparent">
<div className="flex items-center justify-between mb-2">
<h2 className="text-2xl font-black text-white uppercase tracking-tight">Análise de <span className="text-emerald-500">Rotas</span></h2>
<button onClick={onClose} className="w-10 h-10 flex items-center justify-center bg-white/5 hover:bg-white/10 rounded-full transition-all">
<X size={20} className="text-zinc-400" />
</button>
</div>
<p className="text-zinc-500 text-xs font-bold uppercase tracking-widest">Controle de Fluxo e Auditoria por Período</p>
</div>
{/* Filters */}
<div className="p-6 bg-white/5 border-b border-white/5 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[9px] font-black text-zinc-500 uppercase tracking-widest ml-1">Início</label>
<div className="relative">
<Calendar size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
type="date"
value={startDate}
onChange={e => setStartDate(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-black/40 border border-white/5 rounded-xl text-xs text-white focus:outline-none focus:border-emerald-500/50"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[9px] font-black text-zinc-500 uppercase tracking-widest ml-1">Fim</label>
<div className="relative">
<Calendar size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
type="date"
value={endDate}
onChange={e => setEndDate(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-black/40 border border-white/5 rounded-xl text-xs text-white focus:outline-none focus:border-emerald-500/50"
/>
</div>
</div>
</div>
<div className="relative">
<Search size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
type="text"
placeholder="Pesquisar por motorista ou placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 bg-black/40 border border-white/5 rounded-2xl text-xs text-white focus:outline-none focus:border-emerald-500/50 transition-all"
/>
</div>
</div>
{/* List Content */}
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar space-y-4">
{loading ? (
<div className="h-full flex flex-col items-center justify-center gap-4 py-20">
<Loader2 className="animate-spin text-emerald-500" size={32} />
<span className="text-[10px] font-black text-zinc-700 uppercase tracking-widest">Compilando dados...</span>
</div>
) : filteredData.length > 0 ? (
filteredData.map((item, idx) => (
<motion.div
key={item.idsolicitacoes}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className="p-5 bg-white/[0.02] border border-white/5 rounded-[24px] hover:bg-white/[0.04] transition-all group"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-emerald-500/10 text-emerald-400 rounded-lg text-[8px] font-black">#{item.idsolicitacoes}</span>
<span className="text-[8px] font-black text-zinc-600 uppercase tracking-tighter">{item.operacao}</span>
</div>
<span className="text-[10px] font-bold text-zinc-500">{new Date().toLocaleDateString()}</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-zinc-800 flex items-center justify-center text-zinc-500 group-hover:bg-emerald-500/20 group-hover:text-emerald-400 transition-colors">
<User size={14} />
</div>
<div className="min-w-0">
<div className="text-[11px] font-bold text-zinc-300 uppercase truncate">{item.nome_completo}</div>
<div className="text-[8px] font-mono text-zinc-600">{item.cpf}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-zinc-800 flex items-center justify-center text-zinc-500">
<Truck size={14} />
</div>
<div>
<div className="text-[11px] font-bold text-zinc-300 uppercase">{item.placa_do_cavalo}</div>
<div className="text-[8px] font-black text-zinc-600 uppercase tracking-widest">{item.tipo_de_veiculo}</div>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-white/5 flex items-center justify-between">
<div className="flex items-center gap-2 text-[9px] font-bold text-zinc-500 uppercase">
<MapPin size={10} className="text-zinc-600" />
<span>{item.origem}</span>
<ArrowRight size={10} className="text-zinc-800" />
<span>{item.destino}</span>
</div>
<button className="text-[8px] font-black text-emerald-500 hover:text-emerald-400 transition-colors flex items-center gap-1 uppercase tracking-widest">
Visualizar <Download size={10} />
</button>
</div>
</motion.div>
))
) : (
<div className="h-40 flex items-center justify-center text-zinc-800 italic uppercase font-black text-[9px] tracking-widest">
Nenhum registro no período
</div>
)}
</div>
{/* Footer Stats */}
<div className="p-8 bg-black/40 border-t border-white/5 grid grid-cols-3 gap-6">
<div className="text-center">
<div className="text-lg font-black text-white">{stats.total}</div>
<div className="text-[8px] font-black text-zinc-600 uppercase tracking-widest">Total Viagens</div>
</div>
<div className="text-center border-l border-white/10">
<div className="text-lg font-black text-emerald-500">{stats.carreta}</div>
<div className="text-[8px] font-black text-zinc-600 uppercase tracking-widest">Carretas</div>
</div>
<div className="text-center border-l border-white/10">
<div className="text-lg font-black text-blue-500">{stats.truck}</div>
<div className="text-[8px] font-black text-zinc-600 uppercase tracking-widest">Trucks</div>
</div>
</div>
</motion.div>
</motion.div>
);
};

View File

@ -1,174 +0,0 @@
import React, { useState, useEffect } from 'react';
import { X, Send, Clock, Calendar, MessageSquare, ShieldCheck, AlertCircle } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { approveTripRequest } from '../services/prafrotService';
import { getCurrentModuleUser } from '@/utils/tokenManager';
import { toast } from 'sonner';
export const AttendanceFormModal = ({ trip, onClose, onRefresh }) => {
const [loading, setLoading] = useState(false);
const user = getCurrentModuleUser();
const [formData, setFormData] = useState({
atendimento: user?.nome || user?.name || '',
situacao_liberacao: 'LIBERADO',
data_liberacao: new Date().toISOString().split('T')[0],
hora_liberacao: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
observacao: ''
});
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
await approveTripRequest({
idsolicitacoes: trip.idsolicitacoes,
status: formData.situacao_liberacao,
atendimento: formData.atendimento,
data_liberacao: formData.data_liberacao,
hora_liberacao: formData.hora_liberacao,
obs_liberacao: formData.observacao
});
toast.success('Atendimento realizado com sucesso!');
onRefresh();
onClose();
} catch (error) {
toast.error('Erro ao realizar atendimento');
} finally {
setLoading(false);
}
};
const inputStyle = "w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:border-emerald-500/50 transition-all placeholder:text-zinc-600";
const labelStyle = "text-[10px] font-bold text-zinc-500 uppercase tracking-widest mb-2 block";
return (
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 overflow-hidden">
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-lg bg-[#141416] border border-white/10 rounded-[32px] shadow-2xl overflow-hidden"
>
<div className="p-6 border-b border-white/5 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<ShieldCheck size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-white leading-none">Formulário de Atendimento</h2>
<p className="text-[10px] text-zinc-500 uppercase tracking-widest mt-1">Solicitação #{trip.idsolicitacoes}</p>
</div>
</div>
<button onClick={onClose} className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center text-zinc-500 hover:text-white transition-colors">
<X size={18} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div className="space-y-2">
<label className={labelStyle}>Atendimento (Usuário)</label>
<div className="relative">
<ShieldCheck className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
<input
type="text"
className={`${inputStyle} pl-12`}
value={formData.atendimento}
onChange={e => setFormData({...formData, atendimento: e.target.value})}
required
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className={labelStyle}>Data da Liberação</label>
<div className="relative">
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
<input
type="date"
className={`${inputStyle} pl-12`}
value={formData.data_liberacao}
onChange={e => setFormData({...formData, data_liberacao: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Hora da Liberação</label>
<div className="relative">
<Clock className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
<input
type="time"
className={`${inputStyle} pl-12`}
value={formData.hora_liberacao}
onChange={e => setFormData({...formData, hora_liberacao: e.target.value})}
required
/>
</div>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Situação da Liberação</label>
<div className="flex gap-2">
{[
{ id: 'LIBERADO', label: 'Liberado', color: 'bg-emerald-500', icon: ShieldCheck },
{ id: 'PARALIZADO', label: 'Paralizado', color: 'bg-amber-500', icon: AlertCircle }
].map(opt => (
<button
key={opt.id}
type="button"
onClick={() => setFormData({...formData, situacao_liberacao: opt.id})}
className={`flex-1 py-3 px-4 rounded-xl border flex items-center justify-center gap-2 transition-all ${
formData.situacao_liberacao === opt.id
? `border-${opt.id === 'LIBERADO' ? 'emerald' : 'amber'}-500/50 bg-${opt.id === 'LIBERADO' ? 'emerald' : 'amber'}-500/10 text-${opt.id === 'LIBERADO' ? 'emerald' : 'amber'}-500`
: 'border-white/5 bg-white/5 text-zinc-500 hover:border-white/10'
}`}
>
<opt.icon size={14} />
<span className="text-[10px] font-bold uppercase tracking-widest">{opt.label}</span>
</button>
))}
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Observações</label>
<div className="relative">
<MessageSquare className="absolute left-4 top-4 text-zinc-600" size={16} />
<textarea
className={`${inputStyle} pl-12 min-h-[100px] resize-none`}
placeholder="Descreva observações adicionais..."
value={formData.observacao}
onChange={e => setFormData({...formData, observacao: e.target.value})}
/>
</div>
</div>
<div className="pt-4">
<button
type="submit"
disabled={loading}
className="w-full py-4 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl font-bold text-xs uppercase tracking-[0.2em] transition-all shadow-lg shadow-emerald-500/20 flex items-center justify-center gap-2 disabled:opacity-50"
>
{loading ? (
<motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 1 }} className="w-5 h-5 border-2 border-white/20 border-t-white rounded-full" />
) : (
<>
<Send size={16} /> Confirmar Atendimento
</>
)}
</button>
</div>
</form>
</motion.div>
</div>
);
};

View File

@ -70,7 +70,7 @@ const AutocompleteInput = ({
<div className="relative">
<input
type="text"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-3 pr-8 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"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-3 pr-8 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"
placeholder={placeholder}
value={searchTerm}
onChange={(e) => {
@ -96,7 +96,7 @@ const AutocompleteInput = ({
<li
key={index}
className={`px-3 py-2 text-sm cursor-pointer flex items-center justify-between hover:bg-slate-100 dark:hover:bg-[#2a2a2a] ${
isSelected ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 font-medium' : 'text-slate-700 dark:text-slate-300'
isSelected ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 font-medium' : 'text-slate-700 dark:text-slate-300'
}`}
onClick={() => handleSelect(option)}
>
@ -118,3 +118,6 @@ const AutocompleteInput = ({
};
export default AutocompleteInput;

View File

@ -134,12 +134,12 @@ const ExcelTable = ({
<div className="absolute inset-0 z-50 bg-white/80 dark:bg-[#1b1b1b]/80 backdrop-blur-sm flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="w-12 h-12 border-4 border-slate-200 dark:border-[#252525] border-t-emerald-500 rounded-full animate-spin"></div>
<div className="w-12 h-12 border-4 border-slate-200 dark:border-[#252525] border-t-orange-500 rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
</div>
</div>
<span className="text-emerald-500 font-bold uppercase tracking-widest text-[10px] animate-pulse">Carregando Dados...</span>
<span className="text-orange-500 font-bold uppercase tracking-widest text-[10px] animate-pulse">Carregando Dados...</span>
</div>
</div>
)}
@ -165,10 +165,10 @@ const ExcelTable = ({
</button>
{/* Actions Button (Green Highlight) - Commented as requested
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-50 dark:bg-[#1f2824] hover:bg-emerald-100 dark:hover:bg-[#25302b] border border-emerald-200 dark:border-[#2b3832] rounded text-emerald-600 dark:text-emerald-400 font-semibold transition-colors">
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-orange-50 dark:bg-[#1f2824] hover:bg-orange-100 dark:hover:bg-[#25302b] border border-orange-200 dark:border-[#2b3832] rounded text-orange-600 dark:text-orange-400 font-semibold transition-colors">
<span className="uppercase tracking-wide text-[11px]">Ações</span>
<span className="bg-emerald-100 dark:bg-[#15201b] text-emerald-600 px-1 rounded text-[9px]">
<span className="w-2 h-2 rounded-full bg-emerald-500 inline-block"></span>
<span className="bg-orange-100 dark:bg-[#15201b] text-orange-600 px-1 rounded text-[9px]">
<span className="w-2 h-2 rounded-full bg-orange-500 inline-block"></span>
</span>
</button>
*/}
@ -248,7 +248,7 @@ const ExcelTable = ({
ref={input => { if (input) input.indeterminate = isIndeterminate; }}
onChange={handleSelectAll}
disabled={!onSelectionChange}
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-emerald-500 checked:border-emerald-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-orange-500 checked:border-orange-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
/>
</div>
</th>
@ -270,24 +270,24 @@ const ExcelTable = ({
>
<div className="flex items-center justify-between h-full w-full">
<div className="flex items-center gap-1.5">
<span className={`uppercase font-bold tracking-wider text-[11px] ${sortConfig.key === col.field ? 'text-emerald-600 dark:text-emerald-500' : 'text-slate-500 dark:text-[#e0e0e0]'}`}>
<span className={`uppercase font-bold tracking-wider text-[11px] ${sortConfig.key === col.field ? 'text-orange-600 dark:text-orange-500' : 'text-slate-500 dark:text-[#e0e0e0]'}`}>
{col.header}
</span>
</div>
{/* Sort/Menu Icons */}
<div className={`flex flex-col gap-0.5 ${sortConfig.key === col.field ? 'opacity-100' : 'opacity-40 group-hover:opacity-100'}`}>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'asc' ? 'text-emerald-600 dark:text-emerald-500' : 'text-slate-400 dark:text-stone-400'} rotate-180`}>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'asc' ? 'text-orange-600 dark:text-orange-500' : 'text-slate-400 dark:text-stone-400'} rotate-180`}>
<path d="M4 4L0 0H8L4 4Z" />
</svg>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'desc' ? 'text-emerald-600 dark:text-emerald-500' : 'text-slate-400 dark:text-stone-400'}`}>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'desc' ? 'text-orange-600 dark:text-orange-500' : 'text-slate-400 dark:text-stone-400'}`}>
<path d="M4 4L0 0H8L4 4Z" />
</svg>
</div>
</div>
{/* Column Resizer Handle */}
<div className="absolute right-0 top-0 bottom-0 w-[4px] cursor-col-resize hover:bg-emerald-500/50 z-10 translate-x-1/2" onClick={(e) => e.stopPropagation()} />
<div className="absolute right-0 top-0 bottom-0 w-[4px] cursor-col-resize hover:bg-orange-500/50 z-10 translate-x-1/2" onClick={(e) => e.stopPropagation()} />
</th>
))}
{/* Spacer for scrollbar */}
@ -319,7 +319,7 @@ const ExcelTable = ({
checked={selectedIds.includes(row[rowKey])}
onChange={() => handleSelectRow(row[rowKey])}
disabled={!onSelectionChange}
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-emerald-500 checked:border-emerald-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-orange-500 checked:border-orange-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
/>
</div>
</td>
@ -330,7 +330,7 @@ const ExcelTable = ({
<div className="flex items-center justify-center h-full w-full gap-2">
<button
onClick={() => onEdit && onEdit(row)}
className="text-slate-500 dark:text-stone-500 hover:text-emerald-600 dark:hover:text-stone-300 transition-colors p-1"
className="text-slate-500 dark:text-stone-500 hover:text-orange-600 dark:hover:text-stone-300 transition-colors p-1"
>
<Edit2 size={16} strokeWidth={2} />
</button>
@ -375,7 +375,7 @@ const ExcelTable = ({
{/* Left: Totals */}
<div className="flex h-full items-center gap-4">
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[120px]">
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-emerald-500 rounded-r-sm"></div>
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-orange-500 rounded-r-sm"></div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Total:</span>
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{processedData.length}</span>
@ -383,7 +383,7 @@ const ExcelTable = ({
</div>
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[100px]">
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-emerald-500 rounded-r-sm opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-orange-500 rounded-r-sm opacity-0 group-hover:opacity-100 transition-opacity"></div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Página:</span>
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{currentPage} / {totalPages || 1}</span>
</div>
@ -399,7 +399,7 @@ const ExcelTable = ({
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-emerald-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronsLeft size={14} />
</button>
@ -408,14 +408,14 @@ const ExcelTable = ({
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-emerald-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={14} />
</button>
{/* Manual Page Buttons (Simple logic) */}
<div className="flex gap-1 mx-1">
<button className="w-7 h-7 flex items-center justify-center rounded bg-emerald-500 text-white dark:text-black font-bold text-[11px] transition-colors shadow-lg shadow-emerald-500/10">
<button className="w-7 h-7 flex items-center justify-center rounded bg-orange-500 text-white dark:text-black font-bold text-[11px] transition-colors shadow-lg shadow-orange-500/10">
{currentPage}
</button>
</div>
@ -424,7 +424,7 @@ const ExcelTable = ({
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-emerald-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight size={14} />
</button>
@ -433,7 +433,7 @@ const ExcelTable = ({
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-emerald-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronsRight size={14} />
</button>
@ -454,3 +454,6 @@ const ExcelTable = ({
};
export default ExcelTable;

View File

@ -24,14 +24,14 @@ export const useFeedbackStore = create((set) => ({
}));
const icons = {
success: <CheckCircle2 className="w-6 h-6 text-emerald-500" />,
success: <CheckCircle2 className="w-6 h-6 text-orange-500" />,
error: <XCircle className="w-6 h-6 text-rose-500" />,
warning: <AlertCircle className="w-6 h-6 text-amber-500" />,
info: <Info className="w-6 h-6 text-blue-500" />
};
const colors = {
success: "border-emerald-500/20 bg-emerald-50/50 dark:bg-emerald-500/10",
success: "border-orange-500/20 bg-orange-50/50 dark:bg-orange-500/10",
error: "border-rose-500/20 bg-rose-50/50 dark:bg-rose-500/10",
warning: "border-amber-500/20 bg-amber-50/50 dark:bg-amber-500/10",
info: "border-blue-500/20 bg-blue-50/50 dark:bg-blue-500/10"
@ -52,7 +52,7 @@ export const FeedbackContainer = () => {
className={`pointer-events-auto min-w-[320px] max-w-[400px] p-4 rounded-2xl border backdrop-blur-md shadow-2xl flex gap-4 relative overflow-hidden group ${colors[n.type]}`}
>
{/* Background Glow */}
<div className={`absolute -right-4 -top-4 w-24 h-24 blur-3xl opacity-20 group-hover:opacity-40 transition-opacity ${n.type === 'success' ? 'bg-emerald-500' : n.type === 'error' ? 'bg-rose-500' : 'bg-blue-500'}`}></div>
<div className={`absolute -right-4 -top-4 w-24 h-24 blur-3xl opacity-20 group-hover:opacity-40 transition-opacity ${n.type === 'success' ? 'bg-orange-500' : n.type === 'error' ? 'bg-rose-500' : 'bg-blue-500'}`}></div>
<div className="flex-shrink-0 mt-0.5">
{icons[n.type]}
@ -79,7 +79,7 @@ export const FeedbackContainer = () => {
initial={{ scaleX: 1 }}
animate={{ scaleX: 0 }}
transition={{ duration: Math.min((n.duration || 5000) / 1000, 30), ease: "linear" }}
className={`absolute bottom-0 left-0 right-0 h-1 origin-left ${n.type === 'success' ? 'bg-emerald-500' : n.type === 'error' ? 'bg-rose-500' : 'bg-blue-500'}`}
className={`absolute bottom-0 left-0 right-0 h-1 origin-left ${n.type === 'success' ? 'bg-orange-500' : n.type === 'error' ? 'bg-rose-500' : 'bg-blue-500'}`}
/>
</motion.div>
))}
@ -110,3 +110,6 @@ export const useFeedback = () => {
}
};
};

View File

@ -121,7 +121,7 @@ const FinesCardDebug = (initialProps) => {
<div className="flex-1 bg-slate-50 dark:bg-slate-900/50 p-6 rounded-xl border border-slate-200 dark:border-white/10 space-y-6">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider">Debug Controls</h4>
<Button variant="ghost" size="icon" onClick={() => setState(prev => ({ ...prev, isLoading: !prev.isLoading }))} className={state.isLoading ? "text-emerald-500" : "text-slate-500 hover:text-slate-900 dark:hover:text-slate-300"}>
<Button variant="ghost" size="icon" onClick={() => setState(prev => ({ ...prev, isLoading: !prev.isLoading }))} className={state.isLoading ? "text-orange-500" : "text-slate-500 hover:text-slate-900 dark:hover:text-slate-300"}>
<RefreshCcw className={state.isLoading ? "animate-spin" : ""} size={16} />
</Button>
</div>
@ -137,7 +137,7 @@ const FinesCardDebug = (initialProps) => {
type="checkbox"
checked={state.hasData}
onChange={toggleData}
className="w-5 h-5 rounded border-slate-300 dark:border-slate-600 bg-slate-100 dark:bg-slate-700 text-emerald-500 focus:ring-emerald-500 focus:ring-offset-white dark:focus:ring-offset-slate-900"
className="w-5 h-5 rounded border-slate-300 dark:border-slate-600 bg-slate-100 dark:bg-slate-700 text-orange-500 focus:ring-orange-500 focus:ring-offset-white dark:focus:ring-offset-slate-900"
/>
</div>
</div>
@ -154,7 +154,7 @@ const FinesCardDebug = (initialProps) => {
step="100"
value={state.previousValue}
onChange={updatePrevValue}
className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500"
className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-orange-500"
/>
</div>
</div>
@ -169,3 +169,6 @@ const FinesCardDebug = (initialProps) => {
};
export default FinesCardDebug;

View File

@ -43,8 +43,8 @@ export const FinesCard = ({
const isReduction = percentage < 0;
const isIncrease = percentage > 0;
const trendColor = isReduction ? 'text-emerald-500' : isIncrease ? 'text-rose-500' : 'text-slate-500';
const badgeColor = isReduction ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' : isIncrease ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' : 'bg-slate-500/10 text-slate-500 border-slate-500/20';
const trendColor = isReduction ? 'text-orange-500' : isIncrease ? 'text-rose-500' : 'text-slate-500';
const badgeColor = isReduction ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' : isIncrease ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' : 'bg-slate-500/10 text-slate-500 border-slate-500/20';
const TrendIcon = isReduction ? TrendingDown : isIncrease ? TrendingUp : Minus;
const isEmpty = !data || data.length === 0 || currentValue === 0;
@ -84,7 +84,7 @@ export const FinesCard = ({
</div>
<div className="text-right">
<div className={cn("text-2xl font-bold tracking-tight leading-none", currentValue > 0 ? "text-slate-900 dark:text-white" : "text-emerald-600 dark:text-emerald-500")}>
<div className={cn("text-2xl font-bold tracking-tight leading-none", currentValue > 0 ? "text-slate-900 dark:text-white" : "text-orange-600 dark:text-orange-500")}>
{formatCurrency(currentValue)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 font-medium mt-1">{currentCount} registros</div>
@ -124,7 +124,7 @@ export const FinesCard = ({
className={cn(
"flex-1 py-1.5 rounded-md text-[10px] uppercase font-bold tracking-wider transition-all",
period === p
? "bg-white dark:bg-emerald-500 text-emerald-600 dark:text-white shadow-sm ring-1 ring-black/5 dark:ring-0"
? "bg-white dark:bg-orange-500 text-orange-600 dark:text-white shadow-sm ring-1 ring-black/5 dark:ring-0"
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-black/5 dark:hover:bg-white/5"
)}
>
@ -262,3 +262,6 @@ export const FinesCard = ({
);
};

View File

@ -1,2 +1,5 @@
export { FinesCard } from './FinesCard';
export { FinesCard as default } from './FinesCard';

View File

@ -1,278 +0,0 @@
import React, { useState } from 'react';
import {
LayoutDashboard, Package, DollarSign, Truck,
MapPin, Calendar, FileText, Hash, CreditCard,
Scale, Box, AlertCircle
} from 'lucide-react';
import { motion } from 'framer-motion';
const renderValue = (val, type = 'text') => {
if (val === null || val === undefined || val === '') return '---';
if (type === 'money') {
return `R$ ${parseFloat(val).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
if (type === 'date') {
return new Date(val).toLocaleDateString('pt-BR');
}
if (type === 'datetime') {
return new Date(val).toLocaleString('pt-BR');
}
if (type === 'number') {
return parseFloat(val).toLocaleString('pt-BR', { maximumFractionDigits: 4 });
}
return val;
};
const SectionHeader = ({ icon: Icon, title }) => (
<div className="flex items-center gap-2 mb-4 pb-2 border-b border-white/5">
<div className="p-1.5 bg-emerald-500/10 rounded-lg text-emerald-500">
<Icon size={16} />
</div>
<h3 className="text-xs font-bold uppercase tracking-widest text-emerald-500">{title}</h3>
</div>
);
const Field = ({ label, value, type = 'text', fullWidth = false }) => (
<div className={`flex flex-col gap-1 p-3 bg-white/5 rounded-xl border border-white/5 ${fullWidth ? 'col-span-full' : ''}`}>
<span className="text-[9px] font-bold text-zinc-500 uppercase tracking-widest truncate">{label}</span>
<span className={`text-xs font-medium text-zinc-200 break-all ${type === 'money' ? 'font-mono text-emerald-400' : ''}`}>
{renderValue(value, type)}
</span>
</div>
);
export const NfeDataDisplay = ({ data }) => {
const [activeTab, setActiveTab] = useState('geral');
if (!data) return null;
// Normaliza o objeto NFe se estiver aninhado ou na raiz
const nfe = data.nfe || data;
const itens = data.itens || [];
const financeiro = data.financeiro || []; // Faturas?
const pagamentos = data.pagamentos || [];
const transportadora = data.transportadora || {};
const tabs = [
{ id: 'geral', label: 'Visão Geral', icon: LayoutDashboard },
{ id: 'itens', label: 'Itens & Produtos', icon: Package, count: itens.length },
{ id: 'financeiro', label: 'Financeiro', icon: DollarSign },
{ id: 'transporte', label: 'Transporte', icon: Truck },
];
return (
<div className="flex flex-col h-full min-h-0 bg-[#141416] text-white pt-6 relative z-10">
{/* Tabs */}
{/* Tabs */}
<div className="flex items-center gap-2 p-4 bg-black/20 border-b border-white/5 overflow-x-auto md:overflow-hidden [&::-webkit-scrollbar]:hidden" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all whitespace-nowrap border ${
activeTab === tab.id
? 'bg-zinc-800 text-emerald-500 border-white/10 shadow-lg'
: 'border-transparent text-zinc-500 hover:text-zinc-300 hover:bg-white/5'
}`}
>
<tab.icon size={14} />
{tab.label}
{tab.count !== undefined && (
<span className="ml-1 px-1.5 py-0.5 bg-black/40 rounded text-[9px] text-zinc-400">{tab.count}</span>
)}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-6">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{activeTab === 'geral' && (
<div className="space-y-6">
{/* Identificação */}
<div>
<SectionHeader icon={FileText} title="Identificação da Nota" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Número" value={nfe.numero} fullWidth />
<Field label="Série" value={nfe.serie} />
<Field label="Modelo" value={nfe.modelo} />
<Field label="Natureza da Operação" value={nfe.natureza_operacao} fullWidth />
<Field label="Chave de Acesso" value={nfe.chave} fullWidth />
<Field label="Ambiente" value={nfe.ambiente} />
<Field label="Finalidade" value={nfe.finalidade_nf} />
<Field label="Tipo NF" value={nfe.tipo_nf} />
</div>
</div>
{/* Datas */}
<div>
<SectionHeader icon={Calendar} title="Datas e Horários" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Emissão" value={nfe.data_emissao} type="datetime" />
<Field label="Saída/Entrada" value={nfe.data_saida} type="datetime" />
<Field label="Importado em" value={nfe.criado_em} type="datetime" />
</div>
</div>
{/* Participantes */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<SectionHeader icon={MapPin} title="Emitente" />
<div className="space-y-3">
<Field label="Nome / Razão Social" value={nfe.emitente_nome} />
<Field label="CNPJ / CPF" value={nfe.emitente_cnpj} />
</div>
</div>
<div>
<SectionHeader icon={MapPin} title="Destinatário" />
<div className="space-y-3">
<Field label="Nome / Razão Social" value={nfe.destinatario_nome} />
<Field label="CNPJ / CPF" value={nfe.destinatario_doc} />
</div>
</div>
</div>
{/* Totais */}
<div>
<SectionHeader icon={DollarSign} title="Valores Totais" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Valor Produtos" value={nfe.valor_produtos} type="money" />
<Field label="Valor Total NF" value={nfe.valor_total} type="money" />
<Field label="Valor Frete" value={nfe.valor_frete} type="money" />
<Field label="Valor Seguro" value={nfe.valor_seguro} type="money" />
<Field label="Valor Desconto" value={nfe.valor_desconto} type="money" />
<Field label="Outras Despesas" value={nfe.valor_outras_despesas || null} type="money" />
</div>
</div>
{/* Impostos Totais */}
<div>
<SectionHeader icon={Scale} title="Totais de Impostos" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Base ICMS" value={nfe.base_icms} type="money" />
<Field label="Valor ICMS" value={nfe.valor_icms} type="money" />
<Field label="Base ICMS ST" value={nfe.base_icms_st} type="money" />
<Field label="Valor ICMS ST" value={nfe.valor_icms_st} type="money" />
<Field label="Valor IPI" value={nfe.valor_ipi} type="money" />
<Field label="Valor PIS" value={nfe.valor_pis} type="money" />
<Field label="Valor COFINS" value={nfe.valor_cofins} type="money" />
<Field label="Valor Aprox. Trib." value={null} type="money" />
</div>
</div>
</div>
)}
{activeTab === 'itens' && (
<div className="space-y-4">
<div className="overflow-x-auto rounded-2xl border border-white/5">
<table className="w-full text-left border-collapse whitespace-nowrap">
<thead>
<tr className="bg-black/20 border-b border-white/5">
{['#', 'Cód.', 'Descrição', 'NCM', 'CST', 'CFOP', 'Unid.', 'Qtde.', 'V. Unit.', 'V. Total', 'BC ICMS', 'V. ICMS', 'V. IPI', 'V. PIS', 'V. COFINS'].map(h => (
<th key={h} className="px-4 py-3 text-[9px] font-bold text-zinc-500 uppercase tracking-widest">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-white/5 bg-white/5">
{itens.map((item, idx) => (
<tr key={idx} className="hover:bg-white/5 transition-colors">
<td className="px-4 py-3 text-[10px] text-zinc-500">{idx + 1}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-400">{item.codigo}</td>
<td className="px-4 py-3 text-[10px] font-medium text-white max-w-[200px] truncate" title={item.descricao}>{item.descricao}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-400">{item.ncm || '---'}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-400">{item.cst || '---'}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-400">{item.cfop || '---'}</td>
<td className="px-4 py-3 text-[10px] text-zinc-400">{item.unidade || '---'}</td>
<td className="px-4 py-3 text-[10px] font-bold text-white">{renderValue(item.quantidade, 'number')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-300">{renderValue(item.valor_unitario, 'money')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-emerald-400 font-bold">{renderValue(item.valor_total, 'money')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-500">{renderValue(item.base_icms, 'money')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-500">{renderValue(item.valor_icms, 'money')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-500">{renderValue(item.valor_ipi, 'money')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-500">{renderValue(item.valor_pis, 'money')}</td>
<td className="px-4 py-3 text-[10px] font-mono text-zinc-500">{renderValue(item.valor_cofins, 'money')}</td>
</tr>
))}
</tbody>
</table>
</div>
{itens.length === 0 && (
<div className="p-10 text-center text-zinc-500 text-xs uppercase tracking-widest italic border border-dashed border-white/5 rounded-2xl">
Nenhum item registrado
</div>
)}
</div>
)}
{activeTab === 'financeiro' && (
<div className="space-y-6">
{/* Pagamentos */}
<div>
<SectionHeader icon={CreditCard} title="Pagamentos" />
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{pagamentos.map((pag, idx) => (
<div key={idx} className="p-4 bg-white/5 border border-white/5 rounded-2xl space-y-3">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest">Pagamento #{idx + 1}</span>
<span className="text-[10px] font-mono text-zinc-600">{pag.forma || '---'}</span>
</div>
<div className="text-xl font-bold text-emerald-400 font-mono">
{renderValue(pag.valor, 'money')}
</div>
</div>
))}
{pagamentos.length === 0 && (
<div className="col-span-full p-6 text-center text-zinc-500 text-xs italic">
Nenhum pagamento registrado
</div>
)}
</div>
</div>
{/* Faturas / Duplicatas (se houver no JSON futuro) */}
<div>
<SectionHeader icon={FileText} title="Faturas / Duplicatas" />
<div className="p-6 text-center text-zinc-500 text-xs italic border border-dashed border-white/5 rounded-2xl">
Dados de fatura não disponíveis na visualização simplificada.
</div>
</div>
</div>
)}
{activeTab === 'transporte' && (
<div className="space-y-6">
<div>
<SectionHeader icon={Truck} title="Transportadora" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field label="Nome / Razão Social" value={transportadora.transportadora_nome} />
<Field label="CNPJ" value={transportadora.transportadora_cnpj} />
<Field label="Placa Veículo" value={transportadora.placa} />
<Field label="UF Veículo" value={transportadora.uf} />
</div>
</div>
<div>
<SectionHeader icon={Box} title="Volumes e Pesos" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Field label="Quantidade" value={transportadora.quantidade_volumes} />
<Field label="Espécie" value={null} />
<Field label="Marca" value={null} />
<Field label="Numeração" value={null} />
<Field label="Peso Bruto" value={transportadora.peso_bruto} type="number" />
<Field label="Peso Líquido" value={transportadora.peso_liquido} type="number" />
</div>
</div>
</div>
)}
</motion.div>
</div>
</div>
);
};

View File

@ -1,107 +0,0 @@
import React, { useState, useEffect } from 'react';
import { X, Loader2, Download, Maximize2, Minimize2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { getNfe } from '../services/prafrotService';
export const NfeViewerModal = ({ chave, onClose }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isFullscreen, setIsFullscreen] = useState(false);
// Consideramos que o backend retorna o HTML da DANFE
// Se mudar para PDF, ajustaremos o iframe
const nfeUrl = `${window.location.protocol}//${window.location.host}/api/nfe/${chave}`;
return (
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/90 backdrop-blur-md"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{
opacity: 1,
scale: 1,
y: 0,
width: isFullscreen ? '100%' : '90%',
height: isFullscreen ? '100%' : '90%'
}}
className={`relative bg-white rounded-[32px] overflow-hidden flex flex-col transition-all duration-300 ${isFullscreen ? 'rounded-none' : ''}`}
>
{/* Header */}
<div className="bg-[#141416] p-4 flex items-center justify-between border-b border-white/5">
<div className="flex items-center gap-4">
<div className="bg-emerald-500/10 p-2 rounded-xl">
<span className="text-emerald-500 text-xs font-bold uppercase tracking-widest">DANFE Online</span>
</div>
<span className="text-[10px] font-mono text-zinc-500 hidden md:block">{chave}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="w-10 h-10 bg-white/5 hover:bg-white/10 rounded-full flex items-center justify-center text-zinc-400 hover:text-white transition-all"
title={isFullscreen ? "Sair da Tela Cheia" : "Tela Cheia"}
>
{isFullscreen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
</button>
<button
onClick={() => window.open(nfeUrl, '_blank')}
className="w-10 h-10 bg-white/5 hover:bg-white/10 rounded-full flex items-center justify-center text-zinc-400 hover:text-white transition-all"
title="Baixar / Imprimir"
>
<Download size={18} />
</button>
<button
onClick={onClose}
className="w-10 h-10 bg-rose-500/10 hover:bg-rose-500/20 rounded-full flex items-center justify-center text-rose-500 transition-all ml-2"
>
<X size={20} />
</button>
</div>
</div>
{/* Viewer */}
<div className="flex-1 bg-zinc-100 relative overflow-hidden">
{loading && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-100 z-10">
<Loader2 className="animate-spin text-emerald-500 mb-4" size={40} />
<span className="text-sm font-medium text-zinc-600">Carregando DANFE...</span>
</div>
)}
<iframe
src={nfeUrl}
className="w-full h-full border-0"
onLoad={() => setLoading(false)}
onError={() => {
setLoading(false);
setError("Erro ao carregar a nota fiscal.");
}}
/>
{error && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-100 z-20 p-8 text-center">
<div className="w-16 h-16 bg-rose-500/10 text-rose-500 rounded-full flex items-center justify-center mb-4">
<X size={32} />
</div>
<h3 className="text-xl font-bold text-zinc-900 mb-2">Ops! Algo deu errado</h3>
<p className="text-zinc-500 max-w-sm">{error}</p>
<button
onClick={onClose}
className="mt-6 px-8 py-3 bg-[#141416] text-white rounded-2xl font-bold uppercase text-[10px] tracking-widest"
>
Fechar Visualizador
</button>
</div>
)}
</div>
</motion.div>
</div>
);
};

View File

@ -1,125 +0,0 @@
import React, { useState, useEffect } from 'react';
import { X, Plus, Trash2, Loader2, Settings, ShieldCheck } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { getOperations, createOperation, deleteOperation } from '../services/prafrotService';
import { toast } from 'sonner';
export const OperationsManagerModal = ({ onClose }) => {
const [operations, setOperations] = useState([]);
const [loading, setLoading] = useState(true);
const [newOp, setNewOp] = useState('');
const [submitting, setSubmitting] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const data = await getOperations();
setOperations(Array.isArray(data) ? data : data?.data || []);
} catch (error) {
toast.error('Erro ao carregar operações');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleCreate = async (e) => {
e.preventDefault();
if (!newOp.trim()) return;
setSubmitting(true);
try {
await createOperation(newOp);
toast.success('Operação criada');
setNewOp('');
fetchData();
} catch (error) {
toast.error('Erro ao criar');
} finally {
setSubmitting(false);
}
};
const handleDelete = async (id) => {
if (!window.confirm('Excluir operação?')) return;
try {
await deleteOperation(id);
toast.success('Operação excluída');
fetchData();
} catch (error) {
toast.error('Erro ao excluir');
}
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 overflow-hidden">
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="relative w-full max-w-md bg-[#161618] border border-white/10 rounded-[32px] shadow-2xl overflow-hidden"
>
<div className="p-6 border-b border-white/5 flex items-center justify-between">
<div className="flex items-center gap-2">
<Settings size={18} className="text-emerald-400" />
<h2 className="text-lg font-bold text-white">Gestão de Operações</h2>
</div>
<button onClick={onClose} className="text-zinc-500 hover:text-white transition-colors">
<X size={20} />
</button>
</div>
<div className="p-6 space-y-6">
{/* Form */}
<form onSubmit={handleCreate} className="flex gap-2">
<input
type="text"
placeholder="Nova Operação (ex: AMBEV)"
className="flex-1 bg-black/40 border border-white/5 rounded-xl px-4 py-3 text-xs font-bold focus:outline-none focus:border-emerald-500/30 transition-all"
value={newOp}
onChange={e => setNewOp(e.target.value.toUpperCase())}
/>
<button
disabled={submitting}
className="px-4 py-3 bg-emerald-600 hover:bg-emerald-500 rounded-xl text-white font-bold text-[10px] uppercase tracking-widest transition-all disabled:opacity-50"
>
{submitting ? <Loader2 className="animate-spin" size={16} /> : <Plus size={16} />}
</button>
</form>
{/* List */}
<div className="max-h-[300px] overflow-y-auto custom-scrollbar space-y-2 pr-2">
{loading ? (
<div className="flex justify-center py-10"><Loader2 className="animate-spin text-zinc-700" size={32} /></div>
) : operations.length === 0 ? (
<div className="text-center py-10 text-zinc-600 font-bold uppercase text-[10px] tracking-widest italic">Nenhuma operação cadastrada</div>
) : (
operations.map(op => (
<div key={op.idoperacoes_solicitacao} className="flex items-center justify-between p-4 bg-white/5 border border-white/5 rounded-2xl group hover:border-white/10 transition-all">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-800 flex items-center justify-center text-zinc-500 group-hover:text-emerald-400"><ShieldCheck size={16} /></div>
<span className="text-xs font-bold text-zinc-300">{op.operacao}</span>
</div>
<button
onClick={() => handleDelete(op.idoperacoes_solicitacao)}
className="p-2 text-zinc-600 hover:text-red-500 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
))
)}
</div>
</div>
</motion.div>
</div>
);
};

View File

@ -52,7 +52,7 @@
border: 1px solid var(--pfs-border);
}
/* Subtle Top Glow - Emerald */
/* Subtle Top Glow - orange */
.pfs-container::before {
content: '';
position: absolute;
@ -60,7 +60,7 @@
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, #10b981, transparent);
background: linear-gradient(90deg, transparent, #f97316, transparent);
opacity: 0.3;
}
@ -97,8 +97,8 @@
.pfs-toggle-btn:hover {
background: var(--pfs-bg-search);
color: #10b981;
border-color: #10b981;
color: #f97316;
border-color: #f97316;
transform: scale(1.05);
}
@ -128,9 +128,9 @@
}
.pfs-search-input:focus {
border-color: #10b981;
border-color: #f97316;
background: var(--pfs-bg);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.1);
}
/* Navigation Content */
@ -175,10 +175,10 @@
}
.pfs-link.active {
background: #10b981 !important;
background: #f97316 !important;
color: var(--pfs-text-active) !important;
font-weight: 700;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.25);
box-shadow: 0 4px 15px rgba(249, 115, 22, 0.25);
}
.pfs-icon {
@ -250,7 +250,7 @@
}
.pfs-sublink.active {
color: #10b981;
color: #f97316;
font-weight: 700;
}
@ -261,7 +261,7 @@
.pfs-sublink.active .pfs-icon {
opacity: 1;
color: #10b981;
color: #f97316;
}
/* Footer Section */
@ -289,14 +289,14 @@
.pfs-brand-logo {
width: 36px;
height: 36px;
background: #10b981;
background: #f97316;
color: #141414;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.2);
box-shadow: 0 0 15px rgba(249, 115, 22, 0.2);
flex-shrink: 0;
}
@ -314,7 +314,7 @@
}
.pfs-brand-name span {
color: #10b981;
color: #f97316;
}
.pfs-app-sub {
@ -385,7 +385,7 @@
}
.pfs-legal-item svg, .pfs-version svg {
color: #10b981;
color: #f97316;
opacity: 0.6;
}
@ -397,5 +397,8 @@
.pfs-lock-icon {
margin-left: auto;
color: #10b981 !important;
color: #f97316 !important;
}

View File

@ -1,11 +1,11 @@
import React, { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
ChevronLeft,
ChevronRight,
Search,
ChevronDown,
import {
ChevronLeft,
ChevronRight,
Search,
ChevronDown,
LayoutDashboard,
Car,
Users,
@ -21,12 +21,12 @@ import {
Award,
GitBranch,
Lock,
Settings,
Mail
Settings
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAuthContext } from '@/components/shared/AuthProvider';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import logoOestePan from '@/assets/Img/Util/Oeste_Pan/Logo Oeste Pan.png';
import './PrafrotSidebar.css';
const MENU_ITEMS = [
@ -34,17 +34,17 @@ const MENU_ITEMS = [
id: 'dashboard',
label: 'Estatísticas',
icon: LayoutDashboard,
path: '/plataforma/prafrot/estatisticas'
path: '/plataforma/oest-pan/estatisticas'
},
{
id: 'cadastros',
label: 'Cadastros',
icon: ClipboardList,
children: [
{ id: 'c-veiculos', label: 'Veículos', path: '/plataforma/prafrot/veiculos', icon: Car },
{ id: 'c-dispatcher', label: 'Dispatcher', path: '/plataforma/prafrot/dispatcher', icon: ClipboardList },
// { id: 'c-motoristas', label: 'Motoristas', path: '/plataforma/prafrot/motoristas', icon: Users, disabled: true, disabledReason: 'Funcionalidade em manutenção' },
{ id: 'c-oficinas', label: 'Oficinas', path: '/plataforma/prafrot/oficinas', icon: Store }
{ id: 'c-veiculos', label: 'Veículos', path: '/plataforma/oest-pan/veiculos', icon: Car },
{ id: 'c-dispatcher', label: 'Dispatcher', path: '/plataforma/oest-pan/dispatcher', icon: ClipboardList },
// { id: 'c-motoristas', label: 'Motoristas', path: '/plataforma/oest-pan/motoristas', icon: Users, disabled: true, disabledReason: 'Funcionalidade em manutenção' },
{ id: 'c-oficinas', label: 'Oficinas', path: '/plataforma/oest-pan/oficinas', icon: Store }
]
},
{
@ -52,20 +52,17 @@ const MENU_ITEMS = [
label: 'Gerência',
icon: Activity,
children: [
{ id: 'g-solicitacoes', label: 'Solicitações', path: '/plataforma/prafrot/solicitacoes', icon: ShieldAlert },
{ id: 'g-monitoramento', label: 'Monitoramento', path: '/plataforma/prafrot/monitoramento', icon: Radio },
{ id: 'g-status', label: 'Status Frota', path: '/plataforma/prafrot/status', icon: Activity },
{ id: 'g-manutencao', label: 'Manutenção', path: '/plataforma/prafrot/manutencao', icon: Wrench },
{ id: 'g-pendencias-financeiro', label: 'Pendências', path: '/plataforma/prafrot/pendencias-financeiro', icon: ShieldAlert },
{ id: 'g-sinistros', label: 'Sinistros', path: '/plataforma/prafrot/sinistros', icon: AlertTriangle },
{ id: 'g-mensagens', label: 'Processador de XML', path: '/plataforma/prafrot/mensagens', icon: Mail }
{ id: 'g-monitoramento', label: 'Monitoramento', path: '/plataforma/oest-pan/monitoramento', icon: Radio },
{ id: 'g-status', label: 'Status Frota', path: '/plataforma/oest-pan/status', icon: Activity },
{ id: 'g-manutencao', label: 'Manutenção', path: '/plataforma/oest-pan/manutencao', icon: Wrench },
{ id: 'g-sinistros', label: 'Sinistros', path: '/plataforma/oest-pan/sinistros', icon: AlertTriangle }
]
},
{
id: 'config',
label: 'Configurações',
icon: Settings,
path: '/plataforma/prafrot/configuracoes'
path: '/plataforma/oest-pan/configuracoes'
}
];
@ -78,12 +75,12 @@ const MenuItem = React.memo(({ item, isSub = false, isCollapsed, expandedItems,
const isLocked = item.disabled;
// Filter sub-items if searching
const subItems = item.children?.filter(child =>
const subItems = item.children?.filter(child =>
!searchTerm || child.label.toLowerCase().includes(searchTerm.toLowerCase())
);
const content = (
<div
<div
className={cn(
isSub ? "pfs-sublink" : "pfs-link",
isActive && !hasChildren && "active",
@ -97,11 +94,11 @@ const MenuItem = React.memo(({ item, isSub = false, isCollapsed, expandedItems,
>
<Icon size={isSub ? 16 : 20} className="pfs-icon" />
{(!isCollapsed || isSub) && <span className="pfs-label">{item.label}</span>}
{hasChildren && !isCollapsed && (
<ChevronDown
size={14}
className={cn("pfs-chevron", isExpanded && "expanded")}
<ChevronDown
size={14}
className={cn("pfs-chevron", isExpanded && "expanded")}
/>
)}
@ -121,17 +118,17 @@ const MenuItem = React.memo(({ item, isSub = false, isCollapsed, expandedItems,
<AnimatePresence>
{hasChildren && isExpanded && !isCollapsed && (
<motion.div
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="pfs-submenu"
>
{subItems?.map(child => (
<MenuItem
key={child.id}
item={child}
isSub
<MenuItem
key={child.id}
item={child}
isSub
isCollapsed={isCollapsed}
expandedItems={expandedItems}
toggleExpand={toggleExpand}
@ -148,7 +145,7 @@ const MenuItem = React.memo(({ item, isSub = false, isCollapsed, expandedItems,
MenuItem.displayName = 'MenuItem';
export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
export const OestPanSidebar = ({ isCollapsed, onToggle }) => {
const [searchTerm, setSearchTerm] = useState('');
const [expandedItems, setExpandedItems] = useState({ cadastros: true, gerencia: true });
const location = useLocation();
@ -162,69 +159,20 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
}, []);
const filteredItems = useMemo(() => {
// 1. Identificação de Setores (Alinhada com integra_user_prafrot via context)
const userSetores = user?.setores || [];
if (!searchTerm) return MENU_ITEMS;
// Tenta obter especificamente do usuário prafrot caso o contexto global divirja
let allSetores = [...userSetores];
try {
const prafrotUserJson = localStorage.getItem('integra_user_prafrot');
if (prafrotUserJson) {
const prafrotUser = JSON.parse(prafrotUserJson);
if (prafrotUser.setores) {
allSetores = [...new Set([...allSetores, ...prafrotUser.setores])];
}
}
} catch (e) {
console.warn('PrafrotSidebar: Erro ao ler integra_user_prafrot');
}
const isFinanceiro = allSetores.includes('Financeiro');
const isMonitoramento = allSetores.includes('Monitoramento');
let baseItems = MENU_ITEMS;
// Lógica de Filtro
if (isFinanceiro) {
// Usuário Financeiro (mesmo que tenha Monitoramento): Vê apenas Pendências e Configurações
baseItems = MENU_ITEMS.filter(item => {
if (item.id === 'config') return true;
if (item.id === 'gerencia') {
return item.children?.some(c => c.id === 'g-pendencias-financeiro');
}
return false;
}).map(item => {
if (item.id === 'gerencia') {
return { ...item, children: item.children.filter(c => c.id === 'g-pendencias-financeiro') };
}
return item;
});
} else if (isMonitoramento) {
// Usuário apenas Monitoramento: Oculta Pendências Financeiras
baseItems = MENU_ITEMS.map(item => {
if (item.id === 'gerencia') {
return { ...item, children: item.children.filter(c => c.id !== 'g-pendencias-financeiro') };
}
return item;
});
}
// Caso não tenha nenhum dos dois especificamente, mostra tudo (fallback admin)
// 2. Filtro de Busca
if (!searchTerm) return baseItems;
return baseItems.filter(item => {
return MENU_ITEMS.filter(item => {
const matchParent = item.label.toLowerCase().includes(searchTerm.toLowerCase());
const matchChildren = item.children?.some(child =>
const matchChildren = item.children?.some(child =>
child.label.toLowerCase().includes(searchTerm.toLowerCase())
);
return matchParent || matchChildren;
});
}, [searchTerm, user]);
}, [searchTerm]);
const handleLogout = () => {
logout('prafrot');
window.location.href = '/plataforma/prafrot/login';
logout('auth_oestepan');
window.location.href = '/plataforma/oest-pan/login';
};
return (
@ -238,9 +186,9 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
<div className="pfs-search-wrapper">
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--pfs-text-muted)]" />
<input
type="text"
className="pfs-search-input"
<input
type="text"
className="pfs-search-input"
placeholder="Buscar..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
@ -250,9 +198,9 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
<nav className="pfs-nav-content custom-scrollbar">
{filteredItems.map(item => (
<MenuItem
key={item.id}
item={item}
<MenuItem
key={item.id}
item={item}
isCollapsed={isCollapsed}
expandedItems={expandedItems}
toggleExpand={toggleExpand}
@ -264,12 +212,10 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
<footer className="pfs-footer">
<div className="pfs-brand">
<div className="pfs-brand-logo">
PF
</div>
<img src={logoOestePan} alt="OP" className="w-12 h-12 object-contain" />
{!isCollapsed && (
<div className="pfs-brand-info">
<span className="pfs-brand-name">PRA<span>FROTA</span></span>
<span className="pfs-brand-name">Oeste <span>Pan</span></span>
<span className="pfs-app-sub">Fleet Management</span>
</div>
)}
@ -278,8 +224,8 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
<div className="pfs-user-section">
<div className={cn("pfs-user-card", isCollapsed && "justify-center")}>
<Avatar className="h-8 w-8 border border-[var(--pfs-border)]">
<AvatarImage src={`https://ui-avatars.com/api/?name=${user?.name || 'User'}&background=10b981&color=141414`} />
<AvatarFallback className="bg-emerald-500 text-zinc-950 font-bold text-xs">
<AvatarImage src={`https://ui-avatars.com/api/?name=${user?.name || 'User'}&background=f97316&color=141414`} />
<AvatarFallback className="bg-orange-500 text-zinc-950 font-bold text-xs">
{(user?.name || 'U').charAt(0)}
</AvatarFallback>
</Avatar>
@ -298,3 +244,6 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
</aside>
);
};

View File

@ -1,436 +0,0 @@
import React, { useState, useEffect } from 'react';
import { X, User, Truck, MapPin, Package, Hash, Clock, ShieldCheck, Trash2, Send, CheckCircle2, FileText, Loader2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { updateTripStatus, deleteTripRequest, getNfe, closeTripRequest } from '../services/prafrotService';
import { toast } from 'sonner';
import { TripRequestForm } from './TripRequestForm';
import { AttendanceFormModal } from './AttendanceFormModal';
import { NfeDataDisplay } from './NfeDataDisplay';
const formatCPF = (cpf) => {
if (!cpf) return '---';
const cleanCPF = String(cpf).replace(/\D/g, '');
if (cleanCPF.length !== 11) return cpf;
return cleanCPF.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
};
const renderValue = (val) => {
if (!val) return '---';
if (typeof val === 'object' && val !== null) {
return val.numero || val.chave || val.id || JSON.stringify(val);
}
return val;
};
export const TripDetailsModal = ({ trip, onClose, onRefresh }) => {
const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [activeSubTab, setActiveSubTab] = useState('geral');
const [nfeData, setNfeData] = useState(null);
const [loadingNfe, setLoadingNfe] = useState(false);
const [showAttendanceForm, setShowAttendanceForm] = useState(false);
const fetchNfeData = async () => {
if (nfeData) return;
// 1. Tenta usar dados já presentes no objeto trip
if (trip.nfe && typeof trip.nfe === 'object' && !Array.isArray(trip.nfe)) {
setNfeData(trip.nfe);
return;
}
if (trip.nota_fiscal && typeof trip.nota_fiscal === 'object' && !Array.isArray(trip.nota_fiscal)) {
setNfeData(trip.nota_fiscal);
return;
}
// 2. Busca pela chave/CTE se for string
const keyToFetch = trip.cte || (typeof trip.nfe === 'string' ? trip.nfe : null) || (typeof trip.nota_fiscal === 'string' ? trip.nota_fiscal : null);
if (!keyToFetch) return;
setLoadingNfe(true);
try {
const data = await getNfe(keyToFetch);
setNfeData(data);
} catch (error) {
console.error('Erro ao buscar NFe:', error);
toast.error('Não foi possível carregar os dados detalhados.');
} finally {
setLoadingNfe(false);
}
};
useEffect(() => {
if (activeSubTab === 'nfe') {
fetchNfeData();
}
}, [activeSubTab]);
if (!trip) return null;
const handleStatusUpdate = async (newStatus) => {
setLoading(true);
try {
if (newStatus === 'FECHADO' || newStatus === 'FINALIZADO') {
await closeTripRequest(trip.idsolicitacoes);
toast.success('Solicitação finalizada');
} else {
await updateTripStatus(trip.idsolicitacoes, newStatus);
toast.success(`Status atualizado para ${newStatus}`);
}
onRefresh();
onClose();
} catch (error) {
toast.error('Erro ao atualizar status');
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (!window.confirm('Deseja realmente excluir esta solicitação?')) return;
setLoading(true);
try {
await deleteTripRequest(trip.idsolicitacoes);
toast.success('Solicitação excluída');
onRefresh();
onClose();
} catch (error) {
toast.error('Erro ao excluir solicitação');
} finally {
setLoading(false);
}
};
const sectionStyle = "space-y-4 pb-6 border-b border-white/5 last:border-0 pt-6 first:pt-0";
const itemLabelStyle = "text-[11px] font-medium text-zinc-500 uppercase tracking-widest block mb-1";
const itemValueStyle = "text-sm font-medium text-white";
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-6 overflow-hidden">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-4xl bg-[#141416] border border-white/10 rounded-[40px] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
style={{ fontFamily: 'var(--font-main)' }}
>
{/* Header */}
<div className="p-8 pb-4 flex items-center justify-between border-b border-white/5 flex-shrink-0">
<div className="space-y-1">
<h2 className="text-2xl font-bold text-white tracking-tight">
{isEditing ? 'Editar Solicitação' : 'Detalhes da Solicitação'}
</h2>
<div className="flex items-center gap-2">
<span className="text-zinc-500 text-xs font-bold uppercase tracking-tighter"># {trip.idsolicitacoes}</span>
<span className={`px-2 py-0.5 rounded-full text-[8px] font-bold uppercase tracking-widest ${
(trip.status || 'PENDENTE') === 'PENDENTE' ? 'bg-amber-500/20 text-amber-400' :
trip.status === 'LIBERADO' ? 'bg-emerald-500/20 text-emerald-400' :
'bg-zinc-800 text-zinc-400'
}`}>
{trip.status || 'PENDENTE'}
</span>
</div>
</div>
<button
onClick={onClose}
className="w-10 h-10 bg-white/5 hover:bg-white/10 rounded-full flex items-center justify-center text-zinc-400 hover:text-white transition-all border border-white/5"
>
<X size={20} />
</button>
</div>
{/* Tab Switcher - Only in View Mode */}
{!isEditing && (
<div className="flex px-8 py-2 border-b border-white/5 bg-[#1a1a1c]/50 overflow-x-auto [&::-webkit-scrollbar]:hidden relative z-20 flex-shrink-0" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
{[
{ id: 'geral', label: 'Geral', icon: <Hash size={14} /> },
{ id: 'nfe', label: 'Dados da NFe', icon: <FileText size={14} /> }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveSubTab(tab.id)}
className={`flex items-center gap-2 px-6 py-4 text-[10px] font-bold uppercase tracking-widest transition-all relative ${
activeSubTab === tab.id ? 'text-emerald-500' : 'text-zinc-500 hover:text-zinc-300'
}`}
>
{tab.icon}
{tab.label}
{activeSubTab === tab.id && (
<motion.div
layoutId="activeSubTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-emerald-500"
/>
)}
</button>
))}
</div>
)}
{/* Content */}
{isEditing ? (
<div className="flex-1 overflow-y-auto custom-scrollbar p-6">
<TripRequestForm
mode="edit"
initialData={trip}
onSuccess={() => {
onRefresh();
onClose();
}}
/>
</div>
) : (
<>
{activeSubTab === 'geral' ? (
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-6">
{/* Highlight Header */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-[28px] p-6 flex flex-col justify-center">
<div className="flex items-start justify-between mb-2">
<span className="text-[10px] font-medium text-emerald-600 uppercase tracking-widest">CTE / Documento</span>
<span className="px-2 py-1 bg-emerald-500/20 rounded-lg text-[8px] font-medium uppercase text-emerald-400 tracking-widest">{trip.operacao}</span>
</div>
<div className="text-3xl md:text-4xl font-medium text-white tracking-tight font-mono break-all leading-tight">
{renderValue(trip.cte || trip.nota_fiscal)}
</div>
</div>
<div className="bg-[#1c1c1c] border border-white/5 rounded-[28px] p-6 flex flex-col justify-center">
<div className="flex items-center justify-between mb-3">
<span className="text-[10px] font-medium text-zinc-500 uppercase tracking-widest">Data de Criação</span>
<div className={`px-2 py-1 rounded-full text-[8px] font-medium uppercase tracking-widest ${
(trip.status || 'PENDENTE') === 'PENDENTE' ? 'bg-amber-500/20 text-amber-400' : 'bg-emerald-500/20 text-emerald-400'
}`}>
{trip.status || 'PENDENTE'}
</div>
</div>
<div className="text-xl font-medium text-zinc-300">
{new Date(trip.created_at || new Date()).toLocaleDateString('pt-BR')}
</div>
</div>
</div>
{/* Motorista Highlight & CPF Card */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2 p-6 bg-[#1c1c1c] border border-white/5 rounded-[32px] flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-zinc-800 flex items-center justify-center text-zinc-500">
<User size={24} />
</div>
<div>
<span className="text-[11px] font-medium text-zinc-500 uppercase tracking-widest block mb-1">Identificação do Motorista</span>
<div className="text-xl md:text-2xl font-medium text-white uppercase leading-none">{trip.nome_completo}</div>
</div>
</div>
<div className="p-6 bg-[#1c1c1c] border border-emerald-500/20 rounded-[32px] flex flex-col justify-center">
<span className="text-[11px] font-medium text-emerald-600 uppercase tracking-widest block mb-1">CPF</span>
<div className="text-xl font-medium text-white font-mono">{formatCPF(trip.cpf)}</div>
</div>
</div>
{/* Grid Detalhes */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Veículo */}
<div className="p-6 bg-zinc-900/40 border border-white/5 rounded-[32px] space-y-4">
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
<Truck size={16} />
</div>
<span className="text-xs font-medium text-blue-500 uppercase tracking-widest">Veículo</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block mb-1">Cavalo</label>
<div className="text-lg font-medium text-white uppercase">{trip.placa_do_cavalo}</div>
</div>
<div>
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block mb-1">Carreta</label>
<div className="text-lg font-medium text-zinc-400 uppercase">{trip.placa_da_carreta || '---'}</div>
</div>
</div>
<div>
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block mb-1">Modelo</label>
<div className="text-sm font-medium text-zinc-300 uppercase">{trip.tipo_de_veiculo}</div>
</div>
</div>
{/* Rota */}
<div className="p-6 bg-zinc-900/40 border border-white/5 rounded-[32px] space-y-4">
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 rounded-full bg-amber-500/10 flex items-center justify-center text-amber-500">
<MapPin size={16} />
</div>
<span className="text-xs font-medium text-amber-500 uppercase tracking-widest">Rota</span>
</div>
<div className="space-y-4">
<div className="relative pl-4 border-l-2 border-zinc-800">
<div className="absolute -left-[5px] top-1.5 w-2 h-2 rounded-full bg-zinc-600" />
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block">Origem</label>
<div className="text-sm font-medium text-white uppercase">{trip.origem}</div>
</div>
<div className="relative pl-4 border-l-2 border-zinc-800">
<div className="absolute -left-[5px] top-1.5 w-2 h-2 rounded-full bg-emerald-500" />
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block">Destino</label>
<div className="text-sm font-medium text-white uppercase">{trip.destino}</div>
</div>
</div>
</div>
</div>
{/* Dados de Liberação - Only if Released or Finished */}
{(trip.status === 'LIBERADO' || trip.status === 'FINALIZADO') && (
<div className="p-6 bg-emerald-500/5 border border-emerald-500/20 rounded-[32px] space-y-4">
<div className="flex items-center gap-3 mb-2">
<div className="w-8 h-8 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<ShieldCheck size={16} />
</div>
<span className="text-xs font-medium text-emerald-500 uppercase tracking-widest">Dados de Liberação</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block mb-1">Liberado por</label>
<div className="text-sm font-medium text-white truncate">{trip.liberado_por || '---'}</div>
</div>
<div>
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block mb-1">Data/Hora</label>
<div className="text-sm font-medium text-zinc-300">
{trip.data_hora_liberacao ? new Date(trip.data_hora_liberacao).toLocaleString('pt-BR') : '---'}
</div>
</div>
</div>
{trip.obs_liberacao && (
<div>
<label className="text-[9px] font-normal text-zinc-600 uppercase tracking-widest block mb-1">Observações da Liberação</label>
<div className="text-sm font-medium text-emerald-400 italic">
"{trip.obs_liberacao}"
</div>
</div>
)}
</div>
)}
{/* Stats Footer */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-zinc-900/40 border border-white/5 rounded-2xl flex flex-col items-center justify-center">
<span className="text-2xl font-medium text-white">{trip.quantidade_palet || 0}</span>
<span className="text-[8px] font-normal text-zinc-600 uppercase tracking-widest">Palets</span>
</div>
<div className="p-4 bg-zinc-900/40 border border-white/5 rounded-2xl flex flex-col items-center justify-center">
<span className="text-2xl font-medium text-white">{trip.quantidade_chapatex || 0}</span>
<span className="text-[8px] font-normal text-zinc-600 uppercase tracking-widest">Chapatex</span>
</div>
</div>
</div>
) : (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden bg-[#1a1a1c] relative h-full">
{loadingNfe ? (
<div className="flex flex-col items-center justify-center py-20 gap-4 flex-1">
<Loader2 className="animate-spin text-emerald-500" size={32} />
<span className="text-xs font-medium text-zinc-500 uppercase tracking-widest">Buscando dados da NFe...</span>
</div>
) : nfeData ? (
<NfeDataDisplay data={nfeData} />
) : (
<div className="flex flex-col items-center justify-center py-20 gap-4 text-center px-10">
<div className="w-16 h-16 bg-zinc-900 border border-white/10 rounded-full flex items-center justify-center text-zinc-600 mb-4">
<FileText size={32} />
</div>
<h3 className="text-white font-bold uppercase tracking-widest text-xs">Dados não encontrados</h3>
<p className="text-zinc-500 text-[10px] max-w-xs uppercase leading-relaxed font-medium">
Não dados de NFe detalhados para o documento informado ou o serviço não retornou informações.
</p>
</div>
)}
</div>
)}
</>
)
}
{/* Footer Actions */}
<div className="p-8 pt-4 bg-[#1a1a1c] border-t border-white/5 flex items-center justify-between flex-shrink-0">
{/* Botão de excluir comentado conforme solicitado */}
{/*
<button
onClick={handleDelete}
disabled={loading || isEditing}
className="flex items-center gap-2 px-6 py-3 bg-red-500/10 hover:bg-red-500/20 text-red-500 rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all disabled:opacity-30"
>
<Trash2 size={16} /> Excluir
</button>
*/}
<div />
<div className="flex gap-3">
{!isEditing ? (
<>
<button
onClick={() => setIsEditing(true)}
className="px-8 py-3 bg-zinc-800 hover:bg-zinc-700 text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all"
>
Editar
</button>
{(trip.status || 'PENDENTE') === 'PENDENTE' && (
<button
onClick={() => setShowAttendanceForm(true)}
disabled={loading}
className="flex items-center gap-2 px-8 py-3 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all shadow-lg shadow-emerald-600/20"
>
Atender Cliente
</button>
)}
{trip.status === 'LIBERADO' && (
<button
onClick={() => handleStatusUpdate('FINALIZADO')}
disabled={loading}
className="flex items-center gap-2 px-8 py-3 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all shadow-lg shadow-emerald-600/20"
>
<CheckCircle2 size={16} /> Finalizar Solicitação
</button>
)}
</>
) : (
<button
onClick={() => setIsEditing(false)}
className="px-8 py-3 bg-zinc-800 hover:bg-zinc-700 text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all"
>
Cancelar
</button>
)}
<button
onClick={onClose}
className="px-8 py-3 bg-zinc-800 hover:bg-zinc-700 text-white rounded-2xl text-[10px] font-bold uppercase tracking-widest transition-all"
>
Fechar
</button>
</div>
</div>
</motion.div>
<AnimatePresence>
{showAttendanceForm && (
<AttendanceFormModal
trip={trip}
onClose={() => setShowAttendanceForm(false)}
onRefresh={() => {
onRefresh();
onClose();
}}
/>
)}
</AnimatePresence>
</div>
);
};

View File

@ -1,363 +0,0 @@
import React, { useState } from 'react';
import { Send, FileText, User, Truck, MapPin, Package, Hash, Loader2 } from 'lucide-react';
import { submitTripRequest, updateTripRequest, getNfe } from '../services/prafrotService';
import { toast } from 'sonner';
/**
* TripRequestForm Component
* Used in the public DriverPortalView and internal modals.
*/
export const TripRequestForm = ({ operation, onSuccess, mode = 'create', initialData = null }) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
nome_completo: initialData?.nome_completo || '',
cpf: initialData?.cpf || '',
tipo_de_veiculo: initialData?.tipo_de_veiculo || 'Carreta',
placa_do_cavalo: initialData?.placa_do_cavalo || '',
placa_da_carreta: initialData?.placa_da_carreta || '',
cte: initialData?.cte || '',
no_cte: initialData?.cte === 'NÃO POSSUI',
quantidade_palet: initialData?.quantidade_palet || '',
quantidade_chapatex: initialData?.quantidade_chapatex || '',
nota_fiscal: initialData?.nota_fiscal || '',
no_nf: initialData?.nota_fiscal === 'NÃO POSSUI',
origem: initialData?.origem || '',
destino: initialData?.destino || '',
operacao: initialData?.operacao || operation?.id || ''
});
const handleCTeBlur = async () => {
if (!formData.cte || formData.no_cte || formData.cte.length < 20) return;
setLoading(true);
try {
const nfeData = await getNfe(formData.cte);
if (nfeData) {
setFormData(prev => ({
...prev,
nome_completo: nfeData.motorista || nfeData.nome_motorista || nfeData.nome_completo || prev.nome_completo,
cpf: nfeData.cpf_motorista || nfeData.cpf || nfeData.cpf_condutor || prev.cpf,
origem: nfeData.origem || nfeData.cidade_origem || nfeData.municipio_origem || nfeData.origem_cidade || prev.origem,
destino: nfeData.destino || nfeData.cidade_destino || nfeData.municipio_destino || nfeData.destino_cidade || prev.destino,
quantidade_palet: nfeData.quantidade_palet || nfeData.palets || nfeData.qtd_palet || prev.quantidade_palet,
quantidade_chapatex: nfeData.quantidade_chapatex || nfeData.chapatex || nfeData.qtd_chapatex || prev.quantidade_chapatex,
// Prioridade para a CHAVE (44 dígitos) para permitir visualização da DANFE
nota_fiscal: nfeData.chave ||
(typeof nfeData.nfe === 'string' && nfeData.nfe.length > 20 ? nfeData.nfe : null) ||
(typeof nfeData.nota_fiscal === 'string' && nfeData.nota_fiscal.length > 20 ? nfeData.nota_fiscal : null) ||
(typeof nfeData.nfe === 'object' ? nfeData.nfe?.numero : nfeData.nfe) ||
(typeof nfeData.nota_fiscal === 'object' ? nfeData.nota_fiscal?.numero : nfeData.nota_fiscal) ||
nfeData.numero_nf || prev.nota_fiscal,
placa_do_cavalo: nfeData.placa_cavalo || nfeData.placa || prev.placa_do_cavalo,
placa_da_carreta: nfeData.placa_carreta || nfeData.placa_reboque || prev.placa_da_carreta
}));
toast.info('Dados preenchidos automaticamente via CTe!');
} else {
toast.warning('CTe encontrado, mas sem dados de NFe vinculados.');
}
} catch (error) {
console.error('Erro ao buscar NFe:', error);
// Silently fail or minimal toast if it's just auto-fill
} finally {
setLoading(false);
}
};
const handleCPFChange = (e) => {
let value = e.target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
let formattedValue = value;
if (value.length > 9) {
formattedValue = value.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
} else if (value.length > 6) {
formattedValue = value.replace(/(\d{3})(\d{3})(\d{0,3})/, '$1.$2.$3');
} else if (value.length > 3) {
formattedValue = value.replace(/(\d{3})(\d{0,3})/, '$1.$2');
}
setFormData({ ...formData, cpf: formattedValue });
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
// Prepare payload - alignment with backend expected fields
const payload = {
...formData,
operacao: initialData?.operacao || operation?.label || operation?.id || formData.operacao,
cte: formData.no_cte ? 'NÃO POSSUI' : formData.cte,
nota_fiscal: formData.no_nf ? 'NÃO POSSUI' : formData.nota_fiscal
};
// Validation
if (!payload.nome_completo || !payload.cpf || !payload.placa_do_cavalo) {
toast.error('Por favor, preencha os campos obrigatórios (Nome, CPF e Placa).');
setLoading(false);
return;
}
if (mode === 'edit' && initialData?.idsolicitacoes) {
await updateTripRequest(initialData.idsolicitacoes, payload);
toast.success('Solicitação atualizada com sucesso!');
} else {
await submitTripRequest(payload);
toast.success('Solicitação enviada com sucesso!');
}
if (onSuccess) onSuccess();
} catch (error) {
console.error('Erro ao processar solicitação:', error);
toast.error(error.message || 'Erro ao processar solicitação. Tente novamente.');
} finally {
setLoading(false);
}
};
const inputStyle = "w-full pl-12 pr-4 py-4 bg-zinc-900/50 border border-zinc-800 rounded-2xl focus:outline-none focus:border-emerald-500/50 focus:ring-4 focus:ring-emerald-500/5 transition-all text-sm font-medium placeholder:text-zinc-600";
const labelStyle = "text-[10px] font-bold text-zinc-500 uppercase tracking-widest ml-1 mb-2 block";
return (
<form onSubmit={handleSubmit} className="space-y-6 pb-10" style={{ fontFamily: 'var(--font-main)' }}>
{/* Seção 1: Motorista */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className={labelStyle}>Nome Completo</label>
<div className="relative group">
<User size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={inputStyle}
placeholder="Nome do motorista"
value={formData.nome_completo}
onChange={e => setFormData({...formData, nome_completo: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>CPF</label>
<div className="relative group">
<Hash size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={inputStyle}
placeholder="000.000.000-00"
value={formData.cpf}
onChange={handleCPFChange}
required
/>
</div>
</div>
</div>
{/* Seção 2: Veículo */}
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
<div className="space-y-4">
<label className={labelStyle}>Tipo de Veículo</label>
<div className="flex gap-3">
{['Carreta', 'Truck'].map(tipo => (
<button
key={tipo}
type="button"
onClick={() => setFormData({...formData, tipo_de_veiculo: tipo})}
className={`flex-1 py-4 rounded-[20px] border font-black text-[10px] uppercase tracking-[0.2em] transition-all ${
formData.tipo_de_veiculo === tipo
? 'bg-emerald-500 text-[#0a0a0b] border-emerald-500 shadow-lg shadow-emerald-500/20'
: 'bg-zinc-900/30 border-zinc-800 text-zinc-600 hover:border-zinc-700'
}`}
>
{tipo}
</button>
))}
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className={labelStyle}>Placa do Cavalo</label>
<div className="relative group">
<Truck size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={inputStyle}
placeholder="ABC-1234"
value={formData.placa_do_cavalo}
onChange={e => setFormData({...formData, placa_do_cavalo: e.target.value.toUpperCase()})}
required
/>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Placa da Carreta</label>
<div className="relative group">
<Truck size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={inputStyle}
placeholder="XYZ-5678"
value={formData.placa_da_carreta}
onChange={e => setFormData({...formData, placa_da_carreta: e.target.value.toUpperCase()})}
/>
</div>
</div>
</div>
{/* Seção 3: Documentação */}
<div className="space-y-4">
<div className="space-y-2">
<label className={labelStyle}>CTe</label>
<div className="relative group">
<FileText size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={`${inputStyle} ${formData.no_cte ? 'opacity-30 pointer-events-none' : ''}`}
placeholder="Número do CTe"
value={formData.cte}
onChange={e => setFormData({...formData, cte: e.target.value})}
onBlur={handleCTeBlur}
disabled={formData.no_cte}
/>
</div>
<label className="flex items-center gap-3 mt-1 cursor-pointer group w-fit">
<div className="relative flex items-center">
<input
type="checkbox"
className="peer appearance-none w-5 h-5 border-2 border-zinc-800 rounded-lg checked:bg-emerald-500 checked:border-emerald-500 transition-all cursor-pointer"
checked={formData.no_cte}
onChange={e => setFormData({...formData, no_cte: e.target.checked})}
/>
<CheckIcon className="absolute w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100 left-[3px] pointer-events-none transition-opacity" />
</div>
<span className="text-[10px] font-black text-zinc-600 group-hover:text-zinc-400 uppercase tracking-widest transition-colors">Não possuo CTe</span>
</label>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className={labelStyle}>Qtd. Palet</label>
<div className="relative group">
<Package size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="number"
className={`${inputStyle} px-3 pl-12`}
placeholder="0"
value={formData.quantidade_palet}
onChange={e => setFormData({...formData, quantidade_palet: e.target.value})}
/>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Qtd. Chapatex</label>
<div className="relative group">
<Package size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="number"
className={`${inputStyle} px-3 pl-12`}
placeholder="0"
value={formData.quantidade_chapatex}
onChange={e => setFormData({...formData, quantidade_chapatex: e.target.value})}
/>
</div>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Nota Fiscal (Sequência)</label>
<div className="relative group">
<FileText size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={`${inputStyle} ${formData.no_nf ? 'opacity-30 pointer-events-none' : ''}`}
placeholder="Ex: 1 a 10 ou 1 a 10 / 22 a 25"
value={formData.nota_fiscal}
onChange={e => setFormData({...formData, nota_fiscal: e.target.value})}
disabled={formData.no_nf}
/>
</div>
<label className="flex items-center gap-3 mt-1 cursor-pointer group w-fit">
<div className="relative flex items-center">
<input
type="checkbox"
className="peer appearance-none w-5 h-5 border-2 border-zinc-800 rounded-lg checked:bg-emerald-500 checked:border-emerald-500 transition-all cursor-pointer"
checked={formData.no_nf}
onChange={e => setFormData({...formData, no_nf: e.target.checked})}
/>
<CheckIcon className="absolute w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100 left-[3px] pointer-events-none transition-opacity" />
</div>
<span className="text-[10px] font-black text-zinc-600 group-hover:text-zinc-400 uppercase tracking-widest transition-colors">Não possuo NF</span>
</label>
</div>
</div>
{/* Seção 4: Rota */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className={labelStyle}>Origem</label>
<div className="relative group">
<MapPin size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={inputStyle}
placeholder="Cidade de Origem"
value={formData.origem}
onChange={e => setFormData({...formData, origem: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<label className={labelStyle}>Destino</label>
<div className="relative group">
<MapPin size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
className={inputStyle}
placeholder="Cidade de Destino"
value={formData.destino}
onChange={e => setFormData({...formData, destino: e.target.value})}
required
/>
</div>
</div>
</div>
<div className="pt-4">
<button
type="submit"
disabled={loading}
className="w-full py-5 bg-emerald-500 hover:bg-emerald-400 disabled:opacity-50 text-[#0a0a0b] font-black rounded-[32px] shadow-2xl shadow-emerald-500/20 active:scale-95 transition-all text-[11px] uppercase tracking-[0.3em] flex items-center justify-center gap-3"
>
{loading ? (
<Loader2 className="animate-spin" size={20} />
) : (
<>
<Send size={18} /> Solicitar Liberação
</>
)}
</button>
</div>
</form>
);
};
const CheckIcon = ({ className }) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<polyline points="20 6 9 17 4 12" />
</svg>
);

View File

@ -71,3 +71,6 @@ export const useAvailability = create((set, get) => ({
}
}
}));

View File

@ -73,3 +73,6 @@ export const useClaims = create((set, get) => ({
}
}
}));

View File

@ -26,3 +26,6 @@ export const useDispatcher = create((set) => ({
}
}
}));

View File

@ -22,3 +22,6 @@ export const useDrivers = create((set, get) => ({
}
}
}));

View File

@ -105,3 +105,6 @@ export const useFleetLists = create((set, get) => ({
}
}
}));

View File

@ -7,7 +7,6 @@ const notify = (type, title, message) => useFeedbackStore.getState().notify(type
export const useMaintenance = create((set, get) => ({
maintenances: [],
abertoFechadoData: null,
loading: false,
error: null,
@ -18,26 +17,14 @@ export const useMaintenance = create((set, get) => ({
if (type === 'total') {
const data = await prafrotService.getMaintenance();
list = Array.isArray(data) ? data : (data.data || []);
set({ abertoFechadoData: null });
} else if (type === 'aberta' || type === 'fechada' || type === 'aberto_fechado') {
} else {
const data = await prafrotService.getAbertoFechado();
// Concatena abertas e fechadas para manter o estado global se necessário,
// mas a tela de manutenção gerencia o estado `abertoFechadoData` via hook.
// No entanto, para padronizar as ações (save/update), vamos manter o fetchMaintenances funcional.
const abertas = data.abertas || data.aberto || data.abertos || [];
const fechadas = data.fechadas || data.fechado || data.fechados || [];
// Deduplicação por ID para evitar itens duplicados na listagem
const merged = [...abertas, ...fechadas];
const uniqueMap = new Map();
merged.forEach(item => {
const id = item.idmanutencao_frota || item.id;
if (id) uniqueMap.set(id, item);
});
list = Array.from(uniqueMap.values());
set({ abertoFechadoData: data });
} else {
// Assume it's a specific status filter
const data = await prafrotService.getMaintenance(type);
list = Array.isArray(data) ? data : (data.data || []);
list = [...abertas, ...fechadas];
}
const normalized = list.map(m => ({ ...m, previsao_entrega: m.previsao_entrega ?? m.previcao_entrega }));
@ -70,7 +57,7 @@ export const useMaintenance = create((set, get) => ({
try {
await prafrotService.createMaintenance(payload, files);
notify('success', 'Cadastro Concluído', 'Manutenção registrada com sucesso no sistema.');
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -86,7 +73,7 @@ export const useMaintenance = create((set, get) => ({
try {
await prafrotService.updateMaintenance(id, payload, files);
notify('success', 'Atualização!', 'Dados da manutenção atualizados.');
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -103,7 +90,7 @@ export const useMaintenance = create((set, get) => ({
await prafrotService.updateMaintenanceBatch(ids, status);
notify('success', 'Lote Atualizado', `${ids.length} manutenções foram atualizadas.`);
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -130,7 +117,7 @@ export const useMaintenance = create((set, get) => ({
try {
await prafrotService.deleteMaintenance(id);
notify('success', 'Removido', 'Registro de manutenção excluído.');
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -146,7 +133,7 @@ export const useMaintenance = create((set, get) => ({
try {
await prafrotService.fecharManutencao(id);
notify('success', 'Manutenção Fechada', 'Manutenção fechada com sucesso.');
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -162,7 +149,7 @@ export const useMaintenance = create((set, get) => ({
try {
await prafrotService.abrirManutencao(id);
notify('success', 'Manutenção Aberta', 'Manutenção aberta com sucesso.');
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -180,20 +167,8 @@ export const useMaintenance = create((set, get) => ({
// Atualiza também a lista principal 'maintenances' para refletir as mudanças
const abertas = data.abertas || data.aberto || data.abertos || [];
const fechadas = data.fechadas || data.fechado || data.fechados || [];
// Deduplicação por ID
const merged = [...abertas, ...fechadas];
const uniqueMap = new Map();
merged.forEach(item => {
const id = item.idmanutencao_frota || item.id;
if (id) uniqueMap.set(id, item);
});
const normalized = Array.from(uniqueMap.values()).map(m => ({
...m,
previsao_entrega: m.previsao_entrega ?? m.previcao_entrega
}));
set({ maintenances: normalized, abertoFechadoData: data });
const normalized = [...abertas, ...fechadas].map(m => ({ ...m, previsao_entrega: m.previsao_entrega ?? m.previcao_entrega }));
set({ maintenances: normalized });
return data;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
@ -282,3 +257,6 @@ export const useMaintenance = create((set, get) => ({
}
}
}));

View File

@ -71,3 +71,6 @@ export const useMoki = create((set, get) => ({
}
}
}));

View File

@ -71,3 +71,6 @@ export const useMonitoring = create((set, get) => ({
}
}
}));

View File

@ -90,3 +90,6 @@ export const usePrafrotStatistics = () => {
refresh: fetchStatistics
};
};

View File

@ -101,3 +101,6 @@ export const useStatus = create((set, get) => ({
}
}
}));

View File

@ -87,3 +87,6 @@ export const useVehicles = create((set, get) => ({
}
}
}));

View File

@ -71,3 +71,6 @@ export const useWorkshops = create((set, get) => ({
}
}
}));

View File

@ -7,11 +7,11 @@ import {
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
import { PrafrotSidebar } from '../components/PrafrotSidebar';
import { OestPanSidebar } from '../components/PrafrotSidebar';
import { FeedbackContainer } from '../components/FeedbackNotification';
export const PrafrotLayout = () => {
useDocumentMetadata('Prafrota', 'prafrot');
export const OestPanLayout = () => {
useDocumentMetadata('Oeste Pan', 'oest-pan');
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(true);
@ -25,12 +25,12 @@ export const PrafrotLayout = () => {
return (
<div className={cn(
"flex min-h-screen selection:bg-emerald-500/30 theme-frota",
"flex min-h-screen font-inter selection:bg-orange-500/30 theme-frota",
isDarkMode ? "dark bg-[#141414] text-slate-100" : "bg-slate-50 text-slate-900"
)} style={{ fontFamily: 'var(--font-main)' }}>
)}>
<FeedbackContainer />
{/* New Sidebar Component */}
<PrafrotSidebar
<OestPanSidebar
isCollapsed={isSidebarCollapsed}
onToggle={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
/>
@ -50,7 +50,7 @@ export const PrafrotLayout = () => {
{/* <div className="flex items-center gap-4">
<button
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
className="text-slate-400 hover:text-emerald-500 transition-colors p-2 rounded-xl hover:bg-emerald-500/10"
className="text-slate-400 hover:text-orange-500 transition-colors p-2 rounded-xl hover:bg-orange-500/10"
>
<Menu size={20} />
</button>
@ -87,3 +87,6 @@ export const PrafrotLayout = () => {
</div>
);
};

View File

@ -1,6 +1,6 @@
import { lazy, Suspense } from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
import { PrafrotLayout } from './layout/PrafrotLayout';
import { OestPanLayout } from './layout/PrafrotLayout';
import { Zap } from 'lucide-react';
// Lazy loading views
@ -17,59 +17,30 @@ const LoginView = lazy(() => import('./views/LoginView'));
const StatisticsView = lazy(() => import('./views/StatisticsView'));
const DispatcherView = lazy(() => import('./views/DispatcherView'));
const ConfigView = lazy(() => import('./views/ConfigView'));
const SolicitacoesView = lazy(() => import('./views/SolicitacoesView'));
const FinancePendenciesView = lazy(() => import('./views/FinancePendenciesView'));
const MessagesView = lazy(() => import('./views/MessagesView'));
// Loading component matching Prafrot theme
const PrafrotLoader = () => (
const OestPanLoader = () => (
<div className="flex h-screen w-screen items-center justify-center bg-[#141414]">
<div className="flex flex-col items-center gap-6">
<div className="relative">
<div className="w-16 h-16 bg-emerald-500 rounded-2xl flex items-center justify-center shadow-lg shadow-emerald-500/20 animate-bounce">
<div className="w-16 h-16 bg-orange-500 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/20 animate-bounce">
<Zap size={32} className="text-[#1c1c1c]" strokeWidth={2.5} />
</div>
<div className="absolute -inset-4 border-2 border-emerald-500/20 border-t-emerald-500 rounded-full animate-spin" />
<div className="absolute -inset-4 border-2 border-orange-500/20 border-t-orange-500 rounded-full animate-spin" />
</div>
<div className="flex flex-col items-center gap-1">
<span className="text-emerald-500 text-[10px] font-bold uppercase tracking-[0.3em] animate-pulse">Prafrot System</span>
<span className="text-orange-500 text-[10px] font-bold uppercase tracking-[0.3em] animate-pulse">Oeste Pan System</span>
<span className="text-slate-600 text-[8px] font-bold uppercase tracking-widest">Carregando...</span>
</div>
</div>
</div>
);
import { useAuthContext } from '@/components/shared/AuthProvider';
export const PrafrotRoutes = () => {
const { user } = useAuthContext();
// Lógica de detecção de setor para redirecionamento inteligente
const isFinanceiro = (() => {
try {
const userSetores = user?.setores || [];
let allSetores = [...userSetores];
const prafrotUserJson = localStorage.getItem('integra_user_prafrot');
if (prafrotUserJson) {
const prafrotUser = JSON.parse(prafrotUserJson);
if (prafrotUser.setores) {
allSetores = [...new Set([...allSetores, ...prafrotUser.setores])];
}
}
return allSetores.includes('Financeiro');
} catch (e) {
return false;
}
})();
const defaultPath = isFinanceiro
? "/plataforma/prafrot/pendencias-financeiro"
: "/plataforma/prafrot/estatisticas";
export const OestPanRoutes = () => {
return (
<Suspense fallback={<PrafrotLoader />}>
<Suspense fallback={<OestPanLoader />}>
<Routes>
<Route element={<PrafrotLayout />}>
<Route element={<OestPanLayout />}>
<Route path="veiculos" element={<VehiclesView />} />
<Route path="manutencao" element={<MaintenanceView />} />
<Route path="disponibilidade" element={<AvailabilityView />} />
@ -82,12 +53,9 @@ export const PrafrotRoutes = () => {
<Route path="estatisticas" element={<StatisticsView />} />
<Route path="dispatcher" element={<DispatcherView />} />
<Route path="configuracoes" element={<ConfigView />} />
<Route path="solicitacoes" element={<SolicitacoesView />} />
<Route path="pendencias-financeiro" element={<FinancePendenciesView />} />
<Route path="mensagens" element={<MessagesView />} />
<Route index element={<Navigate to={defaultPath} replace />} />
<Route path="*" element={<Navigate to={defaultPath} replace />} />
<Route index element={<Navigate to="/plataforma/oest-pan/estatisticas" replace />} />
<Route path="*" element={<Navigate to="/plataforma/oest-pan/estatisticas" replace />} />
</Route>
</Routes>
</Suspense>
@ -96,3 +64,6 @@ export const PrafrotRoutes = () => {
// Export LoginView separately for use in App.jsx
export { LoginView };

View File

@ -12,3 +12,6 @@ export const dispatcherService = {
}
})
};

View File

@ -50,12 +50,9 @@ export const prafrotService = {
}),
// --- Manutenção ---
getMaintenance: (status) => handleRequest({
getMaintenance: () => handleRequest({
apiFn: async () => {
const url = status
? `${ENDPOINTS.MAINTENANCE}/apresentar?status=${encodeURIComponent(status)}`
: `${ENDPOINTS.MAINTENANCE}/apresentar`;
const { data } = await api.get(url);
const { data } = await api.get(`${ENDPOINTS.MAINTENANCE}/apresentar`);
return data;
}
}),
@ -409,128 +406,5 @@ export const prafrotService = {
})
};
// --- Trip Request (Sabrina) Services ---
export const submitTripRequest = (data) => handleRequest({
apiFn: () => api.post('/solicitacoes', data)
});
export const updateTripRequest = (id, data) => handleRequest({
apiFn: () => api.put(`/solicitacao/${id}`, data)
});
export const deleteTripRequest = (id) => handleRequest({
apiFn: () => api.put(`/cadastro_frota/delete/${id}`)
});
export const updateTripStatus = (id, status, extraData = {}) => handleRequest({
apiFn: () => api.put(`/solicitacao/edicao_status/${id}`, { status, ...extraData })
});
export const getTripRequests = () => handleRequest({
apiFn: () => api.get('/solicitacao/apresentar')
});
export const closeTripRequest = (id) => handleRequest({
apiFn: () => api.get(`/solicitacao/fechado/${id}`)
});
export const approveTripRequest = (data) => handleRequest({
apiFn: () => api.post('/solicitacoes/aprovar', data)
});
export const createOperation = (operacao) => handleRequest({
apiFn: () => api.post('/operacoes', { operacao })
});
export const getOperations = () => handleRequest({
apiFn: () => api.get('/operacoes/apresentar')
});
export const deleteOperation = (id) => handleRequest({
apiFn: () => api.put(`/cadastro_frota/delete/${id}`)
});
export const archiveTripRequest = (id) => handleRequest({
apiFn: () => api.put(`/operacoes/arquivado/${id}`)
});
export const getNfe = (chave) => handleRequest({
apiFn: () => api.get(`/nfe/${chave}`)
});
export const getNfes = () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/nfe');
return data;
}
});
// --- Email XML Processing Services ---
/**
* Salva as credenciais de e-mail para processamento de XML.
* @param {string} email
* @param {string} senha
* @returns {Promise<Object>}
*/
export const saveEmailCredentials = (email, senha) => handleRequest({
apiFn: async () => {
const { data } = await api.post('/email_xml/credenciais', { email, senha });
return data;
}
});
/**
* Obtém a lista de credenciais de e-mail cadastradas.
* @returns {Promise<Object>}
*/
export const getEmailCredentials = () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/email_xml/credenciais/apresentar');
return data;
}
});
/**
* Atualiza uma credencial de e-mail existente.
* @param {number} id
* @param {string} email
* @param {string} senha
* @returns {Promise<Object>}
*/
export const updateEmailCredentials = (id, email, senha) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`/email_xml/credenciais/${id}`, { email, senha });
return data;
}
});
/**
* Inicia o processamento de e-mails para extração de XML.
* @param {Object} data { desde_data, assunto_filtro, salvar_xmls }
* @returns {Promise<Object>}
*/
export const processEmails = (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post('/email_xml/processar', payload);
return data;
}
});
/**
* Valida um arquivo XML enviado.
* @param {File} file
* @returns {Promise<Object>}
*/
export const validateXML = (file) => handleRequest({
apiFn: async () => {
const formData = new FormData();
formData.append('file', file);
const { data } = await api.post('/email_xml/validar', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return data;
}
});

View File

@ -75,3 +75,6 @@ export const prafrotStatisticsService = {
apiFn: () => api.get(ENDPOINTS.QUANTITATIVO_MANUTENCAO)
})
};

View File

@ -1,143 +0,0 @@
import api from '@/services/api';
/**
* Envia uma nova solicitação de liberação de viagem (Portal do Motorista v2.0).
* Endpoint: POST /solicitacoes
*/
export const submitTripRequest = async (data) => {
try {
const response = await api.post('/solicitacoes', data);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao enviar solicitação de viagem');
}
};
/**
* Edita uma solicitação de viagem existente.
* Endpoint: PUT /solicitacao/<idsolicitacoes>
*/
export const updateTripRequest = async (id, data) => {
try {
const response = await api.put(`/solicitacao/${id}`, data);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao editar solicitação');
}
};
/**
* Exclui (deleta) uma solicitação de viagem.
* Endpoint: PUT /cadastro_frota/delete/<idsolicitacoes>
*/
export const deleteTripRequest = async (id) => {
try {
const response = await api.put(`/cadastro_frota/delete/${id}`);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao excluir solicitação');
}
};
/**
* Atualiza apenas o status de uma solicitação.
* Endpoint: PUT /solicitacao/edicao_status/<idsolicitacoes>
*/
export const updateTripStatus = async (id, status) => {
try {
const response = await api.put(`/solicitacao/edicao_status/${id}`, { status });
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao atualizar status da solicitação');
}
};
/**
* Busca todas as solicitações de viagem.
* Endpoint: GET /solicitacao/apresentar
*/
export const getTripRequests = async () => {
try {
const response = await api.get('/solicitacao/apresentar');
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao buscar solicitações');
}
};
/**
* Fecha uma solicitação de viagem.
* Endpoint: GET /solicitacao/fechado
*/
export const closeTripRequest = async () => {
try {
const response = await api.get('/solicitacao/fechado');
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao fechar solicitação');
}
};
/**
* Cria uma nova operação.
* Endpoint: POST /operacoes
*/
export const createOperation = async (operacao) => {
try {
const response = await api.post('/operacoes', { operacao });
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao criar operação');
}
};
/**
* Busca todas as operações configuradas.
* Endpoint: GET /operacoes/apresentar
*/
export const getOperations = async () => {
try {
const response = await api.get('/operacoes/apresentar');
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao buscar operações');
}
};
/**
* Exclui (deleta) uma operação.
* Endpoint: PUT /cadastro_frota/delete/<idoperacoes_solicitacao>
*/
export const deleteOperation = async (id) => {
try {
const response = await api.put(`/cadastro_frota/delete/${id}`);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao excluir operação');
}
};
/**
* Arquiva uma solicitação de viagem.
* Endpoint: PUT /cadastro_frota/arquivado/<id>
*/
export const archiveTripRequest = async (id) => {
try {
const response = await api.put(`/operacoes/arquivado/${id}`);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao arquivar solicitação');
}
};
/**
* Busca dados da NFe pela chave.
* Endpoint: GET /nfe/<chave>
*/
export const getNfe = async (chave) => {
try {
const response = await api.get(`/nfe/${chave}`);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.message || 'Erro ao buscar NFe');
}
};

View File

@ -124,3 +124,6 @@ export const extractFriendlyMessage = (error) => {
// Caso contrário, usa a tradução baseada no status
return getFriendlyErrorMessage(error);
};

View File

@ -13,7 +13,7 @@ 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-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
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>
@ -25,7 +25,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
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 => (
@ -38,7 +38,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
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"
};
@ -113,14 +113,14 @@ export default function AvailabilityView() {
<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-medium text-white tracking-tight">Disponibilidade e Agenda</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Disponibilidade e Agenda</h1>
<p className="text-slate-500 text-sm">Visualização de disponibilidade da frota.</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-lg text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
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)}
@ -138,11 +138,11 @@ export default function AvailabilityView() {
columns={[
{ header: 'ID', field: 'iddisponibilidade_frota', width: '80px' },
{ header: 'VEÍCULO ID', field: 'idveiculo_frota', width: '100px' },
{ header: 'PLACA', field: 'placa', width: '120px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '120px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'DATA', field: 'disponibilidade', width: '180px', render: (row) => row.disponibilidade?.split('T')[0] },
{ header: 'STATUS', field: 'status_disponibilidade', width: '150px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border ${
row.status_disponibilidade === 'Disponível' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
row.status_disponibilidade === 'Disponível' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-red-500/10 text-red-500 border-red-500/20'
}`}>
{row.status_disponibilidade}
@ -168,16 +168,16 @@ export default function AvailabilityView() {
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-4 max-h-[75vh] overflow-y-auto custom-scrollbar">
{formData.iddisponibilidade_frota && (
<div className="bg-emerald-500/5 p-3 rounded-xl border border-emerald-500/10 mb-2">
<p className="text-[10px] uppercase font-bold text-emerald-500/60 tracking-widest">ID do Registro</p>
<p className="text-lg font-bold text-emerald-500">{formData.iddisponibilidade_frota}</p>
<div className="bg-orange-500/5 p-3 rounded-xl border border-orange-500/10 mb-2">
<p className="text-[10px] uppercase font-bold text-orange-500/60 tracking-widest">ID do Registro</p>
<p className="text-lg font-bold text-orange-500">{formData.iddisponibilidade_frota}</p>
</div>
)}
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Placa (Pesquisar)</label>
<input
list="veiculos-list"
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 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
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"
value={formData.placa}
onChange={handlePlateChange}
placeholder="Digite ou selecione a placa..."
@ -206,3 +206,6 @@ export default function AvailabilityView() {
</div>
);
}

View File

@ -15,7 +15,7 @@ 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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -27,7 +27,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -40,7 +40,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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",
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"
};
@ -132,14 +132,14 @@ export default function ClaimsView() {
<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-medium text-white tracking-tight">Sinistros e Devoluções</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Sinistros e Devoluções</h1>
<p className="text-slate-500 text-sm">Gestão de incidentes e movimentações de frota.</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"
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-orange-500 w-full md:w-64"
placeholder="Buscar registro..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -157,7 +157,7 @@ export default function ClaimsView() {
loading={loading}
columns={[
{ header: 'ID', field: 'idsinistro_devolucao_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS', field: 'status', width: '120px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border ${
row.status === 'Sinistro' ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' :
@ -238,7 +238,7 @@ export default function ClaimsView() {
<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"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
>
{tab}
</TabsTrigger>
@ -295,7 +295,7 @@ export default function ClaimsView() {
<div className="gap-1.5 flex flex-col">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-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-xl 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-[80px]"
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-orange-500 transition-all placeholder:text-slate-400 min-h-[80px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
@ -375,3 +375,6 @@ export default function ClaimsView() {
</div>
);
}

View File

@ -1,9 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react';
import { prafrotService, saveEmailCredentials, getEmailCredentials, updateEmailCredentials } from '../services/prafrotService';
import { useAuthContext } from '@/components/shared/AuthProvider';
import { prafrotService } from '../services/prafrotService';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search, Settings, Save, X, Edit, Trash2, Mail, Key, Loader2, ShieldCheck } from 'lucide-react';
import { toast } from 'sonner';
import { Plus, Search, Settings, Save, X, Edit, Trash2 } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
@ -14,7 +12,7 @@ const SidebarItem = ({ active, label, onClick }) => (
onClick={onClick}
className={`w-full text-left px-4 py-3 text-sm font-medium transition-colors border-l-2 ${
active
? 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-500 border-emerald-500'
? 'bg-orange-50 dark:bg-orange-500/10 text-orange-600 dark:text-orange-500 border-orange-500'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-[#2a2a2a] border-transparent'
}`}
>
@ -25,7 +23,7 @@ const SidebarItem = ({ active, label, onClick }) => (
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-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
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",
danger: "bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-500 border border-red-500/20"
@ -42,7 +40,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-2">
{label && <label className="text-[11px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1 block">{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.5 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"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2.5 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>
@ -60,109 +58,6 @@ export default function ConfigView() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [formData, setFormData] = useState({});
const { user } = useAuthContext();
// Credentials State
const [credentialsList, setCredentialsList] = useState([]);
const [isSavingCreds, setIsSavingCreds] = useState(false);
// Load Credentials when route is selected
useEffect(() => {
if (selectedRoute === 'credentials') {
const fetchCreds = async () => {
try {
const data = await getEmailCredentials();
// Ensure data is array
const list = Array.isArray(data) ? data : (data.results || []);
setCredentialsList(list);
} catch (error) {
console.error(error);
toast.error("Erro ao carregar credenciais");
}
};
fetchCreds();
}
}, [selectedRoute]);
const handleSaveCredentialItem = async (e) => {
e.preventDefault();
const email = formData.email;
const senha = formData.senha;
if (!email || !senha) {
toast.error("Preencha e-mail e senha");
return;
}
setIsSavingCreds(true);
try {
if (editingItem) {
const id = editingItem.id || editingItem.idemail_credenciais;
if (!id) {
toast.error("ID da credencial não encontrado.");
return;
}
await updateEmailCredentials(id, email, senha);
toast.success("Credencial atualizada!");
} else {
await saveEmailCredentials(email, senha);
toast.success("Credencial criada!");
}
// Refresh
const data = await getEmailCredentials();
const list = Array.isArray(data) ? data : (data.results || []);
setCredentialsList(list);
setIsModalOpen(false);
} catch (error) {
toast.error(error.message || "Erro ao salvar credencial");
} finally {
setIsSavingCreds(false);
}
};
const filteredConfigOptions = useMemo(() => {
// 1. Identificação de Setores (Alinhada com integra_user_prafrot via context)
const userSetores = user?.setores || [];
// Tenta obter especificamente do usuário prafrot caso o contexto global divirja
let allSetores = [...userSetores];
try {
const prafrotUserJson = localStorage.getItem('integra_user_prafrot');
if (prafrotUserJson) {
const prafrotUser = JSON.parse(prafrotUserJson);
if (prafrotUser.setores) {
allSetores = [...new Set([...allSetores, ...prafrotUser.setores])];
}
}
} catch (e) {
console.warn('ConfigView: Erro ao ler integra_user_prafrot');
}
const isFinanceiro = allSetores.includes('Financeiro');
const isMonitoramento = allSetores.includes('Monitoramento');
// Se ambos ou se não tiver informação de setor, mostra tudo
if ((isFinanceiro && isMonitoramento) || allSetores.length === 0) {
return configOptions;
}
const filtered = {};
Object.entries(configOptions).forEach(([key, opt]) => {
const label = opt.label?.toLowerCase() || '';
const isValidacao = label.includes('validação') || key.toLowerCase().includes('validacao');
if (isFinanceiro) {
// Financeiro só vê validação
if (isValidacao) filtered[key] = opt;
} else if (isMonitoramento) {
// Monitoramento vê tudo exceto validação
if (!isValidacao) filtered[key] = opt;
}
});
return filtered;
}, [configOptions, user]);
// Fetch Config Options on Mount
useEffect(() => {
@ -186,8 +81,6 @@ export default function ConfigView() {
// Fetch Items when Selected Route changes
useEffect(() => {
if (!selectedRoute) return;
// Don't fetch generic items if we are in credentials mode
if (selectedRoute === 'credentials') return;
const loadItems = async () => {
setLoadingItems(true);
@ -241,18 +134,6 @@ export default function ConfigView() {
// Actions
const handleOpenModal = (item = null) => {
if (selectedRoute === 'credentials') {
if (item) {
setEditingItem(item);
setFormData({ email: item.email, senha: '' }); // Password usually empty or placeholder
} else {
setEditingItem(null);
setFormData({ email: '', senha: '' });
}
setIsModalOpen(true);
return;
}
if (item) {
setEditingItem(item);
setFormData({ ...item });
@ -346,7 +227,7 @@ export default function ConfigView() {
}
};
const selectedLabel = selectedRoute === 'credentials' ? 'Credenciais' : (filteredConfigOptions[selectedRoute]?.label || selectedRoute);
const selectedLabel = configOptions[selectedRoute]?.label || selectedRoute;
return (
<div className="flex h-screen bg-slate-50 dark:bg-[#0f0f0f] overflow-hidden">
@ -354,19 +235,13 @@ export default function ConfigView() {
<div className="w-64 bg-white dark:bg-[#141414] border-r border-slate-200 dark:border-[#2a2a2a] flex flex-col">
<div className="p-4 border-b border-slate-200 dark:border-[#2a2a2a]">
<h2 className="text-lg font-bold text-slate-800 dark:text-white flex items-center gap-2">
<Settings className="text-emerald-500" size={20} />
<Settings className="text-orange-500" size={20} />
Configurações
</h2>
<p className="text-xs text-slate-500 mt-1">Gerencie as listas do sistema</p>
</div>
<div className="flex-1 overflow-y-auto py-2 custom-scrollbar">
<SidebarItem
active={selectedRoute === 'credentials'}
label="Credenciais de Acesso"
onClick={() => setSelectedRoute('credentials')}
/>
<div className="h-px bg-slate-200 dark:bg-[#2a2a2a] mx-4 my-2" />
{Object.entries(filteredConfigOptions).map(([key, opt]) => (
{Object.entries(configOptions).map(([key, opt]) => (
<SidebarItem
key={key}
active={selectedRoute === opt.rota}
@ -382,42 +257,33 @@ export default function ConfigView() {
{/* Header */}
<div className="bg-white dark:bg-[#141414] border-b border-slate-200 dark:border-[#2a2a2a] px-8 py-5 flex justify-between items-center">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">{selectedLabel}</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">{selectedLabel}</h1>
<p className="text-slate-500 text-sm">Gerenciamento de opções para {selectedLabel?.toLowerCase()}</p>
</div>
<div className="flex items-center gap-3">
{selectedRoute !== 'credentials' && (
<>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-slate-50 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-emerald-500 w-64"
placeholder="Pesquisar..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Item
</DarkButton>
</>
)}
{selectedRoute === 'credentials' && (
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Nova Credencial
</DarkButton>
)}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-slate-50 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-64"
placeholder="Pesquisar..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Item
</DarkButton>
</div>
</div>
{/* Table or Content */}
{/* Table */}
<div className="flex-1 p-8 overflow-hidden">
<div className="h-full bg-white dark:bg-[#141414] rounded-xl shadow-sm border border-slate-200 dark:border-[#2a2a2a] overflow-hidden flex flex-col">
{loadingItems ? (
<div className="flex-1 flex items-center justify-center text-slate-500">
Carregando...
</div>
) : (selectedRoute !== 'credentials' && (
) : (
<ExcelTable
data={filteredItems}
columns={columns}
@ -431,21 +297,6 @@ export default function ConfigView() {
return idKey ? row[idKey] : Math.random();
}}
/>
))}
{selectedRoute === 'credentials' && (
<div className="flex bg-white dark:bg-[#1a1a1a] rounded-2xl w-full h-full p-4 overflow-hidden">
<ExcelTable
data={credentialsList}
columns={[
{ header: 'ID', field: 'idemail_credenciais', width: '80px' },
{ header: 'E-mail Monitorado', field: 'email', width: '300px' },
{ header: 'Data Criação', field: 'criado_em', width: '150px', render: (row) => row.criado_em ? new Date(row.criado_em).toLocaleDateString() : '---' }
]}
onEdit={handleOpenModal}
rowKey={(row) => row.id || row.idemail_credenciais}
/>
</div>
)}
</div>
</div>
@ -456,48 +307,13 @@ export default function ConfigView() {
<DialogContent className="max-w-2xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200">
<DialogHeader className="border-b border-slate-200 dark:border-[#2a2a2a] pb-4">
<DialogTitle className="text-slate-800 dark:text-white">
{selectedRoute === 'credentials'
? (editingItem ? 'Editar Credencial' : 'Nova Credencial')
: (editingItem ? `Editar ${selectedLabel}` : `Novo Item em ${selectedLabel}`)
}
{editingItem ? `Editar ${selectedLabel}` : `Novo Item em ${selectedLabel}`}
</DialogTitle>
</DialogHeader>
<form onSubmit={selectedRoute === 'credentials' ? handleSaveCredentialItem : handleSave} className="py-4 grid grid-cols-1 gap-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
{selectedRoute === 'credentials' ? (
<>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1">E-mail Monitorado</label>
<div className="relative group">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-emerald-500 transition-colors" size={16} />
<input
type="email"
value={formData.email || ''}
onChange={(e) => setFormData({...formData, email: e.target.value})}
className="w-full pl-10 pr-4 py-3 bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl text-sm font-medium focus:outline-none focus:border-emerald-500 transition-all text-slate-800 dark:text-slate-200"
placeholder="exemplo@email.com"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1">Senha de Aplicativo</label>
<div className="relative group">
<Key className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-emerald-500 transition-colors" size={16} />
<input
type="password"
value={formData.senha || ''}
onChange={(e) => setFormData({...formData, senha: e.target.value})}
className="w-full pl-10 pr-4 py-3 bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl text-sm font-medium focus:outline-none focus:border-emerald-500 transition-all text-slate-800 dark:text-slate-200"
placeholder="••••••••••••"
/>
</div>
<p className="text-[10px] text-slate-400 pl-1">* Utilize uma senha de aplicativo gerada pelo provedor de e-mail por segurança.</p>
</div>
</>
) : (
<>
{/* Dynamic Form Generation for Other Configs */}
{Object.keys(formData).map(key => {
<form onSubmit={handleSave} className="py-4 grid grid-cols-1 gap-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
{/* Dynamic Form Generation */}
{Object.keys(formData).map(key => {
// Hide ID fields completely from form
const isId = key.toLowerCase().startsWith('id') || key === 'created_at' || key === 'updated_at';
if (isId) return null;
@ -530,14 +346,12 @@ export default function ConfigView() {
</div>
</div>
)}
</>
)}
</form>
<DialogFooter className="border-t border-slate-200 dark:border-[#2a2a2a] pt-4">
<DarkButton variant="ghost" type="button" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit" onClick={selectedRoute === 'credentials' ? handleSaveCredentialItem : handleSave}>
{isSavingCreds ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />} Salvar
<DarkButton variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton onClick={handleSave}>
<Save size={16} /> Salvar
</DarkButton>
</DialogFooter>
</DialogContent>
@ -545,3 +359,6 @@ export default function ConfigView() {
</div>
);
}

View File

@ -21,7 +21,7 @@ const StatCard = ({ title, value, subtext, icon: Icon, color, trend }) => (
</div>
{trend && (
<div className="mt-4 flex items-center gap-2">
<span className={`text-xs font-bold px-1.5 py-0.5 rounded flex items-center gap-1 ${trend > 0 ? 'text-emerald-500 bg-emerald-500/10' : 'text-red-500 bg-red-500/10'}`}>
<span className={`text-xs font-bold px-1.5 py-0.5 rounded flex items-center gap-1 ${trend > 0 ? 'text-orange-500 bg-orange-500/10' : 'text-red-500 bg-red-500/10'}`}>
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{Math.abs(trend)}%
</span>
@ -37,7 +37,7 @@ export default function DashboardView() {
<div className="space-y-8">
{/* Header */}
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Visão Geral</h1>
<h1 className="text-2xl font-bold text-white tracking-tight">Visão Geral</h1>
<p className="text-slate-500 text-sm">Monitoramento em tempo real da operação.</p>
</div>
@ -64,7 +64,7 @@ export default function DashboardView() {
value="94.2%"
subtext="Meta: 95%"
icon={CheckCircle2}
color="bg-emerald-500"
color="bg-orange-500"
trend={1.8}
/>
<StatCard
@ -89,7 +89,7 @@ export default function DashboardView() {
<div className="space-y-4">
{[
{ label: 'SP - Capital (SRJ10)', val: 450, tot: 1240, col: 'bg-blue-600' },
{ label: 'RJ - Rio de Janeiro (GIG)', val: 320, tot: 1240, col: 'bg-emerald-500' },
{ label: 'RJ - Rio de Janeiro (GIG)', val: 320, tot: 1240, col: 'bg-orange-500' },
{ label: 'MG - Belo Horizonte', val: 210, tot: 1240, col: 'bg-yellow-500' },
{ label: 'Outras Bases', val: 260, tot: 1240, col: 'bg-slate-600' },
].map((item, i) => (
@ -134,3 +134,6 @@ export default function DashboardView() {
</div>
);
}

View File

@ -36,7 +36,7 @@ const DispatcherView = () => {
return (
<div className="h-full flex flex-col p-4 gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-medium tracking-tight text-white">Dispatcher</h1>
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">Dispatcher</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">Visualização de dados do Dispatcher.</p>
</div>
@ -54,3 +54,6 @@ const DispatcherView = () => {
};
export default DispatcherView;

View File

@ -1,220 +0,0 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Truck, ArrowLeft, Send, CheckCircle2, ChevronRight, Phone, ShieldCheck, Mail, Loader2 } from 'lucide-react';
import { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
import { TripRequestForm } from '../components/TripRequestForm';
import { getOperations } from '../services/prafrotService';
export const DriverPortalView = () => {
useDocumentMetadata('Portal do Motorista', 'prafrot');
const [step, setStep] = useState('selection'); // selection, form, success
const [selectedOp, setSelectedOp] = useState(null);
const [operations, setOperations] = useState([]);
const [loadingOps, setLoadingOps] = useState(true);
useEffect(() => {
const fetchOps = async () => {
try {
const data = await getOperations();
// Backend returns list of { idoperacoes_solicitacao, operacao }
const formatted = (Array.isArray(data) ? data : data?.data || []).map(op => ({
id: op.idoperacoes_solicitacao,
label: op.operacao,
icon: Truck
}));
setOperations(formatted.length > 0 ? formatted : [
{ id: 'ASVD', label: 'ASVD', icon: Truck },
{ id: 'CDI', label: 'CDI', icon: Truck },
{ id: 'ORTOBOM', label: 'ORTOBOM RJ', icon: Truck },
{ id: 'IMPERIO', label: 'IMPERIO', icon: Truck },
{ id: 'ITAIPAVA', label: 'ITAIPAVA', icon: Truck }
]);
} catch (error) {
console.error('Erro ao buscar operações:', error);
} finally {
setLoadingOps(false);
}
};
fetchOps();
}, []);
const handleOpSelect = (op) => {
setSelectedOp(op);
setStep('form');
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleSuccess = () => {
setStep('success');
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const reset = () => {
setSelectedOp(null);
setStep('selection');
};
return (
<div className="min-h-screen bg-[#0a0a0b] text-white font-sans selection:bg-emerald-500/30 selection:text-emerald-200 overflow-x-hidden" style={{ fontFamily: 'var(--font-main)' }}>
{/* Premium Background */}
<div className="fixed inset-0 z-0">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-600/10 blur-[120px] rounded-full animate-pulse" />
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/10 blur-[120px] rounded-full animate-pulse" style={{ animationDelay: '2s' }} />
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 brightness-100 contrast-150 pointer-events-none" />
</div>
<div className="relative z-10 max-w-md md:max-w-5xl mx-auto px-5 py-10 md:py-16">
{/* Header */}
<header className="text-center mb-10 space-y-3">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/20 rounded-full text-emerald-400 text-[9px] font-bold uppercase tracking-[0.2em] mb-2"
>
<ShieldCheck size={12} /> Prafrot System
</motion.div>
<motion.h1
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-3xl md:text-5xl font-medium tracking-tight leading-none"
>
SOLICITAÇÃO DE <span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-emerald-600">ROTA</span>
</motion.h1>
<p className="text-zinc-500 text-xs md:text-sm font-medium uppercase tracking-widest opacity-80">Portal do Motorista</p>
</header>
<AnimatePresence mode="wait">
{step === 'selection' && (
<motion.div
key="selection"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-6"
>
<div className="text-center mb-4">
<h2 className="text-sm md:text-base font-medium text-emerald-500 uppercase tracking-[0.2em]">Selecione a Operação</h2>
</div>
<style>{`
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.3);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(16, 185, 129, 0.5);
}
`}</style>
<div className="flex flex-col gap-3 max-h-[400px] overflow-y-auto custom-scrollbar pr-2">
{loadingOps ? (
<div className="flex justify-center py-20">
<Loader2 className="animate-spin text-emerald-500" size={32} />
</div>
) : (
operations.map((op, idx) => (
<motion.button
key={op.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
onClick={() => handleOpSelect(op)}
className="group relative flex items-center justify-between p-5 bg-zinc-900/80 hover:bg-zinc-800 border border-zinc-800/50 hover:border-emerald-500/50 rounded-[28px] transition-all duration-300 active:scale-[0.97]"
>
<div className="flex items-center gap-4">
<div className="w-11 h-11 bg-zinc-800/50 group-hover:bg-emerald-500/20 rounded-2xl flex items-center justify-center transition-colors">
<op.icon size={20} className="text-zinc-500 group-hover:text-emerald-400" />
</div>
<span className="text-base font-black text-zinc-300 group-hover:text-white uppercase tracking-tight">{op.label}</span>
</div>
<div className="w-8 h-8 rounded-full bg-zinc-800/50 flex items-center justify-center group-hover:bg-emerald-500/20 transition-colors">
<ChevronRight size={18} className="text-zinc-600 group-hover:text-emerald-400" />
</div>
</motion.button>
))
)}
</div>
</motion.div>
)}
{step === 'form' && (
<motion.div
key="form"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
>
<div className="flex flex-col gap-4 mb-8">
<button
onClick={reset}
className="flex items-center gap-2 text-zinc-600 hover:text-white text-[10px] font-black uppercase tracking-widest transition-colors w-fit"
>
<ArrowLeft size={14} /> Voltar
</button>
<div className="flex items-center justify-between p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-[24px]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-emerald-500/20 rounded-xl flex items-center justify-center">
<Truck size={16} className="text-emerald-400" />
</div>
<span className="text-xs font-black text-emerald-400 uppercase tracking-widest">{selectedOp?.label}</span>
</div>
<span className="text-[9px] font-black text-emerald-500/50 uppercase tracking-widest">Passo 02</span>
</div>
</div>
<TripRequestForm
operation={selectedOp}
onSuccess={handleSuccess}
/>
</motion.div>
)}
{step === 'success' && (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-16 bg-zinc-900/50 border border-zinc-800/50 rounded-[40px] px-8"
>
<div className="w-20 h-20 bg-emerald-500/20 rounded-3xl flex items-center justify-center mx-auto mb-8 shadow-2xl shadow-emerald-500/20 -rotate-6">
<CheckCircle2 size={40} className="text-emerald-400" />
</div>
<h2 className="text-2xl font-black mb-3 leading-tight">SOLICITAÇÃO<br /><span className="text-emerald-500">ENVIADA!</span></h2>
<p className="text-zinc-500 text-xs font-bold leading-relaxed mb-10 max-w-[240px] mx-auto uppercase tracking-widest opacity-80">
Sua liberação está sendo processada pela nossa equipe.
</p>
<button
onClick={reset}
className="w-full py-5 bg-zinc-800 hover:bg-zinc-700 text-white font-black rounded-3xl text-[10px] uppercase tracking-[0.3em] transition-all active:scale-95 shadow-xl shadow-black/20"
>
Novo Registro
</button>
</motion.div>
)}
</AnimatePresence>
{/* Footer info */}
{/* <footer className="mt-16 pt-10 border-t border-zinc-900/50 text-center">
<p className="text-zinc-600 text-[9px] font-black uppercase tracking-[0.3em] mb-6 opacity-50">Central de Atendimento</p>
<div className="flex flex-col gap-4 items-center">
<a href="#" className="flex items-center gap-3 text-zinc-500 hover:text-emerald-400 transition-colors text-xs font-black tracking-tight">
<div className="w-8 h-8 rounded-full bg-zinc-900 flex items-center justify-center border border-zinc-800"><Phone size={14} /></div>
0800 123 4567
</a>
<a href="#" className="flex items-center gap-3 text-zinc-500 hover:text-emerald-400 transition-colors text-xs font-black tracking-tight">
<div className="w-8 h-8 rounded-full bg-zinc-900 flex items-center justify-center border border-zinc-800"><Mail size={14} /></div>
suporte@pralog.com.br
</a>
</div>
</footer> */}
</div>
</div>
);
};

View File

@ -11,7 +11,7 @@ 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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -20,7 +20,7 @@ const DarkInput = ({ label, ...props }) => (
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",
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"
};
@ -52,13 +52,13 @@ export default function DriversView() {
<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-medium text-white tracking-tight">Motoristas</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Motoristas</h1>
</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"
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-orange-500 w-full md:w-64"
placeholder="Buscar motorista..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -71,7 +71,7 @@ export default function DriversView() {
<ExcelTable
data={filteredData}
columns={[
{ header: 'NOME', field: 'NOME_FAVORECIDO', width: '300px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'NOME', field: 'NOME_FAVORECIDO', width: '300px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'CPF/CNPJ', field: 'CPF_CNPJ_FAVORECIDO', width: '180px' },
{ header: 'TELEFONE', field: 'TELEFONE', width: '150px' },
{ header: 'ENDEREÇO', field: 'ENDERECO', width: '250px' },
@ -87,3 +87,6 @@ export default function DriversView() {
</div>
);
}

View File

@ -1,714 +0,0 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useMaintenance } from '../hooks/useMaintenance';
import { useFleetLists } from '../hooks/useFleetLists';
const SmartTable = React.lazy(() => import('@/features/dev-tools/components/SmartTable')); // Universal Connector
import { Search, Wrench, CheckCircle, AlertTriangle, FileText, Edit2, History } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { LoadingOverlay } from '../../../components/shared/LoadingOverlay';
import { prafrotService } from '../services/prafrotService';
// --- Styled Components & Helpers ---
const StatusTag = ({ status }) => {
const styles = {
'Pendente Aprovação': 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20',
'Pendente Pagamento': 'bg-amber-500/10 text-amber-500 border-amber-500/20',
'Aprovado': 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20',
'default': 'bg-slate-700/30 text-slate-400 border-slate-600/30'
};
const currentStyle = styles[status] || styles['default'];
return (
<span className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border whitespace-nowrap ${currentStyle}`}>
{status}
</span>
);
};
// --- Reusing Styled Components ---
const DarkInput = ({ label, readOnly, className = '', ...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
readOnly={readOnly}
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 ${readOnly ? 'cursor-not-allowed opacity-80 bg-slate-100 dark:bg-[#1a1a1a]' : ''} ${className}`}
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange, disabled }) => (
<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)}
disabled={disabled}
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 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<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",
danger: "bg-rose-600 hover:bg-rose-500 text-white shadow-rose-500/10"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
const formatCurrency = (v) => {
if (v == null || v === '') return 'R$ 0,00';
const n = typeof v === 'string' ? parseCurrency(v) : Number(v);
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n || 0);
};
const parseCurrency = (s) => {
if (s == null || s === '') return '';
let t = String(s).trim().replace(/\s/g, '').replace(/R\$\s?/gi, '');
if (!t) return '';
if (t.includes(',')) {
t = t.replace(/\./g, '').replace(',', '.');
} else if (t.includes('.')) {
const lastDot = t.lastIndexOf('.');
const afterDot = t.slice(lastDot + 1);
const isLikelyUsDecimal = afterDot.length === 2 && /^\d\d$/.test(afterDot) && (t.match(/\./g) || []).length === 1;
if (!isLikelyUsDecimal) {
t = t.replace(/\./g, '');
}
}
const n = parseFloat(t);
return isNaN(n) ? '' : n;
};
const isInternalFileUrl = (url) => {
if (!url || typeof url !== 'string') return false;
try {
const u = new URL(url);
return u.origin === window.location.origin;
} catch {
return false;
}
};
const DetailSection = ({ title, children }) => (
<div className="space-y-4">
<h3 className="text-xs font-bold text-emerald-600 dark:text-emerald-500 uppercase tracking-[0.2em] border-b border-emerald-500/10 pb-2">{title}</h3>
{children}
</div>
);
function FileLink({ label, url }) {
if (!url) return null;
return (
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4 flex flex-col gap-1 transition-all hover:border-emerald-500/30 group">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider font-mono block mb-0.5">{label}</label>
<a
href={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600 underline font-bold text-sm inline-flex items-center gap-1.5"
>
<FileText size={14} className="group-hover:scale-110 transition-transform" />
Abrir documento
</a>
</div>
);
}
export default function FinancePendenciesView() {
const {
maintenances,
loading,
fetchMaintenances,
updateMaintenance,
updateMaintenanceBatch
} = useMaintenance();
const { fetchListsConfig, validacaoOptions } = useFleetLists();
const [activeTab, setActiveTab] = useState('pendente-aprovacao');
const [selectedMaintenance, setSelectedMaintenance] = useState(null);
const [isDetailOpen, setIsDetailOpen] = useState(false);
const [obsFinanceiro, setObsFinanceiro] = useState('');
const [validacaoFinanceiro, setValidacaoFinanceiro] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
// No modo financeiro, começamos buscando apenas o que está pendente de aprovação
const backendStatus = activeTab === 'pendente-aprovacao' ? 'Pendente Aprovação'
: activeTab === 'pendente-pagamento' ? 'Pendente Pagamento'
: 'Aprovado';
fetchMaintenances(backendStatus);
fetchListsConfig();
}, [fetchMaintenances, fetchListsConfig, activeTab]);
const filteredMaintenances = useMemo(() => {
// Camada de segurança (Safety Filter) para garantir que statuses não se misturem
const backendStatus = activeTab === 'pendente-aprovacao' ? 'Pendente Aprovação'
: activeTab === 'pendente-pagamento' ? 'Pendente Pagamento'
: 'Aprovado';
return maintenances.filter(item => {
// Filtro de Status (Segurança)
if ((item.status || '').trim() !== backendStatus) return false;
// Filtro de Busca
return (
item.placa?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.oficina?.toLowerCase().includes(searchTerm.toLowerCase()) ||
String(item.idmanutencao_frota || item.id || '').toLowerCase().includes(searchTerm.toLowerCase())
);
});
}, [maintenances, searchTerm, activeTab]);
const handleOpenDetail = async (m) => {
// Busca o item completo do array para garantir que temos todos os campos antes de editar
const fullItem = (maintenances || []).find(item => (item.idmanutencao_frota || item.id) === (m.idmanutencao_frota || m.id)) || m;
setSelectedMaintenance(fullItem);
setObsFinanceiro(fullItem.obs_financeiro || '');
setValidacaoFinanceiro(fullItem.validacao_financeiro || '');
setIsDetailOpen(true);
};
const handleApprove = async () => {
if (!selectedMaintenance) return;
setIsSubmitting(true);
try {
// Para evitar perda de dados, enviamos o objeto completo mesclado com as novas informações
const payload = {
...selectedMaintenance,
status: 'Aprovado',
validacao_financeiro: validacaoFinanceiro,
obs_financeiro: obsFinanceiro
};
// Removemos IDs nulos ou campos de controle se necessário, mas o principal é o objeto base estar lá
await updateMaintenance(selectedMaintenance.idmanutencao_frota || selectedMaintenance.id, payload);
setIsDetailOpen(false);
// Recarrega a aba atual para o item sumir da lista
const backendStatus = activeTab === 'pendente-aprovacao' ? 'Pendente Aprovação'
: activeTab === 'pendente-pagamento' ? 'Pendente Pagamento'
: 'Aprovado';
fetchMaintenances(backendStatus);
} catch (error) {
console.error('Erro ao aprovar:', error);
} finally {
setIsSubmitting(false);
}
};
const handleReturn = async () => {
if (!selectedMaintenance || !obsFinanceiro) {
alert('Por favor, descreva o motivo do retorno na observação financeira.');
return;
}
setIsSubmitting(true);
try {
// Determina o status de destino baseado no status atual
const currentStatus = (selectedMaintenance.status || '').trim();
const targetStatus = currentStatus === 'Pendente Pagamento' ? 'Pendente Aprovação' : 'Pendente Pagamento';
const payload = {
...selectedMaintenance,
status: targetStatus,
obs_financeiro: obsFinanceiro
};
await updateMaintenance(selectedMaintenance.idmanutencao_frota || selectedMaintenance.id, payload);
setIsDetailOpen(false);
// Recarrega a aba atual para o item sumir da lista
const backendStatus = activeTab === 'pendente-aprovacao' ? 'Pendente Aprovação'
: activeTab === 'pendente-pagamento' ? 'Pendente Pagamento'
: 'Aprovado';
fetchMaintenances(backendStatus);
} catch (error) {
console.error('Erro ao retornar:', error);
} finally {
setIsSubmitting(false);
}
};
const columns = [
{ header: 'ID', field: 'idmanutencao_frota', width: '80px' },
{ header: 'ATENDIMENTO', field: 'atendimento', width: '100px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA RESERVA', field: 'placa_reserva', width: '100px' },
{ header: 'MODELO', field: 'modelo', width: '110px' },
{ header: 'OFICINA', field: 'oficina', width: '160px' },
{ header: 'BASE FROTA', field: 'base_frota', width: '100px' },
{ header: 'CIDADE', field: 'cidade', width: '120px' },
{ header: 'UF', field: 'uf', width: '60px' },
{ header: 'PROPRIETÁRIO', field: 'proprietario', width: '110px' },
{ header: 'RESPONSÁVEL', field: 'responsavel', width: '120px' },
{ header: 'MOTIVO ATEND.', field: 'motivo_atendimento', width: '120px' },
{ header: 'STATUS', field: 'status', width: '140px', render: (row) => <StatusTag status={row.status} /> },
{ header: 'MANUTENÇÃO', field: 'manutencao', width: '90px' },
{ header: 'STATUS FROTA', field: 'status_frota_veiculo', width: '120px' },
{ header: 'DATA SOLIC.', field: 'data_solicitacao', width: '100px' },
{ header: 'DATA AGEND.', field: 'data_agendamento', width: '100px' },
{ header: 'DATA PARADA', field: 'data_parada_veiculo', width: '100px' },
{ header: 'PREV. ENTREGA', field: 'previsao_entrega', width: '100px' },
{ header: 'DATA FINAL.', field: 'data_finalizacao', width: '100px' },
{ header: 'DATA RETIRADA', field: 'data_retirada', width: '100px' },
{ header: 'ORÇ. INICIAL', field: 'orcamento_inicial', width: '110px', className: 'font-mono text-emerald-600 dark:text-emerald-400', render: (row) => formatCurrency(row.orcamento_inicial) },
{ header: 'ORÇ. FINAL', field: 'orcamento_final', width: '110px', className: 'font-mono text-emerald-600 dark:text-emerald-400', render: (row) => formatCurrency(row.orcamento_final) },
{ header: 'DIF. ORÇ.', field: 'dif_orcamento', width: '100px', className: 'font-mono', render: (row) => formatCurrency(row.dif_orcamento) },
{ header: 'COND. PAG.', field: 'condicao_pagamento', width: '120px', render: (row) => (
<span>
{row.condicao_pagamento}
{row.condicao_pagamento === 'Parcelado' && row.qtd_parcelas_condicao_pag && (
<span className="ml-1 text-emerald-500 font-bold">({row.qtd_parcelas_condicao_pag}x)</span>
)}
</span>
)},
{ header: 'VALID. FINANC.', field: 'validacao_financeiro', width: '110px' },
{ header: 'RESP. APROV.', field: 'resp_aprovacao', width: '110px' },
{ header: 'SLA OFICINA', field: 'sla_oficina', width: '100px' },
{ header: 'SLA PÓS-OF.', field: 'sla_pos_oficina', width: '100px' },
{ header: 'OBS', field: 'obs', width: '180px' },
{ header: 'END. PRESTADOR', field: 'endereco_prestador', width: '180px' },
{ header: 'PDF ORÇAMENTO', field: 'pdf_orcamento_link', width: '100px', render: (row) => (row.pdf_orcamento_link || row.pdf_orcamento) ? <a href={row.pdf_orcamento_link || row.pdf_orcamento} target="_blank" rel="noreferrer" className="underline text-blue-500 font-bold">Abrir</a> : '-' },
{ header: 'NOTA FISCAL', field: 'nota_fiscal', width: '100px', render: (row) => row.nota_fiscal ? <a href={row.nota_fiscal} target="_blank" rel="noreferrer" className="underline text-blue-500 font-bold">Abrir</a> : '-' },
];
return (
<div className="p-6 space-y-6 min-h-screen text-slate-700 dark:text-slate-200 transition-colors">
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-medium text-white uppercase tracking-tighter flex items-center gap-3">
<div className="p-2 bg-emerald-500 rounded-xl shadow-lg shadow-emerald-500/20">
<History size={24} className="text-white" />
</div>
Pendências
</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm font-medium mt-1">Gestão de validação e pagamentos de manutenção</p>
</div>
<div className="relative w-full md:w-80">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
<input
className="w-full 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.5 rounded-xl text-sm focus:outline-none focus:border-emerald-500 transition-all shadow-sm"
placeholder="Buscar por ID, placa ou oficina..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
</header>
<Tabs value={activeTab} onValueChange={setActiveTab} 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-6 rounded-xl">
<TabsTrigger value="pendente-aprovacao" className="data-[state=active]:bg-emerald-600 data-[state=active]:text-white text-slate-500 text-[10px] uppercase font-bold px-6 py-2.5 rounded-lg transition-all">
Pendente Aprovação
</TabsTrigger>
<TabsTrigger value="pendente-pagamento" className="data-[state=active]:bg-amber-600 data-[state=active]:text-white text-slate-500 text-[10px] uppercase font-bold px-6 py-2.5 rounded-lg transition-all">
Pendente Pagamento
</TabsTrigger>
<TabsTrigger value="validado" className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-slate-500 text-[10px] uppercase font-bold px-6 py-2.5 rounded-lg transition-all">
Validados
</TabsTrigger>
</TabsList>
<div className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-2xl overflow-hidden shadow-xl shadow-slate-200/50 dark:shadow-none">
<React.Suspense fallback={<div className="p-12 text-center"><LoadingOverlay isLoading={true} variant="minimal" /></div>}>
<SmartTable
data={filteredMaintenances}
columns={columns}
loading={loading}
onRowClick={handleOpenDetail}
/>
</React.Suspense>
</div>
</Tabs>
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
<DialogContent className="w-[95vw] max-w-4xl h-[75vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none text-slate-700 dark:text-slate-200">
<LoadingOverlay isLoading={isSubmitting} message="Processando..." variant="minimal" />
<DialogHeader className="p-6 border-b border-slate-100 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex items-center gap-4">
<div className="p-3 bg-emerald-500/10 rounded-2xl text-emerald-500 shadow-inner">
<Wrench size={24} />
</div>
<div>
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white uppercase tracking-tight">
Validação #{selectedMaintenance?.idmanutencao_frota}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Veículo: <span className="text-emerald-500 font-bold">{selectedMaintenance?.placa}</span> | Oficina: {selectedMaintenance?.oficina}
</DialogDescription>
</div>
</div>
</DialogHeader>
<Tabs defaultValue="detalhes" className="flex-1 flex flex-col overflow-hidden">
<div className="px-6 border-b border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#0f0f0f] shrink-0">
<TabsList className="bg-transparent border-0 w-fit p-0 h-auto gap-6">
<TabsTrigger
value="detalhes"
className="data-[state=active]:bg-transparent data-[state=active]:text-emerald-600 dark:data-[state=active]:text-emerald-400 data-[state=active]:border-emerald-600 dark:data-[state=active]:border-emerald-400 text-slate-500 border-b-2 border-transparent rounded-none px-0 py-4 text-[10px] uppercase font-bold tracking-wider"
>
Dados Gerais
</TabsTrigger>
{(selectedMaintenance?.pdf_orcamento || selectedMaintenance?.nota_fiscal) && (
<TabsTrigger
value="documentos"
className="data-[state=active]:bg-transparent data-[state=active]:text-emerald-600 dark:data-[state=active]:text-emerald-400 data-[state=active]:border-emerald-600 dark:data-[state=active]:border-emerald-400 text-slate-500 border-b-2 border-transparent rounded-none px-0 py-4 text-[10px] uppercase font-bold tracking-wider flex items-center gap-1.5"
>
<FileText size={14} />
Documentos
</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="detalhes" className="flex-1 overflow-y-auto custom-scrollbar m-0 p-6 space-y-8 bg-white dark:bg-[#151515]">
<DetailSection title="Identificação e veículo">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">ID Manutenção</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.idmanutencao_frota ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Atendimento</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.atendimento ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Placa</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{selectedMaintenance?.placa ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Placa Reserva</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.placa_reserva ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Modelo</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.modelo ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Proprietário</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.proprietario ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Base Frota</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.base_frota ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Manutenção (Situação)</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.manutencao ?? '-'}</p>
</div>
</div>
</DetailSection>
<DetailSection title="Status e oficina">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Status</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.status ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Status Frota Veículo</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.status_frota_veiculo ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Motivo Atendimento</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.motivo_atendimento ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Oficina</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.oficina ?? '-'}</p>
</div>
</div>
</DetailSection>
<DetailSection title="Localização">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Endereço Prestador</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.endereco_prestador ?? '-'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Cidade</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.cidade ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">UF</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.uf ?? '-'}</p>
</div>
</div>
</div>
</DetailSection>
<DetailSection title="Responsáveis e financeiro">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Responsável</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.responsavel ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Resp. Aprovação</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.resp_aprovacao ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Validação Financeiro</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.validacao_financeiro ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Cond. Pagamento</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">
{selectedMaintenance?.condicao_pagamento ?? '-'}
{selectedMaintenance?.condicao_pagamento === 'Parcelado' && selectedMaintenance?.qtd_parcelas_condicao_pag && (
<span className="ml-1 text-emerald-500">({selectedMaintenance?.qtd_parcelas_condicao_pag}x)</span>
)}
</p>
</div>
</div>
</DetailSection>
<DetailSection title="Orçamentos">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Orçamento Inicial</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{formatCurrency(selectedMaintenance?.orcamento_inicial)}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Orçamento Final</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{formatCurrency(selectedMaintenance?.orcamento_final)}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Dif. Orçamento</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{formatCurrency(selectedMaintenance?.dif_orcamento)}</p>
</div>
{selectedMaintenance?.condicao_pagamento === 'Parcelado' && selectedMaintenance?.qtd_parcelas_condicao_pag && (
<div className="bg-emerald-500/5 dark:bg-emerald-500/10 border border-emerald-500/20 rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-emerald-600 dark:text-emerald-400 tracking-wider">Valor da Parcela</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-400 mt-1 font-mono">
{(() => {
const total = parseCurrency(selectedMaintenance?.orcamento_final);
const parcelas = parseInt(selectedMaintenance?.qtd_parcelas_condicao_pag, 10);
if (total && parcelas > 0) return formatCurrency(total / parcelas);
return 'R$ 0,00';
})()}
</p>
</div>
)}
<FileLink label="PDF Orçamento" url={selectedMaintenance?.pdf_orcamento_link || selectedMaintenance?.pdf_orcamento} />
<FileLink label="Nota Fiscal" url={selectedMaintenance?.nota_fiscal} />
</div>
</DetailSection>
<DetailSection title="Datas">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Data Solicitação</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.data_solicitacao ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Data Agendamento</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.data_agendamento ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Data Parada</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.data_parada_veiculo ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Previsão Entrega</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.previsao_entrega ?? selectedMaintenance?.previcao_entrega ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Data Finalização</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.data_finalizacao ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Data Retirada</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.data_retirada ?? '-'}</p>
</div>
</div>
</DetailSection>
{/* <DetailSection title="SLA">
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">SLA Oficina</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.sla_oficina ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">SLA Pós-Oficina</label>
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">{selectedMaintenance?.sla_pos_oficina ?? '-'}</p>
</div>
</div>
</DetailSection> */}
<DetailSection title="Descrição e observações">
<div className="grid grid-cols-1 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Descrição da Manutenção</label>
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance?.manutencao ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Observações do Monitoramento</label>
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance?.obs ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Observações da Manutenção</label>
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance?.obs_manutencao ?? '-'}</p>
</div>
</div>
</DetailSection>
<DetailSection title="Validação e Retorno">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DarkSelect
label="Validação Financeiro"
options={validacaoOptions}
value={validacaoFinanceiro}
onChange={setValidacaoFinanceiro}
/>
<div className="flex flex-col gap-1.5">
<label className="text-[10px] uppercase font-bold text-slate-400 tracking-wider ml-1">Observação do Monitoramento</label>
<div className="w-full bg-slate-50 dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-xl px-3 py-2 text-sm text-slate-500 min-h-[60px]">
{selectedMaintenance?.obs || 'Sem observações'}
</div>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observação Financeira (Obrigatório para retorno)</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-xl px-4 py-3 text-sm text-slate-800 dark:text-white focus:outline-none focus:border-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-600 min-h-[100px]"
placeholder="Caso precise retornar, descreva o motivo aqui..."
value={obsFinanceiro}
onChange={(e) => setObsFinanceiro(e.target.value)}
/>
</div>
</DetailSection>
</TabsContent>
<TabsContent value="documentos" className="flex-1 overflow-hidden flex flex-col m-0 pt-2 p-6 bg-white dark:bg-[#151515]">
{selectedMaintenance && (selectedMaintenance.pdf_orcamento || selectedMaintenance.nota_fiscal) && (
<Tabs defaultValue={selectedMaintenance.pdf_orcamento ? "pdf" : "nota"} className="w-full h-full flex flex-col">
<TabsList className="bg-slate-100/50 dark:bg-emerald-500/5 border border-slate-200 dark:border-emerald-500/20 w-fit p-1 h-auto mb-4 shrink-0 self-center md:self-start">
{selectedMaintenance.pdf_orcamento && (
<TabsTrigger
value="pdf"
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-6 py-2"
>
PDF Orçamento
</TabsTrigger>
)}
{selectedMaintenance.nota_fiscal && (
<TabsTrigger
value="nota"
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-6 py-2"
>
Nota Fiscal
</TabsTrigger>
)}
</TabsList>
<TabsContent value="pdf" className="flex-1 overflow-hidden m-0">
<div className="w-full h-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-2xl overflow-hidden flex flex-col shadow-sm">
<div className="p-3 border-b border-slate-200 dark:border-[#333] flex items-center justify-between bg-white dark:bg-[#1a1a1a]">
<div className="flex items-center gap-2">
<FileText size={16} className="text-emerald-500" />
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Visualização: PDF Orçamento</span>
</div>
<a href={selectedMaintenance.pdf_orcamento} target="_blank" rel="noreferrer" className="text-[10px] uppercase font-bold text-blue-500 hover:underline">Abrir em nova aba</a>
</div>
<div className="flex-1 bg-slate-100 dark:bg-[#0a0a0a]">
{isInternalFileUrl(selectedMaintenance.pdf_orcamento) ? (
<iframe src={selectedMaintenance.pdf_orcamento} title="PDF Orçamento" className="w-full h-full border-0" />
) : (
<div className="p-8 flex flex-col items-center justify-center h-full text-center space-y-4">
<div className="p-6 bg-blue-500/10 rounded-full text-blue-500"><Search size={48} /></div>
<div className="space-y-2">
<h4 className="font-bold text-slate-800 dark:text-white">Documento Externo</h4>
<p className="text-xs text-slate-500 max-w-xs mx-auto">Este arquivo está hospedado em um servidor externo e não pode ser visualizado diretamente aqui por segurança.</p>
<a
href={selectedMaintenance.pdf_orcamento}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 mt-4"
>
Acessar Documento
</a>
</div>
</div>
)}
</div>
</div>
</TabsContent>
<TabsContent value="nota" className="flex-1 overflow-hidden m-0">
<div className="w-full h-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-2xl overflow-hidden flex flex-col shadow-sm">
<div className="p-3 border-b border-slate-200 dark:border-[#333] flex items-center justify-between bg-white dark:bg-[#1a1a1a]">
<div className="flex items-center gap-2">
<FileText size={16} className="text-emerald-500" />
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Visualização: Nota Fiscal</span>
</div>
<a href={selectedMaintenance.nota_fiscal} target="_blank" rel="noreferrer" className="text-[10px] uppercase font-bold text-blue-500 hover:underline">Abrir em nova aba</a>
</div>
<div className="flex-1 bg-slate-100 dark:bg-[#0a0a0a]">
{isInternalFileUrl(selectedMaintenance.nota_fiscal) ? (
<iframe src={selectedMaintenance.nota_fiscal} title="Nota Fiscal" className="w-full h-full border-0" />
) : (
<div className="p-8 flex flex-col items-center justify-center h-full text-center space-y-4">
<div className="p-6 bg-blue-500/10 rounded-full text-blue-500"><Search size={48} /></div>
<div className="space-y-2">
<h4 className="font-bold text-slate-800 dark:text-white">Documento Externo</h4>
<p className="text-xs text-slate-500 max-w-xs mx-auto">Este arquivo está hospedado em um servidor externo e não pode ser visualizado diretamente aqui por segurança.</p>
<a
href={selectedMaintenance.nota_fiscal}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 mt-4"
>
Acessar Documento
</a>
</div>
</div>
)}
</div>
</div>
</TabsContent>
</Tabs>
)}
</TabsContent>
</Tabs>
<DialogFooter className="p-6 border-t border-slate-100 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#0f0f0f] flex flex-wrap gap-3 sm:justify-between">
<DarkButton variant="ghost" onClick={() => setIsDetailOpen(false)}>Cancelar</DarkButton>
<div className="flex gap-3">
<DarkButton variant="secondary" onClick={handleReturn} className="border-rose-500/30 text-rose-500 hover:bg-rose-500/10">
<AlertTriangle size={16} />
Retornar para Análise
</DarkButton>
<DarkButton variant="primary" onClick={handleApprove}>
<CheckCircle size={16} />
Aprovado (Validar)
</DarkButton>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -2,118 +2,111 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthContext } from '@/components/shared/AuthProvider';
import { motion } from 'framer-motion';
import { ArrowRight, Lock, Mail, Truck } from 'lucide-react';
import { ArrowRight, Lock, Mail } from 'lucide-react';
import { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
import logoOestePan from '@/assets/Img/Util/Oeste_Pan/Logo Oeste Pan.png';
export default function LoginView() {
useDocumentMetadata('Login | Prafrota', 'prafrot');
useDocumentMetadata('Login | Oeste Pan', 'oest-pan');
const [formData, setFormData] = useState({ email: '', password: '' });
const { login, loading, error } = useAuthContext();
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
// Simulate login for prafrot environment
const success = await login(formData, 'prafrot');
// Note: 'prafrot' might need to be added to authorized environments in useAuth or mocked
// Login for auth_oestepan environment
const success = await login(formData, 'auth_oestepan');
if (success) {
navigate('/plataforma/prafrot/estatisticas');
navigate('/plataforma/oest-pan/estatisticas');
}
};
return (
<div className="min-h-screen bg-[#141414] flex items-center justify-center p-4" style={{ fontFamily: 'var(--font-main)' }}>
<div className="w-full max-w-5xl h-[600px] flex shadow-2xl rounded-3xl overflow-hidden border border-[#2a2a2a]">
{/* Visual Side */}
<div className="hidden md:flex flex-1 bg-[#1c1c1c] relative items-center justify-center p-12">
<div className="absolute inset-0 bg-emerald-500/5 mix-blend-overlay" />
<div className="absolute top-0 left-0 w-full h-full bg-[url('https://images.unsplash.com/photo-1592838064575-70ed431fb924?q=80&w=2070&auto=format&fit=crop')] bg-cover bg-center opacity-10" />
<div className="relative z-10 text-center">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="w-24 h-24 bg-emerald-500 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-xl shadow-emerald-500/20"
>
<Truck size={40} className="text-[#141414]" strokeWidth={2.5} />
</motion.div>
<h1 className="text-4xl font-medium text-white tracking-tighter mb-2">PRA <span className="text-emerald-500">FROTA</span></h1>
<p className="text-slate-500 font-medium tracking-widest uppercase text-xs">Gestão Inteligente de Ativos</p>
</div>
<div className="absolute inset-0 bg-orange-500/5 mix-blend-overlay" />
<div className="absolute top-0 left-0 w-full h-full bg-[url('https://images.unsplash.com/photo-1592838064575-70ed431fb924?q=80&w=2070&auto=format&fit=crop')] bg-cover bg-center opacity-10" />
<div className="relative z-10 text-center">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex items-center justify-center mx-auto mb-6"
>
<img src={logoOestePan} alt="Oeste Pan Logo" className="w-64 h-auto drop-shadow-2xl" />
</motion.div>
<h1 className="text-4xl font-bold text-white tracking-tighter mb-2">Oeste <span className="text-orange-500">Pan</span></h1>
<p className="text-slate-500 font-medium tracking-widest uppercase text-xs">Gestão Inteligente de Ativos</p>
</div>
</div>
{/* Form Side */}
<div className="flex-1 bg-[#18181b] flex items-center justify-center p-10">
<div className="w-full max-w-sm space-y-8">
<div className="text-center md:text-left">
<h2 className="text-2xl font-medium text-white mb-2">Acesso ao Monitoramento</h2>
<p className="text-slate-400 text-sm">Entre com suas credenciais de gestor.</p>
</div>
<div className="w-full max-w-sm space-y-8">
<div className="text-center md:text-left">
<h2 className="text-2xl font-bold text-white mb-2">Acesso ao Monitoramento</h2>
<p className="text-slate-400 text-sm">Entre com suas credenciais de gestor.</p>
</div>
<form onSubmit={handleLogin} className="space-y-5">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase ml-1">Email</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={18} />
<input
type="email"
value={formData.email}
onChange={e => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-[#27272a] border border-[#3f3f46] text-slate-200 text-sm rounded-xl px-11 py-3 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-600"
placeholder="gestor@prafrota.com"
required
/>
</div>
</div>
<form onSubmit={handleLogin} className="space-y-5">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase ml-1">Email</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={18} />
<input
type="email"
value={formData.email}
onChange={e => setFormData({...formData, email: e.target.value})}
className="w-full bg-[#27272a] border border-[#3f3f46] text-slate-200 text-sm rounded-xl px-11 py-3 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-600"
placeholder="gestor@Oeste_Pan.com"
required
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase ml-1">Senha</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={18} />
<input
type="password"
value={formData.password}
onChange={e => setFormData({ ...formData, password: e.target.value })}
className="w-full bg-[#27272a] border border-[#3f3f46] text-slate-200 text-sm rounded-xl px-11 py-3 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-600"
placeholder="••••••••"
required
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase ml-1">Senha</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={18} />
<input
type="password"
value={formData.password}
onChange={e => setFormData({...formData, password: e.target.value})}
className="w-full bg-[#27272a] border border-[#3f3f46] text-slate-200 text-sm rounded-xl px-11 py-3 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-600"
placeholder="••••••••"
required
/>
</div>
</div>
{error && (
<div className="p-3 bg-red-500/10 border border-red-500/20 text-red-500 text-xs font-bold rounded-lg text-center">
{error}
</div>
)}
{error && (
<div className="p-3 bg-red-500/10 border border-red-500/20 text-red-500 text-xs font-bold rounded-lg text-center">
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-emerald-500 hover:bg-emerald-400 text-[#141414] font-bold py-3.5 rounded-xl transition-all shadow-lg shadow-emerald-500/10 flex items-center justify-center gap-2"
>
{loading ? 'Acessando...' : <>Acessar Painel <ArrowRight size={18} /></>}
</button>
<button
type="submit"
disabled={loading}
className="w-full bg-orange-500 hover:bg-orange-400 text-[#141414] font-bold py-3.5 rounded-xl transition-all shadow-lg shadow-orange-500/10 flex items-center justify-center gap-2"
>
{loading ? 'Acessando...' : <>Acessar Painel <ArrowRight size={18} /></>}
</button>
</form>
<div className="pt-4 flex flex-col items-center gap-4">
<button
type="button"
onClick={() => navigate('/plataforma/solicitacao-viagem')}
className="px-6 py-2 bg-[#27272a] hover:bg-[#3f3f46] border border-[#3f3f46] rounded-full text-[9px] font-bold text-slate-400 hover:text-emerald-400 uppercase tracking-[0.2em] transition-all"
>
<span className="flex items-center gap-2">🚀 Abrir Solicitações</span>
</button>
</div>
</form>
<div className="text-center">
<span className="text-xs text-slate-600 font-medium">© 2026 Prafrot System</span>
</div>
</div>
{/* <div className="text-center">
<span className="text-xs text-slate-600 font-medium">© 2024 Oeste Pan System v2.0</span>
</div> */}
</div>
</div>
</div>
</div>
);
}

View File

@ -3,10 +3,9 @@ import { useMaintenance } from '../hooks/useMaintenance';
import { useVehicles } from '../hooks/useVehicles';
import { useWorkshops } from '../hooks/useWorkshops';
import { useFleetLists } from '../hooks/useFleetLists';
const ItemDetailPanel = React.lazy(() => import('@/components/shared/ItemDetailPanel').then(module => ({ default: module.ItemDetailPanel })));
const SmartTable = React.lazy(() => import('@/features/dev-tools/components/SmartTable')); // Universal Connector
import AutocompleteInput from '../components/AutocompleteInput';
import { Plus, Search, Edit2, Trash2, Wrench, CheckCircle, Truck, Lock, Unlock, FileText, Database } from 'lucide-react';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search, Edit2, Trash2, Wrench, CheckCircle, Truck, Lock, Unlock, FileText } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
@ -76,21 +75,19 @@ const DarkInput = ({ label, readOnly, className = '', ...props }) => (
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
readOnly={readOnly}
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 ${readOnly ? 'cursor-not-allowed opacity-80 bg-slate-100 dark:bg-[#1a1a1a]' : ''} ${className}`}
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 ${readOnly ? 'cursor-not-allowed opacity-80 bg-slate-100 dark:bg-[#1a1a1a]' : ''} ${className}`}
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange, disabled, className = '', ...props }) => (
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)}
disabled={disabled}
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 ${disabled ? 'opacity-50 cursor-not-allowed bg-slate-100 dark:bg-[#1a1a1a]' : ''} ${className}`}
{...props}
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -101,9 +98,9 @@ const DarkSelect = ({ label, options, value, onChange, disabled, className = '',
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-semibold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
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",
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"
};
@ -126,7 +123,7 @@ const CurrencyInput = ({ label, value, onChange, ...props }) => {
<input
type="text"
inputMode="decimal"
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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
placeholder="R$ 0,00"
value={display}
onFocus={() => { setFocused(true); setLocal(isEmpty ? '' : formatCurrencyForInput(value)); }}
@ -169,7 +166,7 @@ const MaintenanceStatusCell = ({ currentStatus, id, options, onUpdate }) => {
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"
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>
@ -185,8 +182,8 @@ const MaintenanceStatusCell = ({ currentStatus, id, options, onUpdate }) => {
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' :
currentStatus === 'Aprovado' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
currentStatus === 'Concluído' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-slate-700/30 text-slate-400 border-slate-600/30'
}`}
>
@ -230,7 +227,7 @@ const isInternalFileUrl = (url) => {
const DetailSection = ({ title, children }) => (
<div className="space-y-4">
<h3 className="text-xs font-bold text-emerald-600 dark:text-emerald-500 uppercase tracking-[0.2em] border-b border-emerald-500/10 pb-2">{title}</h3>
<h3 className="text-xs font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/10 pb-2">{title}</h3>
{children}
</div>
);
@ -240,14 +237,14 @@ function FileLink({ label, url }) {
const isInternal = isInternalFileUrl(url);
return (
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4 flex flex-col gap-1 transition-all hover:border-emerald-500/30 group">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4 flex flex-col gap-1 transition-all hover:border-orange-500/30 group">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider font-mono block mb-0.5">{label}</label>
{isInternal ? (
<a
href={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600 underline font-semibold text-sm inline-flex items-center gap-1.5"
className="text-blue-500 hover:text-blue-600 underline font-bold text-sm inline-flex items-center gap-1.5"
>
<FileText size={14} className="group-hover:scale-110 transition-transform" />
Abrir documento
@ -257,7 +254,7 @@ function FileLink({ label, url }) {
href={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600 underline font-semibold text-xs break-all line-clamp-2"
className="text-blue-500 hover:text-blue-600 underline font-bold text-xs break-all line-clamp-2"
title={url}
>
{url}
@ -289,7 +286,6 @@ export default function MaintenanceView() {
fecharManutencao,
abrirManutencao,
getAbertoFechado,
abertoFechadoData,
getHistoricoCompleto,
getHistoricoDetalhado,
getHistoricoEstatisticas,
@ -301,7 +297,6 @@ export default function MaintenanceView() {
const { fetchListsConfig, statusManutencaoOptions, motivoAtendimentoOptions, responsaveisOptions, validacaoOptions, aprovacaoOptions } = useFleetLists();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('total'); // 'total', 'fechada', 'aberta'
const [viewMode, setViewMode] = useState('maintenance'); // 'maintenance', 'payment_pending'
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [statusStats, setStatusStats] = useState([]);
@ -318,13 +313,14 @@ export default function MaintenanceView() {
});
const [loadingHistorico, setLoadingHistorico] = useState(false);
const [topMaintenances, setTopMaintenances] = useState([]);
const [abertoFechadoData, setAbertoFechadoData] = useState(null);
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: '', obs_financeiro: '', obs_manutencao: '', oficina: '', orcamento_final: '', orcamento_inicial: '',
motivo_atendimento: '', obs: '', oficina: '', orcamento_final: '', orcamento_inicial: '',
pdf_orcamento: '', nota_fiscal: '', placa: '', placa_reserva: '', previsao_entrega: '', proprietario: '',
resp_aprovacao: '', responsavel: '',
status: 'Pendente', uf: '', validacao_financeiro: '', qtd_parcelas_condicao_pag: ''
@ -356,28 +352,24 @@ export default function MaintenanceView() {
}, [isModalOpen, formData.orcamento_inicial, formData.orcamento_final]);
// Carrega dados iniciais
useEffect(() => {
// Se estiver no modo manutenções, usamos o filtro de status (aberta/fechada/total)
if (viewMode === 'maintenance') {
if (statusFilter === 'total') {
fetchMaintenances('total');
} else {
getAbertoFechado();
}
// Carrega a rota inicial baseada no filtro
if (statusFilter === 'total') {
fetchMaintenances('total');
} else {
// Modo Pendências Pagamento: usa filtro nativo do backend
fetchMaintenances('Pendente Pagamento');
getAbertoFechado();
}
fetchVehicles();
fetchWorkshops();
fetchListsConfig();
// Fetch Status Stats
prafrotStatisticsService.getPlacasPorStatus().then(data => {
if (Array.isArray(data)) setStatusStats(data);
});
// Top manutenções (visão geral, cards na tela)
getHistoricoTop().then(data => {
const lista = Array.isArray(data) ? data : (data?.data || []);
const comPosicao = lista.map((item, index) => ({
@ -385,8 +377,34 @@ export default function MaintenanceView() {
posicao: index + 1,
}));
setTopMaintenances(comPosicao);
}).catch((error) => {
console.error('Erro ao carregar top manutenções:', error);
});
}, [statusFilter, viewMode]);
}, [statusFilter]); // Dependência adicionada para recarregar ao trocar o filtro
// Carrega dados de aberto/fechado quando o filtro não é "total"
useEffect(() => {
if (statusFilter === 'total') {
setAbertoFechadoData(null);
return;
}
let isCancelled = false;
(async () => {
try {
const data = await getAbertoFechado();
if (!isCancelled) {
setAbertoFechadoData(data);
}
} catch (error) {
console.error('Erro ao carregar dados de aberto/fechado:', error);
}
})();
return () => {
isCancelled = true;
};
}, [statusFilter, getAbertoFechado]);
const getStatusData = (status) => {
return statusStats.find(item => {
@ -477,11 +495,6 @@ export default function MaintenanceView() {
return;
}
if (formData.status === 'Aprovado' && (!formData.validacao_financeiro || formData.validacao_financeiro.trim() === '')) {
notifyMaintenance('warning', 'Validação Necessária', 'Necessidade da validação do financeiro');
return;
}
// Limpeza do payload: remove strings vazias que podem causar erro no backend
const payload = {};
Object.keys(formData).forEach(key => {
@ -563,50 +576,23 @@ export default function MaintenanceView() {
const [selectedIds, setSelectedIds] = useState([]);
const handleStatusUpdate = async (id, newStatus, extraPayload = null) => {
const handleStatusUpdate = async (id, newStatus) => {
setIsSubmitting(true);
try {
if (extraPayload) {
// Validação para status Aprovado
if (newStatus === 'Aprovado' && (!extraPayload.validacao_financeiro || extraPayload.validacao_financeiro.trim() === '')) {
notifyMaintenance('warning', 'Validação Necessária', 'Necessidade da validação do financeiro');
return;
}
// Se temos um payload extra (como observações), usamos a atualização padrão
// para garantir que todos os campos sejam salvos, não apenas o status
await updateMaintenance(id, { ...extraPayload, status: newStatus });
await refreshMaintenances(statusFilter);
} else if (selectedIds.includes(id)) {
// Update em massa (apenas status)
// Na atualização em massa, verificamos todos os itens selecionados
if (newStatus === 'Aprovado') {
const itemsToUpdate = maintenances.filter(m => selectedIds.includes(m.idmanutencao_frota));
const invalidItems = itemsToUpdate.filter(m => !m.validacao_financeiro || m.validacao_financeiro.trim() === '');
if (invalidItems.length > 0) {
notifyMaintenance('warning', 'Validação Necessária', `Os seguintes itens precisam de validação financeira antes de serem aprovados:\n${invalidItems.map(m => m.placa || m.idmanutencao_frota).join(', ')}\n\nNecessidade da validação do financeiro`);
return;
}
}
// 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([]);
setSelectedIds([]); // Limpa a seleção
await refreshMaintenances(statusFilter);
}
}
} else {
// Update individual simples (apenas status)
if (newStatus === 'Aprovado') {
const item = maintenances.find(m => m.idmanutencao_frota === id);
if (item && (!item.validacao_financeiro || item.validacao_financeiro.trim() === '')) {
notifyMaintenance('warning', 'Validação Necessária', 'Necessidade da validação do financeiro');
return;
}
}
// Single Update (using batch route as requested)
await updateMaintenanceBatch([id], newStatus);
await refreshMaintenances(statusFilter);
}
@ -623,10 +609,8 @@ export default function MaintenanceView() {
// - total: usa a rota padrão /manutencao_frota/apresentar
// - fechada/aberta: usa /manutencao_frota/aberto_fechado/apresentar (grupos separados)
let baseList = maintenances;
if (viewMode === 'payment_pending') {
// Safety Filter: Garante que apenas Pendente Pagamento apareça neste modo
baseList = maintenances.filter(m => (m.status || '').trim() === 'Pendente Pagamento');
} else if (statusFilter !== 'total' && abertoFechadoData) {
if (statusFilter !== 'total' && abertoFechadoData) {
const lower = {};
Object.keys(abertoFechadoData || {}).forEach((key) => {
lower[key.toLowerCase()] = abertoFechadoData[key];
@ -657,7 +641,7 @@ export default function MaintenanceView() {
);
return filtered;
}, [maintenances, searchTerm, statusFilter, abertoFechadoData, viewMode]);
}, [maintenances, searchTerm, statusFilter, abertoFechadoData]);
// Get status options for open/close
const getOpenStatus = () => {
@ -825,14 +809,14 @@ export default function MaintenanceView() {
<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-medium text-white tracking-tight">Manutenção de Frota</h1>
<h1 className="text-2xl font-bold 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 flex-wrap">
<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"
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-orange-500 w-full md:w-64"
placeholder="Buscar por ID, placa ou oficina..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -908,7 +892,7 @@ export default function MaintenanceView() {
</div>
</div>
<div className="flex items-baseline justify-between mb-3">
<span className="text-sm font-bold text-emerald-600 dark:text-emerald-400 font-mono">
<span className="text-sm font-bold text-orange-600 dark:text-orange-400 font-mono">
{item.placa || '-'}
</span>
<span className="text-[11px] text-slate-500 dark:text-slate-400">
@ -926,71 +910,36 @@ export default function MaintenanceView() {
)}
{/* Seletor de Filtro Aberto/Fechado - Destacado acima da tabela */}
<div className="bg-gradient-to-r from-emerald-500/10 via-blue-500/10 to-emerald-500/10 border-2 border-emerald-500/30 dark:border-emerald-500/20 rounded-2xl p-4 shadow-lg">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div className="bg-gradient-to-r from-orange-500/10 via-blue-500/10 to-orange-500/10 border-2 border-orange-500/30 dark:border-orange-500/20 rounded-2xl p-4 shadow-lg">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/20 rounded-xl">
<Database size={20} className="text-emerald-600 dark:text-emerald-400" />
<div className="p-2 bg-orange-500/20 rounded-xl">
<Lock size={20} className="text-orange-600 dark:text-orange-400" />
</div>
<div>
<label className="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-300 block mb-1">
Visualização de Dados
Filtrar por Status de Manutenção
</label>
<p className="text-[10px] text-slate-500 dark:text-slate-400">
Selecione o modo de visualização da tabela
Visualize todas as manutenções, apenas as abertas ou apenas as fechadas
</p>
</div>
</div>
<div className="flex items-center gap-4">
{/* Botões de Modo de Visualização */}
<div className="flex bg-slate-100 dark:bg-[#141414] p-1 rounded-xl border border-slate-200 dark:border-[#333]">
<button
onClick={() => setViewMode('maintenance')}
className={`px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${
viewMode === 'maintenance'
? 'bg-emerald-500 text-white shadow-md'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-200/50 dark:hover:bg-white/5'
}`}
>
Manutenções
</button>
<button
onClick={() => setViewMode('payment_pending')}
className={`px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${
viewMode === 'payment_pending'
? 'bg-emerald-500 text-white shadow-md'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 hover:bg-slate-200/50 dark:hover:bg-white/5'
}`}
>
Pendências Pagamento
</button>
</div>
{/* Dropdown de Status - Apenas visível no modo Manutenção */}
{viewMode === 'maintenance' && (
<div className="h-8 w-[1px] bg-slate-200 dark:bg-[#333] mx-2 hidden md:block" />
)}
{viewMode === 'maintenance' && (
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="bg-white dark:bg-[#1c1c1c] border-2 border-emerald-500/50 dark:border-emerald-500/30 text-slate-700 dark:text-slate-200 px-6 py-2.5 rounded-xl text-xs font-bold focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 cursor-pointer shadow-sm hover:shadow-md transition-all min-w-[150px]"
>
<option value="total">📊 Total</option>
<option value="aberta">🔓 Aberta</option>
<option value="fechada">🔒 Fechada</option>
</select>
)}
</div>
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="bg-white dark:bg-[#1c1c1c] border-2 border-orange-500/50 dark:border-orange-500/30 text-slate-700 dark:text-slate-200 px-6 py-3 rounded-xl text-sm font-bold focus:outline-none focus:border-orange-500 focus:ring-2 focus:ring-orange-500/20 cursor-pointer shadow-md hover:shadow-lg transition-all min-w-[180px]"
>
<option value="total">📊 Total</option>
<option value="aberta">🔓 Aberta</option>
<option value="fechada">🔒 Fechada</option>
</select>
</div>
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
{/* Usando Conector Universal (SmartTable) conforme solicitado */}
<React.Suspense fallback={<div className="flex items-center justify-center h-full"><LoadingOverlay isLoading={true} message="Carregando dados..." variant="minimal" /></div>}>
<SmartTable
{/* Colunas alinhadas ao contrato: docs/PADROES_ROTAS_APRESENTACAO.md § GET /manutencao_frota/apresentar */}
<ExcelTable
data={filteredData}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
@ -998,7 +947,7 @@ export default function MaintenanceView() {
columns={[
{ header: 'ID', field: 'idmanutencao_frota', width: '80px' },
{ header: 'ATENDIMENTO', field: 'atendimento', width: '100px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'PLACA RESERVA', field: 'placa_reserva', width: '100px' },
{ header: 'MODELO', field: 'modelo', width: '110px' },
{ header: 'OFICINA', field: 'oficina', width: '160px' },
@ -1024,14 +973,14 @@ export default function MaintenanceView() {
{ header: 'PREV. ENTREGA', field: 'previsao_entrega', width: '100px' },
{ header: 'DATA FINAL.', field: 'data_finalizacao', width: '100px' },
{ header: 'DATA RETIRADA', field: 'data_retirada', width: '100px' },
{ header: 'ORÇ. INICIAL', field: 'orcamento_inicial', width: '110px', className: 'font-mono text-emerald-600 dark:text-emerald-400', render: (row) => formatCurrency(row.orcamento_inicial) },
{ header: 'ORÇ. FINAL', field: 'orcamento_final', width: '110px', className: 'font-mono text-emerald-600 dark:text-emerald-400', render: (row) => formatCurrency(row.orcamento_final) },
{ header: 'ORÇ. INICIAL', field: 'orcamento_inicial', width: '110px', className: 'font-mono text-orange-600 dark:text-orange-400', render: (row) => formatCurrency(row.orcamento_inicial) },
{ header: 'ORÇ. FINAL', field: 'orcamento_final', width: '110px', className: 'font-mono text-orange-600 dark:text-orange-400', render: (row) => formatCurrency(row.orcamento_final) },
{ header: 'DIF. ORÇ.', field: 'dif_orcamento', width: '100px', className: 'font-mono', render: (row) => formatCurrency(row.dif_orcamento) },
{ header: 'COND. PAG.', field: 'condicao_pagamento', width: '120px', render: (row) => (
<span>
{row.condicao_pagamento}
{row.condicao_pagamento === 'Parcelado' && row.qtd_parcelas_condicao_pag && (
<span className="ml-1 text-emerald-500 font-bold">({row.qtd_parcelas_condicao_pag}x)</span>
<span className="ml-1 text-orange-500 font-bold">({row.qtd_parcelas_condicao_pag}x)</span>
)}
</span>
)},
@ -1057,7 +1006,6 @@ export default function MaintenanceView() {
// onEdit={handleOpenModal}
// onDelete={(item) => deleteMaintenance(item.idmanutencao_frota)}
/>
</React.Suspense>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
@ -1079,9 +1027,9 @@ export default function MaintenanceView() {
<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"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
>
{tab === 'basicos' ? 'Básico' : tab === 'orcamentos' ? 'Orçamentos' : 'Datas'}
{tab}
</TabsTrigger>
))}
</TabsList>
@ -1148,7 +1096,7 @@ export default function MaintenanceView() {
label="Dif. Orçamento"
readOnly
value={formData.dif_orcamento === '' || formData.dif_orcamento == null ? '' : formatCurrency(formData.dif_orcamento)}
className="font-mono text-emerald-600 dark:text-emerald-400"
className="font-mono text-orange-600 dark:text-orange-400"
/>
</div>
<div className="space-y-3">
@ -1205,9 +1153,9 @@ export default function MaintenanceView() {
}}
/>
{valorParcela != null && (
<div className="bg-emerald-500/10 dark:bg-emerald-500/20 border border-emerald-500/30 rounded-xl px-3 py-2 flex flex-col justify-end">
<div className="bg-orange-500/10 dark:bg-orange-500/20 border border-orange-500/30 rounded-xl px-3 py-2 flex flex-col justify-end">
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Valor por parcela</span>
<p className="text-sm font-bold text-emerald-700 dark:text-emerald-400 font-mono mt-0.5">{formatCurrency(valorParcela)}</p>
<p className="text-sm font-bold text-orange-700 dark:text-orange-400 font-mono mt-0.5">{formatCurrency(valorParcela)}</p>
</div>
)}
</div>
@ -1217,15 +1165,12 @@ export default function MaintenanceView() {
})()}
</div>
<div className="grid grid-cols-2 gap-4">
{editingItem && (
<DarkSelect
label="Validação Financeiro"
options={validacaoOptions}
value={formData.validacao_financeiro}
onChange={v => setFormData({...formData, validacao_financeiro: v})}
disabled={true} // Bloqueado estritamente conforme solicitado
/>
)}
<DarkSelect
label="Validação Financeiro"
options={validacaoOptions}
value={formData.validacao_financeiro}
onChange={v => setFormData({...formData, validacao_financeiro: v})}
/>
<DarkSelect
label="Resp. Aprovação"
options={aprovacaoOptions}
@ -1239,7 +1184,7 @@ export default function MaintenanceView() {
<input
type="file"
accept=".pdf,application/pdf"
className="w-full text-sm text-slate-600 dark:text-slate-300 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:bg-emerald-500/20 file:text-emerald-600 dark:file:text-emerald-400 file:font-bold file:cursor-pointer hover:file:bg-emerald-500/30"
className="w-full text-sm text-slate-600 dark:text-slate-300 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:bg-orange-500/20 file:text-orange-600 dark:file:text-orange-400 file:font-bold file:cursor-pointer hover:file:bg-orange-500/30"
onChange={(e) => setFilePdfOrcamento(e.target.files?.[0] ?? null)}
/>
{(filePdfOrcamento || formData.pdf_orcamento) && (
@ -1259,7 +1204,7 @@ export default function MaintenanceView() {
<input
type="file"
accept=".pdf,application/pdf,image/*"
className="w-full text-sm text-slate-600 dark:text-slate-300 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:bg-emerald-500/20 file:text-emerald-600 dark:file:text-emerald-400 file:font-bold file:cursor-pointer hover:file:bg-emerald-500/30"
className="w-full text-sm text-slate-600 dark:text-slate-300 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:bg-orange-500/20 file:text-orange-600 dark:file:text-orange-400 file:font-bold file:cursor-pointer hover:file:bg-orange-500/30"
onChange={(e) => setFileNotaFiscal(e.target.files?.[0] ?? null)}
/>
{(fileNotaFiscal || formData.nota_fiscal) && (
@ -1315,7 +1260,7 @@ export default function MaintenanceView() {
<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]"
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 min-h-[60px]"
value={formData.manutencao}
onChange={e => setFormData({...formData, manutencao: e.target.value})}
/>
@ -1323,55 +1268,15 @@ export default function MaintenanceView() {
<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]"
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 min-h-[60px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
</div>
{/* Campos de Observação Financeira e de Manutenção - Visíveis apenas para itens em Pendente Pagamento durante edição */}
{editingItem && formData.status === 'Pendente Pagamento' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="gap-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-400 tracking-wider ml-1">Obs. Financeiro</label>
<textarea
readOnly
className="w-full bg-amber-500/5 dark:bg-amber-500/10 border border-amber-500/20 rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none transition-all min-h-[60px]"
value={formData.obs_financeiro}
/>
</div>
<div className="gap-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-400 tracking-wider ml-1">Obs. 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.obs_manutencao}
onChange={e => setFormData({...formData, obs_manutencao: e.target.value})}
placeholder="Observações internas da manutenção..."
/>
</div>
</div>
)}
</div>
</form>
<DialogFooter className="bg-slate-50 dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
{editingItem && editingItem.status === 'Pendente Pagamento' && (
<DarkButton
type="button"
variant="secondary"
className="border-emerald-500/30 text-emerald-500 hover:bg-emerald-500/10 mr-auto"
onClick={async () => {
if (window.confirm('Deseja retornar esta manutenção para "Pendente Aprovação"?')) {
// Enviamos o formData completo para não perder a obs_manutencao
await handleStatusUpdate(editingItem.idmanutencao_frota, 'Pendente Aprovação', formData);
setIsModalOpen(false);
}
}}
>
Retornar para Aprovação
</DarkButton>
)}
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit" onClick={handleSubmit}>
{editingItem ? 'Salvar Alterações' : 'Criar Solicitação'}
@ -1387,7 +1292,7 @@ export default function MaintenanceView() {
<DialogHeader className="p-4 md:p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div className="flex items-start md:items-center gap-3 md:gap-4">
<div className="p-3 md:p-4 bg-emerald-500/10 rounded-2xl text-emerald-600 shadow-inner shrink-0">
<div className="p-3 md:p-4 bg-orange-500/10 rounded-2xl text-orange-600 shadow-inner shrink-0">
<Wrench size={24} className="md:w-7 md:h-7" />
</div>
<div className="min-w-0">
@ -1396,7 +1301,7 @@ export default function MaintenanceView() {
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-xs md:text-sm flex flex-wrap items-center gap-y-1 gap-x-2">
<span className="whitespace-nowrap">
Placa: <span className="text-emerald-500 font-bold">{selectedMaintenance?.placa}</span>
Placa: <span className="text-orange-500 font-bold">{selectedMaintenance?.placa}</span>
</span>
<span className="hidden md:inline text-slate-400">|</span>
<span className="whitespace-nowrap">
@ -1410,7 +1315,7 @@ export default function MaintenanceView() {
<span
className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${
(selectedMaintenance.manutencao || '').toLowerCase().startsWith('abert')
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30'
? 'bg-orange-500/10 text-orange-400 border border-orange-500/30'
: (selectedMaintenance.manutencao || '').toLowerCase().startsWith('fech')
? 'bg-slate-500/10 text-slate-300 border border-slate-500/40'
: 'bg-slate-700/20 text-slate-300 border border-slate-600/40'
@ -1437,6 +1342,17 @@ export default function MaintenanceView() {
return <><Lock size={14} /> Fechar Manutenção</>;
})()}
</DarkButton>
<DarkButton
variant="secondary"
onClick={() => {
handleOpenModal(selectedMaintenance);
setIsDetailPanelOpen(false);
}}
className="flex items-center gap-2 h-9 text-xs md:text-sm px-3"
>
<Edit2 size={14} />
Editar
</DarkButton>
<DarkButton
variant="secondary"
onClick={() => {
@ -1450,17 +1366,6 @@ export default function MaintenanceView() {
<Trash2 size={14} />
Excluir
</DarkButton>
<DarkButton
variant="secondary"
onClick={() => {
handleOpenModal(selectedMaintenance);
setIsDetailPanelOpen(false);
}}
className="flex items-center gap-2 h-9 text-xs md:text-sm px-3"
>
<Edit2 size={14} />
Editar
</DarkButton>
</div>
</div>
</DialogHeader>
@ -1468,26 +1373,26 @@ export default function MaintenanceView() {
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
<Tabs defaultValue="detalhes" className="w-full h-full flex flex-col">
<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 shrink-0">
<TabsTrigger value="detalhes" 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">
<TabsTrigger value="detalhes" className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2">
Detalhes
</TabsTrigger>
<TabsTrigger
value="completo"
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"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
onClick={() => handleLoadHistorico('completo')}
>
Histórico Completo
</TabsTrigger>
<TabsTrigger
value="detalhado"
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"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
onClick={() => handleLoadHistorico('detalhado')}
>
Histórico Detalhado
</TabsTrigger>
<TabsTrigger
value="estatisticas"
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"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
onClick={() => handleLoadHistorico('estatisticas')}
>
Estatísticas
@ -1495,7 +1400,7 @@ export default function MaintenanceView() {
{(selectedMaintenance?.pdf_orcamento || selectedMaintenance?.nota_fiscal) && (
<TabsTrigger
value="documentos"
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 flex items-center gap-1.5"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2 flex items-center gap-1.5"
>
<FileText size={14} />
Documentos
@ -1518,7 +1423,7 @@ export default function MaintenanceView() {
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Placa</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{selectedMaintenance.placa ?? '-'}</p>
<p className="text-sm font-bold text-orange-600 dark:text-orange-500 mt-1 font-mono">{selectedMaintenance.placa ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Placa Reserva</label>
@ -1602,7 +1507,7 @@ export default function MaintenanceView() {
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">
{selectedMaintenance.condicao_pagamento ?? '-'}
{selectedMaintenance.condicao_pagamento === 'Parcelado' && selectedMaintenance.qtd_parcelas_condicao_pag && (
<span className="ml-1 text-emerald-500">({selectedMaintenance.qtd_parcelas_condicao_pag}x)</span>
<span className="ml-1 text-orange-500">({selectedMaintenance.qtd_parcelas_condicao_pag}x)</span>
)}
</p>
</div>
@ -1613,20 +1518,20 @@ export default function MaintenanceView() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Orçamento Inicial</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{formatCurrency(selectedMaintenance.orcamento_inicial)}</p>
<p className="text-sm font-bold text-orange-600 dark:text-orange-500 mt-1 font-mono">{formatCurrency(selectedMaintenance.orcamento_inicial)}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Orçamento Final</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{formatCurrency(selectedMaintenance.orcamento_final)}</p>
<p className="text-sm font-bold text-orange-600 dark:text-orange-500 mt-1 font-mono">{formatCurrency(selectedMaintenance.orcamento_final)}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Dif. Orçamento</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-500 mt-1 font-mono">{formatCurrency(selectedMaintenance.dif_orcamento)}</p>
<p className="text-sm font-bold text-orange-600 dark:text-orange-500 mt-1 font-mono">{formatCurrency(selectedMaintenance.dif_orcamento)}</p>
</div>
{selectedMaintenance.condicao_pagamento === 'Parcelado' && selectedMaintenance.qtd_parcelas_condicao_pag && (
<div className="bg-emerald-500/5 dark:bg-emerald-500/10 border border-emerald-500/20 rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-emerald-600 dark:text-emerald-400 tracking-wider">Valor da Parcela</label>
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-400 mt-1 font-mono">
<div className="bg-orange-500/5 dark:bg-orange-500/10 border border-orange-500/20 rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-orange-600 dark:text-orange-400 tracking-wider">Valor da Parcela</label>
<p className="text-sm font-bold text-orange-600 dark:text-orange-400 mt-1 font-mono">
{(() => {
const total = parseCurrency(selectedMaintenance.orcamento_final);
const parcelas = parseInt(selectedMaintenance.qtd_parcelas_condicao_pag, 10);
@ -1693,14 +1598,6 @@ export default function MaintenanceView() {
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Observações</label>
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance.obs ?? '-'}</p>
</div>
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Observação de Manutenção</label>
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance.obs_manutencao ?? '-'}</p>
</div>
<div className="bg-amber-500/5 dark:bg-amber-500/10 border border-amber-500/20 rounded-xl p-4">
<label className="text-[10px] uppercase font-bold text-amber-600 dark:text-amber-400 tracking-wider">Observação Financeira</label>
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance.obs_financeiro ?? '-'}</p>
</div>
</div>
</DetailSection>
</div>
@ -1723,9 +1620,9 @@ export default function MaintenanceView() {
{historicoData.completo.total_manutencoes ?? 0}
</p>
</div>
<div className="bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-xl p-4">
<span className="text-[10px] uppercase font-bold text-emerald-700 dark:text-emerald-300 tracking-wider">Concluídas</span>
<p className="text-2xl font-bold text-emerald-700 dark:text-emerald-300 mt-1">
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-xl p-4">
<span className="text-[10px] uppercase font-bold text-orange-700 dark:text-orange-300 tracking-wider">Concluídas</span>
<p className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
{historicoData.completo.manutencoes_concluidas ?? 0}
</p>
</div>
@ -1781,11 +1678,11 @@ export default function MaintenanceView() {
) : (
<div className="space-y-4">
{historicoData.detalhado.length > 0 ? (
<SmartTable
<ExcelTable
data={historicoData.detalhado}
columns={[
{ header: 'ID Manutenção', field: 'idmanutencao_frota', width: '120px' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '140px' },
{ header: 'Oficina', field: 'oficina', width: '180px' },
{ header: 'Motivo', field: 'motivo_atendimento', width: '140px' },
@ -1847,7 +1744,7 @@ export default function MaintenanceView() {
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">
Valor Total Gasto
</span>
<p className="text-2xl font-bold text-emerald-700 dark:text-emerald-300 mt-1">
<p className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
{formatCurrency(historicoData.estatisticas.valor_total_gasto)}
</p>
</div>
@ -1858,7 +1755,7 @@ export default function MaintenanceView() {
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">
Valor Médio Manutenção
</span>
<p className="text-sm font-bold text-emerald-700 dark:text-emerald-300 mt-1">
<p className="text-sm font-bold text-orange-700 dark:text-orange-300 mt-1">
{formatCurrency(historicoData.estatisticas.valor_medio_manutencao)}
</p>
</div>
@ -1890,11 +1787,11 @@ export default function MaintenanceView() {
<TabsContent value="documentos" className="flex-1 overflow-hidden flex flex-col m-0 pt-2">
{selectedMaintenance && (selectedMaintenance.pdf_orcamento || selectedMaintenance.nota_fiscal) && (
<Tabs defaultValue={selectedMaintenance.pdf_orcamento ? "pdf" : "nota"} className="w-full h-full flex flex-col">
<TabsList className="bg-slate-100/50 dark:bg-emerald-500/5 border border-slate-200 dark:border-emerald-500/20 w-fit p-1 h-auto mb-4 shrink-0 self-center md:self-start">
<TabsList className="bg-slate-100/50 dark:bg-orange-500/5 border border-slate-200 dark:border-orange-500/20 w-fit p-1 h-auto mb-4 shrink-0 self-center md:self-start">
{selectedMaintenance.pdf_orcamento && (
<TabsTrigger
value="pdf"
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-6 py-2"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-6 py-2"
>
PDF Orçamento
</TabsTrigger>
@ -1902,7 +1799,7 @@ export default function MaintenanceView() {
{selectedMaintenance.nota_fiscal && (
<TabsTrigger
value="nota"
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-6 py-2"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-6 py-2"
>
Nota Fiscal
</TabsTrigger>
@ -1913,7 +1810,7 @@ export default function MaintenanceView() {
<div className="w-full h-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-2xl overflow-hidden flex flex-col shadow-sm">
<div className="p-3 border-b border-slate-200 dark:border-[#333] flex items-center justify-between bg-white dark:bg-[#1a1a1a]">
<div className="flex items-center gap-2">
<FileText size={16} className="text-emerald-500" />
<FileText size={16} className="text-orange-500" />
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Visualização: PDF Orçamento</span>
</div>
<a href={selectedMaintenance.pdf_orcamento} target="_blank" rel="noreferrer" className="text-[10px] uppercase font-bold text-blue-500 hover:underline">Abrir em nova aba</a>
@ -1941,7 +1838,7 @@ export default function MaintenanceView() {
<div className="w-full h-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-2xl overflow-hidden flex flex-col shadow-sm">
<div className="p-3 border-b border-slate-200 dark:border-[#333] flex items-center justify-between bg-white dark:bg-[#1a1a1a]">
<div className="flex items-center gap-2">
<FileText size={16} className="text-emerald-500" />
<FileText size={16} className="text-orange-500" />
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Visualização: Nota Fiscal</span>
</div>
<a href={selectedMaintenance.nota_fiscal} target="_blank" rel="noreferrer" className="text-[10px] uppercase font-bold text-blue-500 hover:underline">Abrir em nova aba</a>
@ -1992,7 +1889,7 @@ export default function MaintenanceView() {
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>.
Listagem técnica dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-orange-500 font-bold">"{selectedStatusRecords?.title}"</span>.
</DialogDescription>
</div>
</div>
@ -2000,10 +1897,10 @@ export default function MaintenanceView() {
<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">
<SmartTable
<ExcelTable
data={selectedStatusRecords?.records || []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
@ -2034,3 +1931,6 @@ export default function MaintenanceView() {
</div>
);
}

View File

@ -1,462 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Mail, Calendar, Clock, Upload, CircleAlert, CircleCheckBig, Truck, LayoutDashboard, Key, ShieldCheck, FileSearch, Trash2, Loader2, Settings, X, Package, DollarSign, Copy, Store, User, Activity, Columns, RefreshCcw } from 'lucide-react';
import * as prafrotService from '../services/prafrotService';
import { useAuthContext } from '@/components/shared/AuthProvider';
import { toast } from 'sonner';
import { NfeDataDisplay } from '../components/NfeDataDisplay';
const MessagesView = () => {
const { userData } = useAuthContext();
const user = userData?.usuario || { username: '', email: '' };
const [dataCorte, setDataCorte] = useState(new Date().toISOString().split('T')[0]);
const [assuntoFiltro, setAssuntoFiltro] = useState('NFe');
const [isProcessing, setIsProcessing] = useState(false);
const [results, setResults] = useState(null);
// Email Credentials State
const [emailCred, setEmailCred] = useState(user.email || '');
const [passwordCred, setPasswordCred] = useState('');
const [isSavingCreds, setIsSavingCreds] = useState(false);
// XML Validation State
const [isValidating, setIsValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [activeTab, setActiveTab] = useState('manual'); // 'manual' | 'auto'
const [nfes, setNfes] = useState([]);
const [loadingNfes, setLoadingNfes] = useState(false);
const [selectedNfeDetail, setSelectedNfeDetail] = useState(null);
useEffect(() => {
if (activeTab === 'auto') {
fetchNfes();
}
}, [activeTab]);
const fetchNfes = async () => {
setLoadingNfes(true);
try {
const data = await prafrotService.getNfes();
const list = Array.isArray(data) ? data : (data.results || []);
setNfes(list);
} catch (error) {
toast.error("Erro ao carregar NFes");
} finally {
setLoadingNfes(false);
}
};
const handleSaveCredentials = async () => {
if (!emailCred || !passwordCred) {
toast.error("Preencha e-mail e senha");
return;
}
setIsSavingCreds(true);
try {
await prafrotService.saveEmailCredentials(emailCred, passwordCred);
toast.success("Credenciais salvas com sucesso");
setPasswordCred('');
} catch (error) {
toast.error(error.message || "Erro ao salvar credenciais");
} finally {
setIsSavingCreds(false);
}
};
const handleProcessEmails = async () => {
setIsProcessing(true);
setResults(null);
try {
const resp = await prafrotService.processEmails({
desde_data: dataCorte,
assunto_filtro: assuntoFiltro,
salvar_xmls: true
});
setResults(resp);
toast.success(`${resp.total_xmls} XMLs processados com sucesso`);
} catch (error) {
toast.error(error.message || "Erro ao processar e-mails");
} finally {
setIsProcessing(false);
}
};
const handleValidateXML = async (e) => {
const file = e.target.files[0];
if (!file) return;
setIsValidating(true);
setValidationResult(null);
try {
const resp = await prafrotService.validateXML(file);
setValidationResult(resp);
if (resp.valido) {
toast.success("XML válido!");
} else {
toast.error("XML inválido para o sistema");
}
} catch (error) {
toast.error(error.message || "Erro ao validar XML");
} finally {
setIsValidating(false);
}
};
return (
<div className="min-h-screen text-white p-4 md:p-8 animate-in fade-in duration-500 overflow-hidden flex flex-col gap-6">
{/* Header - Fixed Height Area */}
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-emerald-500/10 rounded-2xl flex items-center justify-center border border-emerald-500/20 shadow-lg shadow-emerald-500/5">
<Mail className="text-emerald-500" size={24} />
</div>
<div>
<h1 className="text-2xl font-black tracking-tight uppercase">
Processador de <span className="text-emerald-500">XML</span>
</h1>
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em]">
Ambiente Seguro de Processamento
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex p-1 bg-white/5 rounded-xl border border-white/5 shadow-inner">
<button
onClick={() => setActiveTab('manual')}
className={`px-6 py-2 text-[10px] font-bold uppercase tracking-widest rounded-lg transition-all ${
activeTab === 'manual'
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Manual
</button>
<button
onClick={() => setActiveTab('auto')}
className={`px-6 py-2 text-[10px] font-bold uppercase tracking-widest rounded-lg transition-all ${
activeTab === 'auto'
? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20'
: 'text-slate-400 hover:text-slate-200'
}`}
>
Automático
</button>
</div>
{/* <button className="p-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl transition-all text-slate-400">
<Settings size={18} />
</button> */}
</div>
</header>
{/* Main Content - Fluid Area */}
<main className="flex-1 flex gap-6 min-h-0 relative">
{/* Main List/Controls Section */}
<div className={`flex-1 flex flex-col gap-6 transition-all duration-500 ${selectedNfeDetail ? 'hidden lg:flex' : 'flex'}`}>
{activeTab === 'manual' ? (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full">
{/* Left Column: Controls */}
<div className="lg:col-span-4 space-y-6">
<div className="bg-[#141414] rounded-[2rem] border border-white/5 p-8 space-y-6 shadow-2xl">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Data de Corte</label>
<div className="relative group">
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-emerald-500 transition-colors" size={16} />
<input
type="date"
value={dataCorte}
onChange={(e) => setDataCorte(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-black/40 border border-white/5 rounded-2xl text-xs font-bold focus:outline-none focus:border-emerald-500 focus:ring-4 focus:ring-emerald-500/5 transition-all"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Filtro de Conteúdo</label>
<div className="relative group">
<FileSearch className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-emerald-500 transition-colors" size={16} />
<input
type="text"
value={assuntoFiltro}
onChange={(e) => setAssuntoFiltro(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-black/40 border border-white/5 rounded-2xl text-xs font-bold focus:outline-none focus:border-emerald-500 focus:ring-4 focus:ring-emerald-500/5 transition-all"
placeholder="Ex: NFe, Fatura..."
/>
</div>
</div>
</div>
<button
onClick={handleProcessEmails}
disabled={isProcessing}
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-emerald-500 hover:bg-emerald-600 text-white rounded-2xl font-black text-[11px] uppercase tracking-[0.2em] transition-all shadow-xl shadow-emerald-500/20 disabled:opacity-50 hover:scale-[1.02] active:scale-[0.98]"
>
{isProcessing ? <Loader2 className="animate-spin" size={20} /> : <Upload size={20} />}
Sincronizar Mensagens
</button>
{/* <div className="h-px bg-white/5" />
<div className="space-y-4">
<h3 className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Configuração de Credenciais</h3>
<div className="space-y-3">
<div className="relative group">
<User className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
type="email"
value={emailCred}
onChange={(e) => setEmailCred(e.target.value)}
placeholder="E-mail"
className="w-full pl-12 pr-4 py-3.5 bg-black/40 border border-white/5 rounded-2xl text-xs font-bold focus:outline-none focus:border-emerald-500 transition-all"
/>
</div>
<div className="relative group">
<Key className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
type="password"
value={passwordCred}
onChange={(e) => setPasswordCred(e.target.value)}
placeholder="Senha"
className="w-full pl-12 pr-4 py-3.5 bg-black/40 border border-white/5 rounded-2xl text-xs font-bold focus:outline-none focus:border-emerald-500 transition-all"
/>
</div>
<button
onClick={handleSaveCredentials}
disabled={isSavingCreds}
className="w-full py-3 bg-white/5 hover:bg-white/10 text-slate-300 rounded-2xl font-bold text-[10px] uppercase tracking-widest border border-white/10 transition-all active:scale-[0.98]"
>
{isSavingCreds ? <Loader2 className="animate-spin mx-auto" size={16} /> : "Atualizar Acesso"}
</button>
</div>
</div> */}
</div>
</div>
{/* Right Column: Execution History */}
<div className="lg:col-span-8 flex flex-col gap-6">
<div className="flex-1 bg-[#141414] rounded-[2rem] border border-white/5 flex flex-col shadow-2xl overflow-hidden">
<header className="p-8 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-lg">
<Activity className="text-emerald-500" size={16} />
</div>
<h3 className="text-[11px] font-black uppercase tracking-[0.2em]">Fluxo de Dados em Tempo Real</h3>
</div>
{results && (
<span className="text-[9px] font-black bg-emerald-500/10 text-emerald-500 px-3 py-1 rounded-full border border-emerald-500/20 uppercase tracking-widest">
Processamento Concluído
</span>
)}
</header>
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
{isProcessing ? (
<div className="h-full flex flex-col items-center justify-center space-y-4 opacity-40">
<Loader2 className="animate-spin text-emerald-500" size={48} />
<p className="text-sm font-black text-slate-500 uppercase tracking-[0.3em]">Varrendo Servidores de E-mail...</p>
</div>
) : results ? (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="grid grid-cols-1 gap-4">
<div className="bg-black/40 p-6 rounded-[1.5rem] border border-white/5">
<span className="text-[9px] font-bold text-slate-500 uppercase block mb-2 tracking-widest">Total XMLs</span>
<span className="text-4xl font-black text-emerald-500 tracking-tighter">{results.total_xmls}</span>
</div>
{/* <div className="md:col-span-2 bg-black/40 p-6 rounded-[1.5rem] border border-white/5 relative overflow-hidden">
<span className="text-[9px] font-bold text-slate-500 uppercase block mb-2 tracking-widest">Conta Processada</span>
<span className="text-lg font-bold text-white tracking-tight">{results.email_processado}</span>
<ShieldCheck className="absolute right-[-10px] bottom-[-10px] text-emerald-500/10" size={80} />
</div> */}
</div>
<div className="space-y-3">
<h4 className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Arquivos Extraídos</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-[400px] overflow-y-auto custom-scrollbar pr-2">
{results.resultados.map((res, i) => (
<div key={i} className="flex items-center justify-between bg-black/40 p-4 rounded-2xl border border-white/5 group hover:border-emerald-500/30 transition-all duration-300">
<div className="flex items-center gap-3 min-w-0">
<div className="p-2 bg-white/5 rounded-lg group-hover:bg-emerald-500/10 transition-colors">
<Package size={14} className="text-slate-500 group-hover:text-emerald-500 transition-colors" />
</div>
<span className="text-[11px] font-bold text-slate-400 group-hover:text-white transition-colors truncate">{res.arquivo}</span>
</div>
<CircleCheckBig size={16} className="text-emerald-500 opacity-40 group-hover:opacity-100 transition-all" />
</div>
))}
</div>
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center space-y-4 opacity-20">
<Mail size={64} className="text-slate-600" />
<div className="space-y-1">
<p className="text-sm font-black uppercase tracking-[0.2em]">Aguardando Comando</p>
<p className="text-[10px] font-bold text-slate-500 uppercase">Inicie a sincronização para buscar novos XMLs no servidor.</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
) : (
<div className="bg-[#141414] rounded-[2rem] border border-white/5 flex-1 flex flex-col shadow-2xl overflow-hidden animate-in slide-in-from-right-4 duration-500">
<header className="p-8 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-lg">
<Columns size={16} className="text-emerald-500" />
</div>
<h3 className="text-[11px] font-black uppercase tracking-[0.2em]">Repositório de Documentos Processados</h3>
</div>
<div className="flex items-center gap-2">
<button onClick={fetchNfes} className="p-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-slate-400 transition-all">
<RefreshCcw size={16} className={loadingNfes ? 'animate-spin' : ''} />
</button>
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest bg-black/40 px-4 py-2 rounded-xl border border-white/5">
{nfes.length} Registros Encontrados
</span>
</div>
</header>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{loadingNfes ? (
<div className="h-full flex flex-col items-center justify-center py-12 gap-4">
<Loader2 className="animate-spin text-emerald-500" size={40} />
<span className="text-[10px] font-black text-slate-500 uppercase tracking-[0.3em]">Carregando Repositório...</span>
</div>
) : (
<div className="p-8">
<table className="w-full text-left border-separate border-spacing-y-3">
<thead>
<tr>
<th className="px-6 py-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Identificador</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Emitente / Origem</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Valor Fiscal</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-500 uppercase tracking-widest">Data Emissão</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-500 uppercase tracking-widest text-right">Ações</th>
</tr>
</thead>
<tbody>
{nfes.map(item => {
const nfeData = item.nfe || item;
const nfeId = item.id || nfeData.id;
const isSelected = selectedNfeDetail?.id === nfeId;
return (
<tr
key={nfeId}
onClick={() => setSelectedNfeDetail(item)}
className={`group cursor-pointer transition-all duration-300 ${isSelected ? 'scale-[0.99]' : 'hover:scale-[1.01]'}`}
>
<td className={`px-6 py-5 bg-black/40 rounded-l-[1.5rem] border-y border-l transition-all duration-300 ${isSelected ? 'bg-emerald-500/10 border-emerald-500/20' : 'border-white/5 hover:border-emerald-500/30'}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg transition-colors ${isSelected ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20' : 'bg-white/5 group-hover:bg-emerald-500/10 group-hover:text-emerald-500 text-slate-500'}`}>
<FileSearch size={16} />
</div>
<span className="text-xs font-black tracking-tight text-white">#{nfeId}</span>
</div>
</td>
<td className={`px-6 py-5 bg-black/40 border-y transition-all duration-300 ${isSelected ? 'bg-emerald-500/10 border-emerald-500/20' : 'border-white/5 group-hover:border-emerald-500/30'}`}>
<div className="flex flex-col">
<span className="text-[11px] font-bold text-white uppercase group-hover:text-emerald-400 transition-colors">{nfeData.emitente_nome || '---'}</span>
<span className="text-[9px] font-bold text-slate-500 font-mono mt-0.5">{nfeData.emitente_cnpj || '---'}</span>
</div>
</td>
<td className={`px-6 py-5 bg-black/40 border-y transition-all duration-300 ${isSelected ? 'bg-emerald-500/10 border-emerald-500/20' : 'border-white/5 group-hover:border-emerald-500/30'}`}>
<span className="text-sm font-black text-emerald-500 font-mono tracking-tighter">
R$ {parseFloat(nfeData.valor_total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</span>
</td>
<td className={`px-6 py-5 bg-black/40 border-y transition-all duration-300 ${isSelected ? 'bg-emerald-500/10 border-emerald-500/20' : 'border-white/5 group-hover:border-emerald-500/30'}`}>
<div className="flex items-center gap-2 text-[11px] font-bold text-slate-400 group-hover:text-slate-200 transition-colors">
<Calendar size={12} className="opacity-40" />
{nfeData.data_emissao ? new Date(nfeData.data_emissao).toLocaleDateString() : '---'}
</div>
</td>
<td className={`px-6 py-5 bg-black/40 rounded-r-[1.5rem] border-y border-r text-right transition-all duration-300 ${isSelected ? 'bg-emerald-500/10 border-emerald-500/20' : 'border-white/5 group-hover:border-emerald-500/30'}`}>
<button
className={`p-2.5 rounded-xl transition-all ${isSelected ? 'bg-emerald-500 text-white' : 'bg-white/5 group-hover:bg-emerald-500 text-white shadow-lg'}`}
>
<LayoutDashboard size={16} />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
{nfes.length === 0 && !loadingNfes && (
<div className="py-20 flex flex-col items-center justify-center space-y-4 opacity-20">
<CircleAlert size={48} />
<span className="text-[10px] font-black uppercase tracking-[0.2em]">Nenhuma NFe processada encontrada</span>
</div>
)}
</div>
)}
</div>
</div>
)}
</div>
{/* Integrated Side Panel - The "Power-Up" */}
<div className={`fixed inset-y-0 right-0 w-full lg:w-[60vw] xl:w-[50vw] bg-[#141416] border-l border-white/10 shadow-[-20px_0_50px_rgba(0,0,0,0.5)] transform transition-transform duration-200 ease-out z-40 flex flex-col will-change-transform ${selectedNfeDetail ? 'translate-x-0' : 'translate-x-full'}`}>
{selectedNfeDetail && (
<>
<header className="p-8 border-b border-white/5 flex items-center justify-between bg-black/20">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-emerald-500 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-emerald-500/20">
<FileSearch size={24} />
</div>
<div>
<h3 className="text-xl font-black tracking-tight uppercase">Detalhes do <span className="text-emerald-500">Documento</span></h3>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Protocolo Fiscal:</span>
<span className="text-[10px] font-mono text-emerald-500/80 font-bold bg-emerald-500/5 px-2 py-0.5 rounded border border-emerald-500/10">
{selectedNfeDetail.nfe?.numero || selectedNfeDetail.id}
</span>
</div>
</div>
</div>
<button
onClick={() => setSelectedNfeDetail(null)}
className="p-3 bg-white/5 hover:bg-rose-500 hover:text-white border border-white/10 rounded-2xl transition-all group"
>
<X size={20} className="transition-transform group-hover:rotate-90 duration-300" />
</button>
</header>
<div className="flex-1 overflow-hidden relative">
<NfeDataDisplay data={selectedNfeDetail} />
</div>
<footer className="p-6 bg-black/40 border-t border-white/5 flex justify-end gap-3">
<button className="flex items-center gap-2 px-6 py-3 bg-white/5 hover:bg-white/10 text-slate-300 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border border-white/10">
<Copy size={16} /> Copiar Chave
</button>
<button
onClick={() => setSelectedNfeDetail(null)}
className="px-8 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-lg shadow-emerald-500/20"
>
Fechar Visualização
</button>
</footer>
</>
)}
</div>
</main>
<footer className="flex items-center justify-between opacity-40 hover:opacity-100 transition-opacity duration-500 px-4">
{/* <div className="flex items-center gap-2">
<ShieldCheck size={14} className="text-emerald-500" />
<span className="text-[9px] font-black text-slate-500 uppercase tracking-[0.3em]">Criptografia iT Guys Standard (256-bit AES)</span>
</div> */}
{/* <div className="text-[9px] font-black text-slate-500 uppercase tracking-[0.3em]">Ambiente Platform Sistemas v4.0</div> */}
</footer>
</div>
);
};
export default MessagesView;

View File

@ -11,7 +11,7 @@ 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-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
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>
@ -23,7 +23,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
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 => (
@ -36,7 +36,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
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"
};
@ -107,14 +107,14 @@ export default function MokiView() {
<div className="space-y-6 min-w-0 overflow-hidden">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Checklists Moki</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Checklists Moki</h1>
<p className="text-slate-500 text-sm">Inspeções e vistorias realizadas.</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="w-full md:w-64 bg-white dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-10 pr-4 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"
className="w-full md:w-64 bg-white dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-10 pr-4 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"
placeholder="Buscar checklist..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -132,12 +132,12 @@ export default function MokiView() {
columns={[
{ header: 'ID', field: 'idmoki_frota', width: '80px' },
{ header: 'DATA', field: 'data', width: '100px', render: (row) => row.data?.split('T')[0] || row.data_checklist?.split('T')[0] },
{ header: 'CHECKLIST', field: 'checklist', width: '220px', className: 'font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'CHECKLIST', field: 'checklist', width: '220px', className: 'font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS CHECKLIST', field: 'status_checklist', width: '150px' },
{ header: 'STATUS', field: 'status', width: '120px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border ${
row.status === 'Não Conforme' ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' :
row.status === 'Aprovado' || row.status === 'Conforme' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
row.status === 'Aprovado' || row.status === 'Conforme' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-amber-500/10 text-amber-500 border-amber-500/20'
}`}>
{row.status || 'Pendente'}
@ -175,7 +175,7 @@ export default function MokiView() {
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-4 space-y-4 max-h-[70vh] overflow-y-auto custom-scrollbar">
<div className="bg-emerald-500/5 p-4 rounded-xl border border-emerald-500/10 mb-2">
<div className="bg-orange-500/5 p-4 rounded-xl border border-orange-500/10 mb-2">
<DarkInput
type="number"
label="ID do Moki (Obrigatório)"
@ -184,7 +184,7 @@ export default function MokiView() {
required
placeholder="Ex: 123456"
/>
<p className="text-[10px] text-emerald-500/60 mt-1 ml-1 uppercase font-bold tracking-widest">Este campo deve ser preenchido manualmente para novos registros.</p>
<p className="text-[10px] text-orange-500/60 mt-1 ml-1 uppercase font-bold tracking-widest">Este campo deve ser preenchido manualmente para novos registros.</p>
</div>
<div className="grid grid-cols-2 gap-4">
@ -219,3 +219,6 @@ export default function MokiView() {
</div>
);
}

View File

@ -14,7 +14,7 @@ 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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -26,7 +26,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -39,7 +39,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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",
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"
};
@ -115,7 +115,17 @@ export default function MonitoringView() {
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...initialFormState, ...item });
// Format date properly for HTML date input (YYYY-MM-DD)
const formattedItem = { ...item };
if (formattedItem.data_carga) {
// Handle various date formats from backend
const date = new Date(formattedItem.data_carga);
if (!isNaN(date.getTime())) {
// Format to YYYY-MM-DD for HTML date input
formattedItem.data_carga = date.toISOString().split('T')[0];
}
}
setFormData({ ...initialFormState, ...formattedItem });
} else {
setEditingItem(null);
setFormData(initialFormState);
@ -143,14 +153,14 @@ export default function MonitoringView() {
<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-medium text-white tracking-tight">Monitoramento & Logística</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Monitoramento & Logística</h1>
<p className="text-slate-500 text-sm">Acompanhamento em tempo real e rastreabilidade.</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"
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-orange-500 w-full md:w-64"
placeholder="Buscar unidade..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -165,7 +175,7 @@ export default function MonitoringView() {
{/* Status Highlights */}
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Em Operação', status: 'Em Operação', color: 'bg-emerald-500/10 text-emerald-600', icon: <CheckCircle size={20} />, hover: 'hover:border-emerald-500/30' },
{ label: 'Em Operação', status: 'Em Operação', color: 'bg-orange-500/10 text-orange-600', icon: <CheckCircle size={20} />, hover: 'hover:border-orange-500/30' },
{ label: 'Veículo Alugado', status: 'Veículo Alugado', color: 'bg-blue-500/10 text-blue-600', icon: <Truck size={20} />, hover: 'hover:border-blue-500/30' },
].map((card, i) => (
<div
@ -195,7 +205,7 @@ export default function MonitoringView() {
columns={[
{ header: 'ID', field: 'idmonitoramento_frota', width: '80px' },
{ header: 'ID EXTERNO', field: 'id_externo', width: '120px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'UNIDADE', field: 'unidade', width: '150px' },
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
{ header: 'DATA CARGA', field: 'data_carga', width: '150px' },
@ -246,7 +256,7 @@ export default function MonitoringView() {
valueKey="NOME_FAVORECIDO"
placeholder="Buscar motorista..."
/>
<DarkInput type="date" label="Data Carga" value={formData.data_carga?.split('T')[0]} onChange={e => setFormData({...formData, data_carga: e.target.value})} />
<DarkInput type="date" label="Data Carga" value={formData.data_carga || ''} onChange={e => setFormData({...formData, data_carga: e.target.value})} />
</div>
<div className="grid grid-cols-1 gap-4 pb-4">
<DarkInput label="ID Externo" value={formData.id_externo} onChange={e => setFormData({...formData, id_externo: e.target.value})} />
@ -255,7 +265,7 @@ export default function MonitoringView() {
<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 de Monitoramento</label>
<textarea
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 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[100px]"
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-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[100px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
@ -274,7 +284,7 @@ export default function MonitoringView() {
<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-emerald-500/10 rounded-2xl text-emerald-600 shadow-inner">
<div className="p-4 bg-orange-500/10 rounded-2xl text-orange-600 shadow-inner">
<Truck size={28} />
</div>
<div>
@ -282,7 +292,7 @@ export default function MonitoringView() {
Veículos: {selectedStatusRecords?.title}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Monitoramento detalhado dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-emerald-500 font-bold">"{selectedStatusRecords?.title}"</span>.
Monitoramento detalhado dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-orange-500 font-bold">"{selectedStatusRecords?.title}"</span>.
</DialogDescription>
</div>
</div>
@ -293,7 +303,7 @@ export default function MonitoringView() {
<ExcelTable
data={selectedStatusRecords?.records || []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
@ -324,3 +334,6 @@ export default function MonitoringView() {
</div>
);
}

View File

@ -1,676 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Search, Plus, X, Loader2, Activity, Filter, RefreshCcw, Calendar, Trash2, Eye, User, Truck, Clock, ShieldCheck, MapPin, LayoutGrid, Table2, Columns, Archive, FileText, Package, CheckCircle2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { getTripRequests, updateTripStatus, deleteTripRequest, closeTripRequest, archiveTripRequest } from '../services/prafrotService';
import { TripDetailsModal } from '../components/TripDetailsModal';
import { OperationsManagerModal } from '../components/OperationsManagerModal';
import ExcelTable from '../components/ExcelTable';
import { GrKanbanBoard } from '../../gr/components/GrKanbanBoard';
import { toast } from 'sonner';
import { AnalysisPanel } from '../components/AnalysisPanel';
import { NfeViewerModal } from '../components/NfeViewerModal';
import { AttendanceFormModal } from '../components/AttendanceFormModal';
const formatCPF = (cpf) => {
if (!cpf) return '---';
const cleanCPF = String(cpf).replace(/\D/g, '');
if (cleanCPF.length !== 11) return cpf;
return cleanCPF.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
};
const ensureArray = (val) => {
if (Array.isArray(val)) return val;
if (val && typeof val === 'object') return Object.values(val);
return [];
};
const renderValue = (val) => {
if (!val) return '---';
if (typeof val === 'object' && val !== null) {
return val.numero || val.chave || val.id || JSON.stringify(val);
}
return val;
};
const SolicitacoesView = () => {
const [activeTab, setActiveTab] = useState('PENDENTE'); // PENDENTE, MONITORAMENTO, RELATORIO, FINALIZADO
const [searchTerm, setSearchTerm] = useState('');
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedTrip, setSelectedTrip] = useState(null);
const [showOpsManager, setShowOpsManager] = useState(false);
const [viewMode, setViewMode] = useState('cards'); // cards, table, kanban
const [showAnalysis, setShowAnalysis] = useState(false);
const [selectedNfe, setSelectedNfe] = useState(null);
const [showAttendanceForm, setShowAttendanceForm] = useState(false);
const [tripToAttend, setTripToAttend] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const data = await getTripRequests();
let allRequests = [];
if (Array.isArray(data)) {
allRequests = data;
} else if (data && (data.LIBERADO || data.PENDENTE || data.FECHADO)) {
// Nova estrutura agrupada por status: LIBERADO, PENDENTE, FECHADO
const liberados = ensureArray(data.LIBERADO).map(r => ({
...r,
status: 'LIBERADO'
}));
const pendentes = ensureArray(data.PENDENTE).map(r => ({
...r,
status: 'PENDENTE'
}));
const finalizados = ensureArray(data.FINALIZADO || data.FECHADO).map(r => ({
...r,
status: 'FINALIZADO'
}));
allRequests = [...liberados, ...pendentes, ...finalizados];
} else if (data?.data) {
allRequests = Array.isArray(data.data) ? data.data : (typeof data.data === 'object' ? Object.values(data.data) : []);
} else if (typeof data === 'object' && data !== null) {
// Fallback: tenta pegar qualquer array presente no objeto
allRequests = Object.values(data).find(Array.isArray) || [];
}
setRequests(allRequests);
} catch (error) {
toast.error('Erro ao carregar solicitações');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const filteredRequests = requests.filter(req => {
const status = req.status || 'PENDENTE';
const matchesSearch =
req.nome_completo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
req.placa_do_cavalo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
req.idsolicitacoes?.toString().includes(searchTerm);
if (activeTab === 'PENDENTE') return matchesSearch && status === 'PENDENTE';
if (activeTab === 'MONITORAMENTO') return matchesSearch && status === 'LIBERADO';
if (activeTab === 'FECHADO' || activeTab === 'FINALIZADO') return matchesSearch && (status === 'FECHADO' || status === 'FINALIZADO');
return matchesSearch;
});
const stats = {
pendentes: requests.filter(r => (r.status || 'PENDENTE') === 'PENDENTE').length,
monitorando: requests.filter(r => r.status === 'LIBERADO').length,
total: requests.length
};
return (
<div className="min-h-full bg-transparent text-white font-sans selection:bg-emerald-500/30 selection:text-emerald-200" style={{ fontFamily: 'var(--font-main)' }}>
{/* Header section integrated with Dashboard style */}
<header className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-8">
<div className="space-y-1">
<div className="flex items-center gap-2 text-emerald-400 text-[10px] font-bold uppercase tracking-widest mb-1">
<ShieldCheck size={14} /> Painel de Monitoramento
</div>
<h1 className="text-3xl font-bold tracking-tight uppercase text-white">Gestão de <span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-emerald-600">Solicitações</span></h1>
<p className="text-zinc-500 font-medium text-xs">Controle centralizado de solicitações e monitoramento em tempo real.</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-4 py-2 bg-[#1c1c1c] border border-white/5 rounded-xl">
<RefreshCcw size={14} className={`text-zinc-500 cursor-pointer hover:text-emerald-400 transition-colors ${loading ? 'animate-spin' : ''}`} onClick={fetchData} />
<div className="w-px h-4 bg-zinc-800 mx-1" />
<Calendar size={14} className="text-zinc-500" />
<span className="text-[10px] font-bold text-zinc-300">{new Date().toLocaleDateString('pt-BR')}</span>
</div>
<button
onClick={() => setShowOpsManager(true)}
className="px-6 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all border border-white/5"
>
Gerenciar Operações
</button>
{/* {activeTab === 'MONITORAMENTO' && (
<button
onClick={() => setShowAnalysis(true)}
className="px-6 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all shadow-lg shadow-emerald-500/20"
>
Análises
</button>
)} */}
</div>
</header>
{/* Main Container */}
<section className="space-y-6">
{/* Filter Bar */}
<div className="bg-[#1c1c1c]/50 border border-white/5 rounded-[32px] p-6 flex flex-col gap-6 backdrop-blur-xl">
{/* Navigation Tabs - The Selector */}
<div className="flex items-center gap-1 p-1.5 bg-black/40 rounded-2xl border border-white/5 overflow-x-auto max-w-full">
{[
{ id: 'PENDENTE', label: 'Pendentes', count: stats.pendentes, color: 'amber' },
{ id: 'MONITORAMENTO', label: 'Monitoramento', count: stats.monitorando, color: 'emerald' },
//{ id: 'RELATORIO', label: 'Relatório', count: null, color: 'blue' },
{ id: 'FINALIZADO', label: 'Finalizados', count: null, color: 'zinc' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all whitespace-nowrap ${activeTab === tab.id
? `bg-zinc-800 text-white border border-white/10 shadow-lg`
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
{tab.label}
{tab.count !== null && (
<span className={`w-5 h-5 flex items-center justify-center rounded-lg text-[8px] ${activeTab === tab.id ? 'bg-zinc-700 text-white' : 'bg-zinc-900 text-zinc-600'
}`}>
{tab.count}
</span>
)}
</button>
))}
</div>
{/* Search & Global Actions - Now Below the Selector */}
<div className="flex flex-col md:flex-row items-center gap-4 w-full">
{/* View Mode Switcher */}
<div className="flex items-center gap-1 p-1 bg-black/40 rounded-xl border border-white/5 w-full md:w-auto justify-center">
{[
{ id: 'cards', icon: LayoutGrid, label: 'Cards' },
{ id: 'table', icon: Table2, label: 'Tabela' },
{ id: 'kanban', icon: Columns, label: 'Kanban' }
].map(mode => (
<button
key={mode.id}
onClick={() => setViewMode(mode.id)}
className={`p-2.5 rounded-lg transition-all ${viewMode === mode.id ? 'bg-zinc-800 text-white shadow-lg' : 'text-zinc-600 hover:text-zinc-400'}`}
title={mode.label}
>
<mode.icon size={14} />
</button>
))}
</div>
<div className="flex-1 flex items-center gap-4 w-full">
<div className="relative group flex-1">
<Search size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600 group-focus-within:text-emerald-500 transition-colors" />
<input
type="text"
placeholder="Buscar motorista, placa ou ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 bg-black/40 border border-white/5 rounded-xl text-xs focus:outline-none focus:ring-4 focus:ring-emerald-500/5 focus:border-emerald-500/30 transition-all font-semibold"
/>
</div>
<button
onClick={() => setSearchTerm('')}
className="px-4 py-3 text-[9px] font-bold text-zinc-600 uppercase tracking-widest hover:text-white transition-colors border border-white/5 rounded-xl hover:bg-white/5"
>
Limpar
</button>
</div>
</div>
</div>
{/* Dynamic Content */}
<AnimatePresence mode="wait">
{loading ? (
<motion.div
key="loading"
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="flex flex-col items-center justify-center py-40 gap-4"
>
<Loader2 className="animate-spin text-emerald-500" size={32} />
<span className="text-zinc-600 font-bold uppercase tracking-widest text-[9px]">Acessando dados...</span>
</motion.div>
) : (
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="min-h-[400px]"
>
{activeTab === 'PENDENTE' || activeTab === 'MONITORAMENTO' ? (
<>
{viewMode === 'cards' && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
{filteredRequests.length > 0 ? (
filteredRequests.map(trip => (
<TripCard
key={trip.idsolicitacoes}
trip={trip}
onView={() => setSelectedTrip(trip)}
onViewNfe={(chave) => setSelectedNfe(chave)}
onRefresh={fetchData}
onAttend={() => {
setTripToAttend(trip);
setShowAttendanceForm(true);
}}
/>
))
) : (
<EmptyState message="Nenhum registro encontrado." />
)}
</div>
)}
{viewMode === 'table' && (
<div className="bg-[#1c1c1c]/50 border border-white/5 rounded-[32px] overflow-hidden h-[600px]">
<ExcelTable
data={filteredRequests}
columns={[
{ header: 'ID', field: 'idsolicitacoes', width: '80px' },
{ header: 'Motorista', field: 'nome_completo', width: '200px' },
{
header: 'CPF',
field: 'cpf',
width: '130px',
render: (row) => formatCPF(row.cpf)
},
{ header: 'Cavalo', field: 'placa_do_cavalo', width: '100px', className: 'font-mono uppercase font-bold text-emerald-500' },
{ header: 'Carreta', field: 'placa_da_carreta', width: '100px' },
{ header: 'Operação', field: 'operacao', width: '120px' },
{
header: 'NFE',
field: 'nota_fiscal',
width: '80px',
render: (row) => {
// Tenta obter a chave da NFe de várias fontes possíveis
// Estrutura complexa: row.nota_fiscal pode ser { nfe: { chave: ... }, itens: ... }
const nfeComplex = row.nfe || row.nota_fiscal;
// Tenta extrair a chave de nfeComplex.nfe.chave OU nfeComplex.chave OU nfeComplex (string)
const chave = nfeComplex?.nfe?.chave || nfeComplex?.chave || (typeof nfeComplex === 'string' ? nfeComplex : null);
const numero = nfeComplex?.nfe?.numero || nfeComplex?.numero;
if (chave && chave !== 'NÃO POSSUI' && chave.length > 20) {
return (
<button
onClick={(e) => { e.stopPropagation(); setSelectedNfe(chave); }}
className="p-2 bg-emerald-500/10 text-emerald-500 rounded-lg hover:bg-emerald-500 hover:text-white transition-all shadow-sm flex items-center gap-2"
title={`Ver NFe ${numero || ''}`}
>
<Eye size={16} />
{numero && <span className="text-[10px] font-bold">{numero}</span>}
</button>
);
}
return renderValue(nfeComplex);
}
},
{ header: 'CTe', field: 'cte', width: '120px' },
{ header: 'Origem', field: 'origem', width: '150px' },
{ header: 'Destino', field: 'destino', width: '150px' },
{ header: 'Palets', field: 'quantidade_palet', width: '80px' },
{ header: 'Chapatex', field: 'quantidade_chapatex', width: '80px' },
{ header: 'Tipo', field: 'tipo_de_veiculo', width: '100px' },
{ header: 'Situação', field: 'situacao', width: '150px' },
{ header: 'Status', field: 'status', width: '120px' }
]}
onEdit={(item) => setSelectedTrip(item)}
onDelete={(item) => deleteTripRequest(item.idsolicitacoes).then(fetchData)}
/>
</div>
)}
{viewMode === 'kanban' && (
<div className="h-[600px] w-full">
<GrKanbanBoard
columns={[
{
id: 'PENDENTE',
title: 'Pendentes',
color: 'amber',
cards: ensureArray(requests).filter(r => (r.status || 'PENDENTE') === 'PENDENTE' && (
r.nome_completo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.placa_do_cavalo?.toLowerCase().includes(searchTerm.toLowerCase())
)).map(r => ({
id: r.idsolicitacoes,
title: r.nome_completo,
subtitle: formatCPF(r.cpf),
date: new Date(r.created_at || new Date()).toLocaleDateString('pt-BR'),
base: r.operacao,
details: [
{ label: 'Cavalo', value: r.placa_do_cavalo },
{ label: 'Carreta', value: r.placa_da_carreta || '---' },
{ label: 'Doc', value: renderValue(r.cte || r.nota_fiscal) },
{ label: 'Rota', value: `${r.origem}${r.destino}` }
],
...r
}))
},
{
id: 'LIBERADO',
title: 'Monitoramento',
color: 'emerald',
cards: ensureArray(requests).filter(r => r.status === 'LIBERADO' && (
r.nome_completo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.placa_do_cavalo?.toLowerCase().includes(searchTerm.toLowerCase())
)).map(r => ({
id: r.idsolicitacoes,
title: r.nome_completo,
subtitle: formatCPF(r.cpf),
date: new Date(r.created_at || new Date()).toLocaleDateString('pt-BR'),
base: r.operacao,
details: [
{ label: 'Cavalo', value: r.placa_do_cavalo },
{ label: 'Carreta', value: r.placa_da_carreta || '---' },
{ label: 'Doc', value: renderValue(r.cte || r.nota_fiscal) },
{ label: 'Rota', value: `${r.origem}${r.destino}` }
],
...r
}))
},
{
id: 'FINALIZADO',
title: 'Finalizados',
color: 'zinc',
cards: ensureArray(requests).filter(r => (r.status === 'FECHADO' || r.status === 'FINALIZADO') && (
r.nome_completo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.placa_do_cavalo?.toLowerCase().includes(searchTerm.toLowerCase())
)).map(r => ({
id: r.idsolicitacoes,
title: r.nome_completo,
subtitle: formatCPF(r.cpf),
date: new Date(r.created_at || new Date()).toLocaleDateString('pt-BR'),
base: r.operacao,
details: [
{ label: 'Cavalo', value: r.placa_do_cavalo },
{ label: 'Carreta', value: r.placa_da_carreta || '---' },
{ label: 'Doc', value: renderValue(r.cte || r.nota_fiscal) },
{ label: 'Rota', value: `${r.origem}${r.destino}` }
],
...r
}))
}
]}
onCardClick={(card) => setSelectedTrip(card)}
onCardMove={async (id, newStatus) => {
if (newStatus === 'LIBERADO') {
const trip = requests.find(r => r.idsolicitacoes === id);
if (trip) {
setTripToAttend(trip);
setShowAttendanceForm(true);
return;
}
}
try {
await updateTripStatus(id, newStatus);
toast.success('Status atualizado');
fetchData();
} catch (e) { toast.error('Erro ao mover'); }
}}
/>
</div>
)}
</>
) : (activeTab === 'FECHADO' || activeTab === 'FINALIZADO') ? (
<div className="bg-[#1c1c1c]/30 border border-white/5 rounded-3xl overflow-hidden">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-white/5 bg-black/20">
{['ID', 'Motorista', 'Placa', 'Operação', 'Status', 'Data'].map(h => (
<th key={h} className="px-6 py-4 text-[9px] font-bold text-zinc-500 uppercase tracking-widest">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{filteredRequests.map(trip => (
<tr key={trip.idsolicitacoes} className="hover:bg-white/5 transition-colors group">
<td className="px-6 py-4 text-xs font-bold text-zinc-500">#{trip.idsolicitacoes}</td>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-800 flex items-center justify-center text-[10px] font-bold text-zinc-500 group-hover:text-emerald-400">{trip.nome_completo?.[0]}</div>
<span className="text-xs font-bold text-zinc-300 uppercase">{trip.nome_completo}</span>
</div>
</td>
<td className="px-6 py-4 text-xs font-bold text-zinc-400 uppercase">{trip.placa_do_cavalo}</td>
<td className="px-6 py-4">
<span className="px-3 py-1 bg-zinc-800 rounded-lg text-[9px] font-bold uppercase tracking-tighter text-zinc-400">{trip.operacao}</span>
</td>
<td className="px-6 py-4">
<span className="px-3 py-1 bg-emerald-500/10 text-emerald-400 rounded-full text-[8px] font-bold uppercase tracking-tighter">FINALIZADO</span>
</td>
<td className="px-6 py-4 text-xs font-bold text-zinc-600">{new Date().toLocaleDateString('pt-BR')}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="grid lg:grid-cols-2 gap-6">
<div className="bg-[#1c1c1c]/30 border border-white/5 border-dashed rounded-[32px] p-8 aspect-[16/9] flex items-center justify-center text-zinc-700 font-bold uppercase tracking-widest italic text-[10px]">
[ Histórico de Atendimento ]
</div>
<div className="bg-[#1c1c1c]/30 border border-white/5 border-dashed rounded-[32px] p-8 aspect-[16/9] flex items-center justify-center text-zinc-700 font-bold uppercase tracking-widest italic text-[10px]">
[ Desempenho por Unidade ]
</div>
</div>
)}
</motion.div>
)}
</AnimatePresence>
</section>
{/* Modals */}
<AnimatePresence>
{selectedTrip && (
<TripDetailsModal
trip={selectedTrip}
onClose={() => setSelectedTrip(null)}
onRefresh={fetchData}
/>
)}
{showOpsManager && (
<OperationsManagerModal
onClose={() => setShowOpsManager(false)}
/>
)}
{showAnalysis && (
<AnalysisPanel
onClose={() => setShowAnalysis(false)}
/>
)}
{selectedNfe && (
<NfeViewerModal
chave={selectedNfe}
onClose={() => setSelectedNfe(null)}
/>
)}
{showAttendanceForm && tripToAttend && (
<AttendanceFormModal
trip={tripToAttend}
onClose={() => {
setShowAttendanceForm(false);
setTripToAttend(null);
}}
onRefresh={fetchData}
/>
)}
</AnimatePresence>
</div>
);
};
const TripCard = ({ trip, onView, onRefresh, onViewNfe, onAttend }) => {
const [loading, setLoading] = useState(false);
const handleAttend = (e) => {
e.stopPropagation();
onAttend();
};
const handleArchive = async (e) => {
e.stopPropagation();
if (!window.confirm('Finalizar esta solicitação?')) return;
setLoading(true);
try {
await closeTripRequest(trip.idsolicitacoes);
toast.success('Solicitação finalizada!');
onRefresh();
} catch (e) { toast.error('Erro ao finalizar'); }
finally { setLoading(false); }
};
const handleDelete = async (e) => {
e.stopPropagation();
if (!window.confirm('Excluir?')) return;
setLoading(true);
try {
await deleteTripRequest(trip.idsolicitacoes);
toast.success('Excluído');
onRefresh();
} catch (e) { toast.error('Erro ao excluir'); }
finally { setLoading(false); }
};
return (
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
onClick={onView}
className="bg-[#1c1c1c]/50 border border-white/5 rounded-[28px] p-5 hover:bg-[#252525]/50 hover:border-white/10 transition-all group relative overflow-hidden cursor-pointer active:scale-95 flex flex-col justify-between min-h-[180px]"
>
{/* Decorative Accent */}
<div className={`absolute top-0 left-0 right-0 h-1.5 ${trip.status === 'PENDENTE' ? 'bg-amber-500/50' : 'bg-emerald-500/50'
}`} />
<div>
<div className="flex items-center justify-between mb-4">
<span className="px-3 py-1 bg-zinc-800 rounded-lg text-xs font-medium uppercase text-zinc-300 group-hover:text-emerald-400 transition-colors tracking-wide">{trip.operacao}</span>
{/* Tag de status removida de dentro do card conforme solicitado */}
</div>
<div className="space-y-4 mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center text-zinc-500 group-hover:bg-emerald-500/10 group-hover:text-emerald-400 transition-colors">
<User size={18} />
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-zinc-200 group-hover:text-white transition-colors uppercase truncate leading-tight">{trip.nome_completo}</div>
<div className="text-[11px] font-normal text-zinc-500 uppercase">{formatCPF(trip.cpf)}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center text-zinc-500">
<Truck size={18} />
</div>
<div className="flex-1 grid grid-cols-2 gap-2">
<div>
<div className="text-[11px] font-medium text-zinc-200 uppercase">{trip.placa_do_cavalo}</div>
<div className="text-[9px] font-normal text-zinc-500 uppercase">Cavalo</div>
</div>
<div>
<div className="text-[11px] font-medium text-zinc-300 uppercase">{trip.placa_da_carreta || '---'}</div>
<div className="text-[9px] font-normal text-zinc-500 uppercase">Carreta</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3 pt-3 border-t border-white/5">
<div className="flex items-center gap-2">
<FileText size={12} className="text-zinc-600" />
<div className="min-w-0">
<div className="text-[10px] font-medium text-zinc-300 truncate">{renderValue(trip.cte || trip.nota_fiscal)}</div>
<div className="text-[8px] font-normal text-zinc-600 uppercase">Documento</div>
</div>
</div>
<div className="flex items-center gap-2">
<Package size={12} className="text-zinc-600" />
<div className="min-w-0">
<div className="text-[10px] font-medium text-zinc-300">P: {trip.quantidade_palet || 0} / C: {trip.quantidade_chapatex || 0}</div>
<div className="text-[8px] font-normal text-zinc-600 uppercase">Palet / Chap</div>
</div>
</div>
</div>
{trip.situacao && (
<div className="mt-3 px-3 py-2 bg-blue-500/10 border border-blue-500/20 rounded-xl">
<div className="text-[9px] font-bold text-blue-400 uppercase tracking-widest mb-0.5">Observação / Situação</div>
<div className="text-[10px] text-zinc-300 line-clamp-2">{trip.situacao}</div>
</div>
)}
<div className="flex items-center gap-3 pt-3 border-t border-white/5">
<div className="w-8 h-8 rounded-xl bg-zinc-800/20 flex items-center justify-center text-zinc-600">
<MapPin size={14} />
</div>
<div className="text-[10px] font-normal text-zinc-400 uppercase flex items-center gap-1.5 min-w-0">
<span className="truncate">{trip.origem}</span>
<span className="text-zinc-600 font-medium"></span>
<span className="truncate">{trip.destino}</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2 pt-3 border-t border-white/5">
{trip.status === 'PENDENTE' && (
<button
onClick={handleAttend}
disabled={loading}
className="flex-1 h-9 bg-emerald-600 hover:bg-emerald-500 text-white rounded-xl text-[10px] font-medium uppercase tracking-widest transition-all"
>
Atender
</button>
)}
{trip.status === 'LIBERADO' && (
<button
onClick={handleArchive}
disabled={loading}
className="h-10 w-12 bg-emerald-600/20 hover:bg-emerald-600 text-emerald-500 hover:text-white rounded-xl flex items-center justify-center transition-all"
title="Finalizar Solicitação"
>
<CheckCircle2 size={20} />
</button>
)}
{(() => {
const nfeComplex = trip.nfe || trip.nota_fiscal;
const chave = nfeComplex?.nfe?.chave || nfeComplex?.chave || (typeof nfeComplex === 'string' ? nfeComplex : null);
if (chave && chave !== 'NÃO POSSUI' && chave.length > 20) {
return (
<button
onClick={(e) => { e.stopPropagation(); onViewNfe(chave); }}
className="h-10 px-4 bg-emerald-600/10 hover:bg-emerald-600 text-emerald-500 hover:text-white rounded-xl flex items-center gap-2 transition-all font-bold text-[10px] uppercase tracking-widest border border-emerald-600/20"
title="Visualizar DANFE"
>
<Eye size={18} /> NFe
</button>
);
}
return null;
})()}
{trip.status === 'LIBERADO' && (<div className="flex-1"></div>)} {/* Spacer */}
{/* Botão de excluir comentado conforme solicitado */}
{/*
<button
onClick={handleDelete}
disabled={loading}
className="h-10 w-12 bg-black/40 hover:bg-red-500/20 text-zinc-600 hover:text-red-500 rounded-xl flex items-center justify-center transition-all border border-white/5"
title="Excluir"
>
<Trash2 size={20} />
</button>
*/}
</div>
</motion.div>
);
};
const EmptyState = ({ message }) => (
<div className="col-span-full py-40 flex flex-col items-center justify-center text-zinc-800 font-bold uppercase tracking-[0.2em] italic border border-dashed border-white/5 rounded-3xl text-[10px]">
{message}
</div>
);
export default SolicitacoesView;

View File

@ -113,7 +113,7 @@ export default function StatisticsView() {
label: 'Frota Total',
value: totalVeiculos.toLocaleString(),
icon: <Truck />,
color: 'bg-emerald-500/10 text-emerald-600'
color: 'bg-orange-500/10 text-orange-600'
},
{
label: 'Em Manutenção (Aberta)',
@ -152,8 +152,8 @@ export default function StatisticsView() {
{/* Header com Design Premium */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-1">
<h1 className="text-3xl font-medium text-white tracking-tight">
Dashboard <span className="text-emerald-500">Estatístico</span>
<h1 className="text-4xl font-bold text-slate-900 dark:text-white tracking-tight">
Dashboard <span className="text-orange-500">Estatístico</span>
</h1>
<p className="text-slate-500 dark:text-slate-400 font-medium text-lg">
Monitoramento de BI e KPIs em tempo real da operação de frota.
@ -167,7 +167,7 @@ export default function StatisticsView() {
<RefreshCw size={18} className={statsLoading ? "animate-spin" : ""} />
Sincronizar Dados
</button>
{/* <button className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-emerald-600 text-white font-bold text-sm hover:bg-emerald-700 hover:shadow-xl hover:shadow-emerald-500/20 hover:-translate-y-0.5 transition-all">
{/* <button className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-orange-600 text-white font-bold text-sm hover:bg-orange-700 hover:shadow-xl hover:shadow-orange-500/20 hover:-translate-y-0.5 transition-all">
<Download size={18} />
Exportar BI
</button> */}
@ -176,7 +176,7 @@ export default function StatisticsView() {
{/* KPI Section */}
<div className="relative">
<div className="absolute -inset-1 bg-gradient-to-r from-emerald-500/10 to-blue-500/10 blur-2xl opacity-50 -z-10" />
<div className="absolute -inset-1 bg-gradient-to-r from-orange-500/10 to-blue-500/10 blur-2xl opacity-50 -z-10" />
<StatsGrid stats={stats} />
</div>
@ -267,7 +267,7 @@ export default function StatisticsView() {
<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-emerald-500/10 rounded-2xl text-emerald-600 shadow-inner">
<div className="p-4 bg-orange-500/10 rounded-2xl text-orange-600 shadow-inner">
<Truck size={28} />
</div>
<div>
@ -275,7 +275,7 @@ export default function StatisticsView() {
Veículos: {selectedStatus?.status}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Listagem completa dos {selectedStatus?.total} veículos com status <span className="text-emerald-500 font-bold">"{selectedStatus?.status}"</span>.
Listagem completa dos {selectedStatus?.total} veículos com status <span className="text-orange-500 font-bold">"{selectedStatus?.status}"</span>.
</DialogDescription>
</div>
</div>
@ -286,7 +286,7 @@ export default function StatisticsView() {
<ExcelTable
data={selectedStatus?.registros ? JSON.parse(selectedStatus.registros).filter(r => r !== null) : []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
@ -324,7 +324,7 @@ export default function StatisticsView() {
<Card className="border-none shadow-sm hover:shadow-md transition-all overflow-hidden bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between border-b dark:border-[#2a2a2a] mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-xl text-emerald-600">
<div className="p-2 bg-orange-500/10 rounded-xl text-orange-600">
<LineChartIcon size={20} />
</div>
<CardTitle className="text-xl font-bold">Fluxo de Manutenção Mensal</CardTitle>
@ -391,7 +391,7 @@ export default function StatisticsView() {
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Regional</span>
<MapPin size={14} className="text-emerald-500" />
<MapPin size={14} className="text-orange-500" />
</div>
<CardTitle className="text-lg font-bold">Veículos por Base</CardTitle>
</CardHeader>
@ -490,7 +490,7 @@ export default function StatisticsView() {
<CardTitle className="text-xl font-bold">Disponibilidade Detalhada</CardTitle>
<p className="text-sm text-slate-500">Breakdown por status de operação.</p>
</div>
<div className="p-3 bg-emerald-500/10 rounded-2xl text-emerald-600">
<div className="p-3 bg-orange-500/10 rounded-2xl text-orange-600">
<Gauge size={24} strokeWidth={2.5} />
</div>
</CardHeader>
@ -500,7 +500,7 @@ export default function StatisticsView() {
<div key={i} className="group cursor-default">
<div className="flex items-center justify-between mb-2">
<div className="flex flex-col min-w-0">
<span className="text-sm font-bold text-slate-800 dark:text-slate-100 group-hover:text-emerald-500 transition-colors uppercase tracking-tight truncate">{item.status_disponibilidade || 'NÃO INFORMADO'}</span>
<span className="text-sm font-bold text-slate-800 dark:text-slate-100 group-hover:text-orange-500 transition-colors uppercase tracking-tight truncate">{item.status_disponibilidade || 'NÃO INFORMADO'}</span>
<span className="text-[10px] font-bold text-slate-400 tracking-widest uppercase truncate">{item.disponibilidade}</span>
</div>
<div className="flex items-center gap-3 shrink-0">
@ -512,7 +512,7 @@ export default function StatisticsView() {
</div>
<div className="w-full h-2 bg-slate-100 dark:bg-[#2a2a2a] rounded-full overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full transition-all duration-1000 ease-out"
className="h-full bg-orange-500 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${(item.total / (totalVeiculos || 1)) * 100}%` }}
/>
</div>
@ -556,3 +556,6 @@ export default function StatisticsView() {
</div>
);
}

View File

@ -15,7 +15,7 @@ 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-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
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>
@ -27,7 +27,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
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 => (
@ -40,7 +40,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
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"
};
@ -79,7 +79,7 @@ const StatusCell = ({ currentStatus, id, options, onUpdate }) => {
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"
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>
@ -94,7 +94,7 @@ const StatusCell = ({ currentStatus, id, options, onUpdate }) => {
<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-emerald-500/10 text-emerald-500 border-emerald-500/20' :
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'
}`}
@ -203,7 +203,7 @@ export default function StatusView() {
{/* ... Header ... */}
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Status da Frota</h1>
<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">
@ -211,7 +211,7 @@ export default function StatusView() {
<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-emerald-500 w-full md:w-64"
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)}
@ -232,7 +232,7 @@ export default function StatusView() {
rowKey="idstatus_frota"
columns={[
{ header: 'ID', field: 'idstatus_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ 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}
@ -327,7 +327,7 @@ export default function StatusView() {
<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-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[80px]"
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})}
/>
@ -344,3 +344,6 @@ export default function StatusView() {
);
}

View File

@ -106,3 +106,6 @@ const TableDebug = () => {
};
export default TableDebug;

View File

@ -21,7 +21,7 @@ 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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -33,7 +33,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -46,7 +46,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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",
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"
};
@ -87,7 +87,7 @@ const StatusCell = ({ currentStatus, idVehicle, options, onUpdate }) => {
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"
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()} // Prevent row selection if applied
>
<option value="">Selecione...</option>
@ -102,7 +102,7 @@ const StatusCell = ({ currentStatus, idVehicle, options, onUpdate }) => {
<div
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border cursor-pointer hover:opacity-80 transition-opacity ${
currentStatus === 'ATIVO' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
currentStatus === 'ATIVO' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-slate-500/10 text-slate-500 border-slate-500/20'
}`}
>
@ -234,14 +234,14 @@ export default function VehiclesView() {
{/* Header Actions */}
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Frota & Ativos</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Frota & Ativos</h1>
<p className="text-slate-500 text-sm">Gerencie os veículos cadastrados na plataforma.</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"
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-orange-500 w-full md:w-64"
placeholder="Buscar placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -273,10 +273,10 @@ export default function VehiclesView() {
</div>
<div
onClick={() => handleStatusClick('Em Operação')}
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-xl p-6 shadow-sm hover:shadow-md transition-all flex items-center justify-between group cursor-pointer hover:border-emerald-500/30"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-xl p-6 shadow-sm hover:shadow-md transition-all flex items-center justify-between group cursor-pointer hover:border-orange-500/30"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-emerald-500/10 rounded-xl text-emerald-600 group-hover:bg-emerald-500 group-hover:text-white transition-colors">
<div className="p-3 bg-orange-500/10 rounded-xl text-orange-600 group-hover:bg-orange-500 group-hover:text-white transition-colors">
<CheckCircle size={24} />
</div>
<div className="flex flex-col">
@ -297,7 +297,7 @@ export default function VehiclesView() {
columns={[
{ header: 'ID', field: 'idveiculo_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'MODELO', field: 'modelo', width: '140px' },
{ header: ' FABRICANTE', field: 'fabricante', width: '120px' },
{ header: 'CORES', field: 'cor', width: '100px' },
@ -347,7 +347,7 @@ export default function VehiclesView() {
<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-medium text-white uppercase tracking-tight">
<DialogTitle className="text-xl font-bold text-slate-800 dark:text-white uppercase tracking-tight">
{editingVehicle ? `Editando: ${formData.placa}` : 'Cadastro de Novo Veículo'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-stone-500">
@ -366,7 +366,7 @@ export default function VehiclesView() {
<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"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
>
{tab === 'tecnico' ? 'RASTREADOR' : tab}
</TabsTrigger>
@ -472,7 +472,7 @@ export default function VehiclesView() {
<div className="gap-4 pt-2">
<label className="text-[10px] uppercase font-bold text-slate-500 tracking-wider ml-1">Observações Gerais</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-[80px]"
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 min-h-[80px]"
value={formData.observacoes}
onChange={e => setFormData({...formData, observacoes: e.target.value})}
/>
@ -498,7 +498,7 @@ export default function VehiclesView() {
<Truck size={28} />
</div>
<div>
<DialogTitle className="text-2xl font-medium text-white uppercase tracking-tight">
<DialogTitle className="text-2xl font-bold 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">
@ -513,7 +513,7 @@ export default function VehiclesView() {
<ExcelTable
data={selectedStatusRecords?.records || []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
@ -543,3 +543,6 @@ export default function VehiclesView() {
</div>
);
}

View File

@ -11,7 +11,7 @@ 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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -23,7 +23,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<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"
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-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -36,7 +36,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
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",
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"
};
@ -107,14 +107,14 @@ export default function WorkshopsView() {
<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-medium text-white tracking-tight uppercase">Gestão de Oficinas</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight uppercase">Gestão de Oficinas</h1>
<p className="text-slate-500 text-sm">Rede credenciada e prestadores de serviços.</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 transition-all"
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-orange-500 w-full md:w-64 transition-all"
placeholder="Buscar por nome, CNPJ ou cidade..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -134,7 +134,7 @@ export default function WorkshopsView() {
columns={[
{ header: 'ID', field: 'idoficinas_frota', width: '80px' },
{ header: 'Cod. Estab.', field: 'cod_estabelecimento', width: '100px' },
{ header: 'Nome Reduzido', field: 'nome_reduzido', width: '220px', className: 'font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Nome Reduzido', field: 'nome_reduzido', width: '220px', className: 'font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Razão Social', field: 'razao_social', width: '250px' },
{ header: 'CNPJ', field: 'cnpj', width: '150px' },
{ header: 'Tipo', field: 'tipo_estabelecimento', width: '180px' },
@ -160,7 +160,7 @@ export default function WorkshopsView() {
<DialogContent className="max-w-3xl 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/50 dark:bg-[#222]/30">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold flex items-center gap-2">
<div className="w-2 h-6 bg-emerald-500 rounded-xl"></div>
<div className="w-2 h-6 bg-orange-500 rounded-xl"></div>
{editingItem ? 'Editar Oficina' : 'Cadastrar Nova Oficina'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
@ -171,7 +171,7 @@ export default function WorkshopsView() {
<form onSubmit={handleSubmit} className="px-6 py-6 space-y-6 max-h-[70vh] overflow-y-auto custom-scrollbar">
{/* Seção: Identificação */}
<div className="space-y-4">
<h3 className="text-[11px] font-bold text-emerald-600 dark:text-emerald-500 uppercase tracking-[0.2em] border-b border-emerald-500/20 pb-1">Identificação</h3>
<h3 className="text-[11px] font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/20 pb-1">Identificação</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<DarkInput label="Cod. Estabelecimento" value={formData.cod_estabelecimento} onChange={e => setFormData({...formData, cod_estabelecimento: e.target.value})} />
@ -191,7 +191,7 @@ export default function WorkshopsView() {
{/* Seção: Localização e Contato */}
<div className="space-y-4">
<h3 className="text-[11px] font-bold text-emerald-600 dark:text-emerald-500 uppercase tracking-[0.2em] border-b border-emerald-500/20 pb-1">Localização e Contato</h3>
<h3 className="text-[11px] font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/20 pb-1">Localização e Contato</h3>
<DarkInput label="Endereço" value={formData.endereco} onChange={e => setFormData({...formData, endereco: e.target.value})} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@ -216,3 +216,6 @@ export default function WorkshopsView() {
</div>
);
}