Cliente/OestPan #2
|
|
@ -133,3 +133,6 @@ const AdvancedFiltersModal = ({ isOpen, onClose, onApply, options = {}, initialF
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdvancedFiltersModal;
|
export default AdvancedFiltersModal;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -70,7 +70,7 @@ const AutocompleteInput = ({
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
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}
|
placeholder={placeholder}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -96,7 +96,7 @@ const AutocompleteInput = ({
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={`px-3 py-2 text-sm cursor-pointer flex items-center justify-between hover:bg-slate-100 dark:hover:bg-[#2a2a2a] ${
|
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)}
|
onClick={() => handleSelect(option)}
|
||||||
>
|
>
|
||||||
|
|
@ -118,3 +118,6 @@ const AutocompleteInput = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AutocompleteInput;
|
export default AutocompleteInput;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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="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="flex flex-col items-center gap-4">
|
||||||
<div className="relative">
|
<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="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>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -165,10 +165,10 @@ const ExcelTable = ({
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Actions Button (Green Highlight) - Commented as requested
|
{/* 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="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="bg-orange-100 dark:bg-[#15201b] text-orange-600 px-1 rounded text-[9px]">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500 inline-block"></span>
|
<span className="w-2 h-2 rounded-full bg-orange-500 inline-block"></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
*/}
|
*/}
|
||||||
|
|
@ -248,7 +248,7 @@ const ExcelTable = ({
|
||||||
ref={input => { if (input) input.indeterminate = isIndeterminate; }}
|
ref={input => { if (input) input.indeterminate = isIndeterminate; }}
|
||||||
onChange={handleSelectAll}
|
onChange={handleSelectAll}
|
||||||
disabled={!onSelectionChange}
|
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>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -270,24 +270,24 @@ const ExcelTable = ({
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between h-full w-full">
|
<div className="flex items-center justify-between h-full w-full">
|
||||||
<div className="flex items-center gap-1.5">
|
<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}
|
{col.header}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sort/Menu Icons */}
|
{/* Sort/Menu Icons */}
|
||||||
<div className={`flex flex-col gap-0.5 ${sortConfig.key === col.field ? 'opacity-100' : 'opacity-40 group-hover:opacity-100'}`}>
|
<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" />
|
<path d="M4 4L0 0H8L4 4Z" />
|
||||||
</svg>
|
</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" />
|
<path d="M4 4L0 0H8L4 4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column Resizer Handle */}
|
{/* 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>
|
</th>
|
||||||
))}
|
))}
|
||||||
{/* Spacer for scrollbar */}
|
{/* Spacer for scrollbar */}
|
||||||
|
|
@ -319,7 +319,7 @@ const ExcelTable = ({
|
||||||
checked={selectedIds.includes(row[rowKey])}
|
checked={selectedIds.includes(row[rowKey])}
|
||||||
onChange={() => handleSelectRow(row[rowKey])}
|
onChange={() => handleSelectRow(row[rowKey])}
|
||||||
disabled={!onSelectionChange}
|
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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -330,7 +330,7 @@ const ExcelTable = ({
|
||||||
<div className="flex items-center justify-center h-full w-full gap-2">
|
<div className="flex items-center justify-center h-full w-full gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit && onEdit(row)}
|
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} />
|
<Edit2 size={16} strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -375,7 +375,7 @@ const ExcelTable = ({
|
||||||
{/* Left: Totals */}
|
{/* Left: Totals */}
|
||||||
<div className="flex h-full items-center gap-4">
|
<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="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-[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>
|
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{processedData.length}</span>
|
||||||
|
|
||||||
|
|
@ -383,7 +383,7 @@ const ExcelTable = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[100px]">
|
<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-[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>
|
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{currentPage} / {totalPages || 1}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -399,7 +399,7 @@ const ExcelTable = ({
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(1)}
|
onClick={() => setCurrentPage(1)}
|
||||||
disabled={currentPage === 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} />
|
<ChevronsLeft size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -408,14 +408,14 @@ const ExcelTable = ({
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 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} />
|
<ChevronLeft size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Manual Page Buttons (Simple logic) */}
|
{/* Manual Page Buttons (Simple logic) */}
|
||||||
<div className="flex gap-1 mx-1">
|
<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}
|
{currentPage}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -424,7 +424,7 @@ const ExcelTable = ({
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
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} />
|
<ChevronRight size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -433,7 +433,7 @@ const ExcelTable = ({
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(totalPages)}
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
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} />
|
<ChevronsRight size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -454,3 +454,6 @@ const ExcelTable = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExcelTable;
|
export default ExcelTable;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@ export const useFeedbackStore = create((set) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const icons = {
|
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" />,
|
error: <XCircle className="w-6 h-6 text-rose-500" />,
|
||||||
warning: <AlertCircle className="w-6 h-6 text-amber-500" />,
|
warning: <AlertCircle className="w-6 h-6 text-amber-500" />,
|
||||||
info: <Info className="w-6 h-6 text-blue-500" />
|
info: <Info className="w-6 h-6 text-blue-500" />
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = {
|
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",
|
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",
|
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"
|
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]}`}
|
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 */}
|
{/* 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">
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
{icons[n.type]}
|
{icons[n.type]}
|
||||||
|
|
@ -79,7 +79,7 @@ export const FeedbackContainer = () => {
|
||||||
initial={{ scaleX: 1 }}
|
initial={{ scaleX: 1 }}
|
||||||
animate={{ scaleX: 0 }}
|
animate={{ scaleX: 0 }}
|
||||||
transition={{ duration: Math.min((n.duration || 5000) / 1000, 30), ease: "linear" }}
|
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>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -110,3 +110,6 @@ export const useFeedback = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-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">
|
<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>
|
<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} />
|
<RefreshCcw className={state.isLoading ? "animate-spin" : ""} size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,7 +137,7 @@ const FinesCardDebug = (initialProps) => {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={state.hasData}
|
checked={state.hasData}
|
||||||
onChange={toggleData}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -154,7 +154,7 @@ const FinesCardDebug = (initialProps) => {
|
||||||
step="100"
|
step="100"
|
||||||
value={state.previousValue}
|
value={state.previousValue}
|
||||||
onChange={updatePrevValue}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -169,3 +169,6 @@ const FinesCardDebug = (initialProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FinesCardDebug;
|
export default FinesCardDebug;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ export const FinesCard = ({
|
||||||
const isReduction = percentage < 0;
|
const isReduction = percentage < 0;
|
||||||
const isIncrease = percentage > 0;
|
const isIncrease = percentage > 0;
|
||||||
|
|
||||||
const trendColor = isReduction ? 'text-emerald-500' : isIncrease ? 'text-rose-500' : 'text-slate-500';
|
const trendColor = isReduction ? 'text-orange-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 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 TrendIcon = isReduction ? TrendingDown : isIncrease ? TrendingUp : Minus;
|
||||||
|
|
||||||
const isEmpty = !data || data.length === 0 || currentValue === 0;
|
const isEmpty = !data || data.length === 0 || currentValue === 0;
|
||||||
|
|
@ -84,7 +84,7 @@ export const FinesCard = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right">
|
<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)}
|
{formatCurrency(currentValue)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500 dark:text-slate-400 font-medium mt-1">{currentCount} registros</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(
|
className={cn(
|
||||||
"flex-1 py-1.5 rounded-md text-[10px] uppercase font-bold tracking-wider transition-all",
|
"flex-1 py-1.5 rounded-md text-[10px] uppercase font-bold tracking-wider transition-all",
|
||||||
period === p
|
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"
|
: "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 = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
export { FinesCard } from './FinesCard';
|
export { FinesCard } from './FinesCard';
|
||||||
export { FinesCard as default } from './FinesCard';
|
export { FinesCard as default } from './FinesCard';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
border: 1px solid var(--pfs-border);
|
border: 1px solid var(--pfs-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle Top Glow - Emerald */
|
/* Subtle Top Glow - orange */
|
||||||
.pfs-container::before {
|
.pfs-container::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: linear-gradient(90deg, transparent, #10b981, transparent);
|
background: linear-gradient(90deg, transparent, #f97316, transparent);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,8 +97,8 @@
|
||||||
|
|
||||||
.pfs-toggle-btn:hover {
|
.pfs-toggle-btn:hover {
|
||||||
background: var(--pfs-bg-search);
|
background: var(--pfs-bg-search);
|
||||||
color: #10b981;
|
color: #f97316;
|
||||||
border-color: #10b981;
|
border-color: #f97316;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,9 +128,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pfs-search-input:focus {
|
.pfs-search-input:focus {
|
||||||
border-color: #10b981;
|
border-color: #f97316;
|
||||||
background: var(--pfs-bg);
|
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 */
|
/* Navigation Content */
|
||||||
|
|
@ -175,10 +175,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pfs-link.active {
|
.pfs-link.active {
|
||||||
background: #10b981 !important;
|
background: #f97316 !important;
|
||||||
color: var(--pfs-text-active) !important;
|
color: var(--pfs-text-active) !important;
|
||||||
font-weight: 700;
|
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 {
|
.pfs-icon {
|
||||||
|
|
@ -250,7 +250,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pfs-sublink.active {
|
.pfs-sublink.active {
|
||||||
color: #10b981;
|
color: #f97316;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
|
|
||||||
.pfs-sublink.active .pfs-icon {
|
.pfs-sublink.active .pfs-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: #10b981;
|
color: #f97316;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer Section */
|
/* Footer Section */
|
||||||
|
|
@ -289,14 +289,14 @@
|
||||||
.pfs-brand-logo {
|
.pfs-brand-logo {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background: #10b981;
|
background: #f97316;
|
||||||
color: #141414;
|
color: #141414;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: 900;
|
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;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +314,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pfs-brand-name span {
|
.pfs-brand-name span {
|
||||||
color: #10b981;
|
color: #f97316;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pfs-app-sub {
|
.pfs-app-sub {
|
||||||
|
|
@ -385,7 +385,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pfs-legal-item svg, .pfs-version svg {
|
.pfs-legal-item svg, .pfs-version svg {
|
||||||
color: #10b981;
|
color: #f97316;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,5 +397,8 @@
|
||||||
|
|
||||||
.pfs-lock-icon {
|
.pfs-lock-icon {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #10b981 !important;
|
color: #f97316 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,12 @@ import {
|
||||||
Award,
|
Award,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Lock,
|
Lock,
|
||||||
Settings,
|
Settings
|
||||||
Mail
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuthContext } from '@/components/shared/AuthProvider';
|
import { useAuthContext } from '@/components/shared/AuthProvider';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import logoOestePan from '@/assets/Img/Util/Oeste_Pan/Logo Oeste Pan.png';
|
||||||
import './PrafrotSidebar.css';
|
import './PrafrotSidebar.css';
|
||||||
|
|
||||||
const MENU_ITEMS = [
|
const MENU_ITEMS = [
|
||||||
|
|
@ -34,17 +34,17 @@ const MENU_ITEMS = [
|
||||||
id: 'dashboard',
|
id: 'dashboard',
|
||||||
label: 'Estatísticas',
|
label: 'Estatísticas',
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
path: '/plataforma/prafrot/estatisticas'
|
path: '/plataforma/oest-pan/estatisticas'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cadastros',
|
id: 'cadastros',
|
||||||
label: 'Cadastros',
|
label: 'Cadastros',
|
||||||
icon: ClipboardList,
|
icon: ClipboardList,
|
||||||
children: [
|
children: [
|
||||||
{ id: 'c-veiculos', label: 'Veículos', path: '/plataforma/prafrot/veiculos', icon: Car },
|
{ id: 'c-veiculos', label: 'Veículos', path: '/plataforma/oest-pan/veiculos', icon: Car },
|
||||||
{ id: 'c-dispatcher', label: 'Dispatcher', path: '/plataforma/prafrot/dispatcher', icon: ClipboardList },
|
{ id: 'c-dispatcher', label: 'Dispatcher', path: '/plataforma/oest-pan/dispatcher', icon: ClipboardList },
|
||||||
// { id: 'c-motoristas', label: 'Motoristas', path: '/plataforma/prafrot/motoristas', icon: Users, disabled: true, disabledReason: 'Funcionalidade em manutenção' },
|
// { 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/prafrot/oficinas', icon: Store }
|
{ id: 'c-oficinas', label: 'Oficinas', path: '/plataforma/oest-pan/oficinas', icon: Store }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -52,20 +52,17 @@ const MENU_ITEMS = [
|
||||||
label: 'Gerência',
|
label: 'Gerência',
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
children: [
|
children: [
|
||||||
{ id: 'g-solicitacoes', label: 'Solicitações', path: '/plataforma/prafrot/solicitacoes', icon: ShieldAlert },
|
{ id: 'g-monitoramento', label: 'Monitoramento', path: '/plataforma/oest-pan/monitoramento', icon: Radio },
|
||||||
{ id: 'g-monitoramento', label: 'Monitoramento', path: '/plataforma/prafrot/monitoramento', icon: Radio },
|
{ id: 'g-status', label: 'Status Frota', path: '/plataforma/oest-pan/status', icon: Activity },
|
||||||
{ id: 'g-status', label: 'Status Frota', path: '/plataforma/prafrot/status', icon: Activity },
|
{ id: 'g-manutencao', label: 'Manutenção', path: '/plataforma/oest-pan/manutencao', icon: Wrench },
|
||||||
{ id: 'g-manutencao', label: 'Manutenção', path: '/plataforma/prafrot/manutencao', icon: Wrench },
|
{ id: 'g-sinistros', label: 'Sinistros', path: '/plataforma/oest-pan/sinistros', icon: AlertTriangle }
|
||||||
{ 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: 'config',
|
id: 'config',
|
||||||
label: 'Configurações',
|
label: 'Configurações',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
path: '/plataforma/prafrot/configuracoes'
|
path: '/plataforma/oest-pan/configuracoes'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -148,7 +145,7 @@ const MenuItem = React.memo(({ item, isSub = false, isCollapsed, expandedItems,
|
||||||
|
|
||||||
MenuItem.displayName = 'MenuItem';
|
MenuItem.displayName = 'MenuItem';
|
||||||
|
|
||||||
export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
|
export const OestPanSidebar = ({ isCollapsed, onToggle }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [expandedItems, setExpandedItems] = useState({ cadastros: true, gerencia: true });
|
const [expandedItems, setExpandedItems] = useState({ cadastros: true, gerencia: true });
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -162,69 +159,20 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredItems = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
// 1. Identificação de Setores (Alinhada com integra_user_prafrot via context)
|
if (!searchTerm) return MENU_ITEMS;
|
||||||
const userSetores = user?.setores || [];
|
|
||||||
|
|
||||||
// Tenta obter especificamente do usuário prafrot caso o contexto global divirja
|
return MENU_ITEMS.filter(item => {
|
||||||
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 => {
|
|
||||||
const matchParent = item.label.toLowerCase().includes(searchTerm.toLowerCase());
|
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())
|
child.label.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
return matchParent || matchChildren;
|
return matchParent || matchChildren;
|
||||||
});
|
});
|
||||||
}, [searchTerm, user]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout('prafrot');
|
logout('auth_oestepan');
|
||||||
window.location.href = '/plataforma/prafrot/login';
|
window.location.href = '/plataforma/oest-pan/login';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -264,12 +212,10 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
|
||||||
|
|
||||||
<footer className="pfs-footer">
|
<footer className="pfs-footer">
|
||||||
<div className="pfs-brand">
|
<div className="pfs-brand">
|
||||||
<div className="pfs-brand-logo">
|
<img src={logoOestePan} alt="OP" className="w-12 h-12 object-contain" />
|
||||||
PF
|
|
||||||
</div>
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="pfs-brand-info">
|
<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>
|
<span className="pfs-app-sub">Fleet Management</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -278,8 +224,8 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
|
||||||
<div className="pfs-user-section">
|
<div className="pfs-user-section">
|
||||||
<div className={cn("pfs-user-card", isCollapsed && "justify-center")}>
|
<div className={cn("pfs-user-card", isCollapsed && "justify-center")}>
|
||||||
<Avatar className="h-8 w-8 border border-[var(--pfs-border)]">
|
<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`} />
|
<AvatarImage src={`https://ui-avatars.com/api/?name=${user?.name || 'User'}&background=f97316&color=141414`} />
|
||||||
<AvatarFallback className="bg-emerald-500 text-zinc-950 font-bold text-xs">
|
<AvatarFallback className="bg-orange-500 text-zinc-950 font-bold text-xs">
|
||||||
{(user?.name || 'U').charAt(0)}
|
{(user?.name || 'U').charAt(0)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
@ -298,3 +244,6 @@ export const PrafrotSidebar = ({ isCollapsed, onToggle }) => {
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 há 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
@ -71,3 +71,6 @@ export const useAvailability = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,3 +73,6 @@ export const useClaims = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,6 @@ export const useDispatcher = create((set) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,6 @@ export const useDrivers = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,3 +105,6 @@ export const useFleetLists = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const notify = (type, title, message) => useFeedbackStore.getState().notify(type
|
||||||
|
|
||||||
export const useMaintenance = create((set, get) => ({
|
export const useMaintenance = create((set, get) => ({
|
||||||
maintenances: [],
|
maintenances: [],
|
||||||
abertoFechadoData: null,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
|
|
@ -18,26 +17,14 @@ export const useMaintenance = create((set, get) => ({
|
||||||
if (type === 'total') {
|
if (type === 'total') {
|
||||||
const data = await prafrotService.getMaintenance();
|
const data = await prafrotService.getMaintenance();
|
||||||
list = Array.isArray(data) ? data : (data.data || []);
|
list = Array.isArray(data) ? data : (data.data || []);
|
||||||
set({ abertoFechadoData: null });
|
} else {
|
||||||
} else if (type === 'aberta' || type === 'fechada' || type === 'aberto_fechado') {
|
|
||||||
const data = await prafrotService.getAbertoFechado();
|
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 abertas = data.abertas || data.aberto || data.abertos || [];
|
||||||
const fechadas = data.fechadas || data.fechado || data.fechados || [];
|
const fechadas = data.fechadas || data.fechado || data.fechados || [];
|
||||||
|
list = [...abertas, ...fechadas];
|
||||||
// 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 || []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = list.map(m => ({ ...m, previsao_entrega: m.previsao_entrega ?? m.previcao_entrega }));
|
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 {
|
try {
|
||||||
await prafrotService.createMaintenance(payload, files);
|
await prafrotService.createMaintenance(payload, files);
|
||||||
notify('success', 'Cadastro Concluído', 'Manutenção registrada com sucesso no sistema.');
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(error);
|
const friendlyMsg = extractFriendlyMessage(error);
|
||||||
|
|
@ -86,7 +73,7 @@ export const useMaintenance = create((set, get) => ({
|
||||||
try {
|
try {
|
||||||
await prafrotService.updateMaintenance(id, payload, files);
|
await prafrotService.updateMaintenance(id, payload, files);
|
||||||
notify('success', 'Atualização!', 'Dados da manutenção atualizados.');
|
notify('success', 'Atualização!', 'Dados da manutenção atualizados.');
|
||||||
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
|
get().fetchMaintenances();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(error);
|
const friendlyMsg = extractFriendlyMessage(error);
|
||||||
|
|
@ -103,7 +90,7 @@ export const useMaintenance = create((set, get) => ({
|
||||||
await prafrotService.updateMaintenanceBatch(ids, status);
|
await prafrotService.updateMaintenanceBatch(ids, status);
|
||||||
|
|
||||||
notify('success', 'Lote Atualizado', `${ids.length} manutenções foram atualizadas.`);
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(error);
|
const friendlyMsg = extractFriendlyMessage(error);
|
||||||
|
|
@ -130,7 +117,7 @@ export const useMaintenance = create((set, get) => ({
|
||||||
try {
|
try {
|
||||||
await prafrotService.deleteMaintenance(id);
|
await prafrotService.deleteMaintenance(id);
|
||||||
notify('success', 'Removido', 'Registro de manutenção excluído.');
|
notify('success', 'Removido', 'Registro de manutenção excluído.');
|
||||||
// get().fetchMaintenances(); // Desativado auto-fetch para evitar poluição de dados
|
get().fetchMaintenances();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(error);
|
const friendlyMsg = extractFriendlyMessage(error);
|
||||||
|
|
@ -146,7 +133,7 @@ export const useMaintenance = create((set, get) => ({
|
||||||
try {
|
try {
|
||||||
await prafrotService.fecharManutencao(id);
|
await prafrotService.fecharManutencao(id);
|
||||||
notify('success', 'Manutenção Fechada', 'Manutenção fechada com sucesso.');
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(error);
|
const friendlyMsg = extractFriendlyMessage(error);
|
||||||
|
|
@ -162,7 +149,7 @@ export const useMaintenance = create((set, get) => ({
|
||||||
try {
|
try {
|
||||||
await prafrotService.abrirManutencao(id);
|
await prafrotService.abrirManutencao(id);
|
||||||
notify('success', 'Manutenção Aberta', 'Manutenção aberta com sucesso.');
|
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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(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
|
// Atualiza também a lista principal 'maintenances' para refletir as mudanças
|
||||||
const abertas = data.abertas || data.aberto || data.abertos || [];
|
const abertas = data.abertas || data.aberto || data.abertos || [];
|
||||||
const fechadas = data.fechadas || data.fechado || data.fechados || [];
|
const fechadas = data.fechadas || data.fechado || data.fechados || [];
|
||||||
|
const normalized = [...abertas, ...fechadas].map(m => ({ ...m, previsao_entrega: m.previsao_entrega ?? m.previcao_entrega }));
|
||||||
// Deduplicação por ID
|
set({ maintenances: normalized });
|
||||||
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 });
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const friendlyMsg = extractFriendlyMessage(error);
|
const friendlyMsg = extractFriendlyMessage(error);
|
||||||
|
|
@ -282,3 +257,6 @@ export const useMaintenance = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,6 @@ export const useMoki = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,6 @@ export const useMonitoring = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,3 +90,6 @@ export const usePrafrotStatistics = () => {
|
||||||
refresh: fetchStatistics
|
refresh: fetchStatistics
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,3 +101,6 @@ export const useStatus = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,3 +87,6 @@ export const useVehicles = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,6 @@ export const useWorkshops = create((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import {
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
|
import { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
|
||||||
import { PrafrotSidebar } from '../components/PrafrotSidebar';
|
import { OestPanSidebar } from '../components/PrafrotSidebar';
|
||||||
import { FeedbackContainer } from '../components/FeedbackNotification';
|
import { FeedbackContainer } from '../components/FeedbackNotification';
|
||||||
|
|
||||||
export const PrafrotLayout = () => {
|
export const OestPanLayout = () => {
|
||||||
useDocumentMetadata('Prafrota', 'prafrot');
|
useDocumentMetadata('Oeste Pan', 'oest-pan');
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
const [isDarkMode, setIsDarkMode] = useState(true);
|
const [isDarkMode, setIsDarkMode] = useState(true);
|
||||||
|
|
||||||
|
|
@ -25,12 +25,12 @@ export const PrafrotLayout = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<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"
|
isDarkMode ? "dark bg-[#141414] text-slate-100" : "bg-slate-50 text-slate-900"
|
||||||
)} style={{ fontFamily: 'var(--font-main)' }}>
|
)}>
|
||||||
<FeedbackContainer />
|
<FeedbackContainer />
|
||||||
{/* New Sidebar Component */}
|
{/* New Sidebar Component */}
|
||||||
<PrafrotSidebar
|
<OestPanSidebar
|
||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
onToggle={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
onToggle={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -50,7 +50,7 @@ export const PrafrotLayout = () => {
|
||||||
{/* <div className="flex items-center gap-4">
|
{/* <div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
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} />
|
<Menu size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -87,3 +87,6 @@ export const PrafrotLayout = () => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import { Route, Routes, Navigate } from 'react-router-dom';
|
import { Route, Routes, Navigate } from 'react-router-dom';
|
||||||
import { PrafrotLayout } from './layout/PrafrotLayout';
|
import { OestPanLayout } from './layout/PrafrotLayout';
|
||||||
import { Zap } from 'lucide-react';
|
import { Zap } from 'lucide-react';
|
||||||
|
|
||||||
// Lazy loading views
|
// Lazy loading views
|
||||||
|
|
@ -17,59 +17,30 @@ const LoginView = lazy(() => import('./views/LoginView'));
|
||||||
const StatisticsView = lazy(() => import('./views/StatisticsView'));
|
const StatisticsView = lazy(() => import('./views/StatisticsView'));
|
||||||
const DispatcherView = lazy(() => import('./views/DispatcherView'));
|
const DispatcherView = lazy(() => import('./views/DispatcherView'));
|
||||||
const ConfigView = lazy(() => import('./views/ConfigView'));
|
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
|
// 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 h-screen w-screen items-center justify-center bg-[#141414]">
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col items-center gap-6">
|
||||||
<div className="relative">
|
<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} />
|
<Zap size={32} className="text-[#1c1c1c]" strokeWidth={2.5} />
|
||||||
</div>
|
</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>
|
||||||
<div className="flex flex-col items-center gap-1">
|
<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>
|
<span className="text-slate-600 text-[8px] font-bold uppercase tracking-widest">Carregando...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
import { useAuthContext } from '@/components/shared/AuthProvider';
|
export const OestPanRoutes = () => {
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<PrafrotLoader />}>
|
<Suspense fallback={<OestPanLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<PrafrotLayout />}>
|
<Route element={<OestPanLayout />}>
|
||||||
<Route path="veiculos" element={<VehiclesView />} />
|
<Route path="veiculos" element={<VehiclesView />} />
|
||||||
<Route path="manutencao" element={<MaintenanceView />} />
|
<Route path="manutencao" element={<MaintenanceView />} />
|
||||||
<Route path="disponibilidade" element={<AvailabilityView />} />
|
<Route path="disponibilidade" element={<AvailabilityView />} />
|
||||||
|
|
@ -82,12 +53,9 @@ export const PrafrotRoutes = () => {
|
||||||
<Route path="estatisticas" element={<StatisticsView />} />
|
<Route path="estatisticas" element={<StatisticsView />} />
|
||||||
<Route path="dispatcher" element={<DispatcherView />} />
|
<Route path="dispatcher" element={<DispatcherView />} />
|
||||||
<Route path="configuracoes" element={<ConfigView />} />
|
<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 index element={<Navigate to="/plataforma/oest-pan/estatisticas" replace />} />
|
||||||
<Route path="*" element={<Navigate to={defaultPath} replace />} />
|
<Route path="*" element={<Navigate to="/plataforma/oest-pan/estatisticas" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
@ -96,3 +64,6 @@ export const PrafrotRoutes = () => {
|
||||||
|
|
||||||
// Export LoginView separately for use in App.jsx
|
// Export LoginView separately for use in App.jsx
|
||||||
export { LoginView };
|
export { LoginView };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,6 @@ export const dispatcherService = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,9 @@ export const prafrotService = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// --- Manutenção ---
|
// --- Manutenção ---
|
||||||
getMaintenance: (status) => handleRequest({
|
getMaintenance: () => handleRequest({
|
||||||
apiFn: async () => {
|
apiFn: async () => {
|
||||||
const url = status
|
const { data } = await api.get(`${ENDPOINTS.MAINTENANCE}/apresentar`);
|
||||||
? `${ENDPOINTS.MAINTENANCE}/apresentar?status=${encodeURIComponent(status)}`
|
|
||||||
: `${ENDPOINTS.MAINTENANCE}/apresentar`;
|
|
||||||
const { data } = await api.get(url);
|
|
||||||
return data;
|
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,6 @@ export const prafrotStatisticsService = {
|
||||||
apiFn: () => api.get(ENDPOINTS.QUANTITATIVO_MANUTENCAO)
|
apiFn: () => api.get(ENDPOINTS.QUANTITATIVO_MANUTENCAO)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -124,3 +124,6 @@ export const extractFriendlyMessage = (error) => {
|
||||||
// Caso contrário, usa a tradução baseada no status
|
// Caso contrário, usa a tradução baseada no status
|
||||||
return getFriendlyErrorMessage(error);
|
return getFriendlyErrorMessage(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -25,7 +25,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -38,7 +38,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
|
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 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 = {
|
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]",
|
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"
|
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="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Visualização de disponibilidade da frota.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar placa..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -138,11 +138,11 @@ export default function AvailabilityView() {
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'iddisponibilidade_frota', width: '80px' },
|
{ header: 'ID', field: 'iddisponibilidade_frota', width: '80px' },
|
||||||
{ header: 'VEÍCULO ID', field: 'idveiculo_frota', width: '100px' },
|
{ 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: 'DATA', field: 'disponibilidade', width: '180px', render: (row) => row.disponibilidade?.split('T')[0] },
|
||||||
{ header: 'STATUS', field: 'status_disponibilidade', width: '150px', render: (row) => (
|
{ 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 ${
|
<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'
|
'bg-red-500/10 text-red-500 border-red-500/20'
|
||||||
}`}>
|
}`}>
|
||||||
{row.status_disponibilidade}
|
{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">
|
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-4 max-h-[75vh] overflow-y-auto custom-scrollbar">
|
||||||
{formData.iddisponibilidade_frota && (
|
{formData.iddisponibilidade_frota && (
|
||||||
<div className="bg-emerald-500/5 p-3 rounded-xl border border-emerald-500/10 mb-2">
|
<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-emerald-500/60 tracking-widest">ID do Registro</p>
|
<p className="text-[10px] uppercase font-bold text-orange-500/60 tracking-widest">ID do Registro</p>
|
||||||
<p className="text-lg font-bold text-emerald-500">{formData.iddisponibilidade_frota}</p>
|
<p className="text-lg font-bold text-orange-500">{formData.iddisponibilidade_frota}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-1.5">
|
<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>
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Placa (Pesquisar)</label>
|
||||||
<input
|
<input
|
||||||
list="veiculos-list"
|
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}
|
value={formData.placa}
|
||||||
onChange={handlePlateChange}
|
onChange={handlePlateChange}
|
||||||
placeholder="Digite ou selecione a placa..."
|
placeholder="Digite ou selecione a placa..."
|
||||||
|
|
@ -206,3 +206,6 @@ export default function AvailabilityView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -27,7 +27,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -40,7 +40,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...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 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 = {
|
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]",
|
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"
|
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="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Gestão de incidentes e movimentações de frota.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar registro..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -157,7 +157,7 @@ export default function ClaimsView() {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idsinistro_devolucao_frota', width: '80px' },
|
{ 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) => (
|
{ 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 ${
|
<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' :
|
row.status === 'Sinistro' ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' :
|
||||||
|
|
@ -238,7 +238,7 @@ export default function ClaimsView() {
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={tab}
|
key={tab}
|
||||||
value={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}
|
{tab}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|
@ -295,7 +295,7 @@ export default function ClaimsView() {
|
||||||
<div className="gap-1.5 flex flex-col">
|
<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>
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações</label>
|
||||||
<textarea
|
<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}
|
value={formData.obs}
|
||||||
onChange={e => setFormData({...formData, obs: e.target.value})}
|
onChange={e => setFormData({...formData, obs: e.target.value})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -375,3 +375,6 @@ export default function ClaimsView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { prafrotService, saveEmailCredentials, getEmailCredentials, updateEmailCredentials } from '../services/prafrotService';
|
import { prafrotService } from '../services/prafrotService';
|
||||||
import { useAuthContext } from '@/components/shared/AuthProvider';
|
|
||||||
import ExcelTable from '../components/ExcelTable';
|
import ExcelTable from '../components/ExcelTable';
|
||||||
import { Plus, Search, Settings, Save, X, Edit, Trash2, Mail, Key, Loader2, ShieldCheck } from 'lucide-react';
|
import { Plus, Search, Settings, Save, X, Edit, Trash2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
@ -14,7 +12,7 @@ const SidebarItem = ({ active, label, onClick }) => (
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`w-full text-left px-4 py-3 text-sm font-medium transition-colors border-l-2 ${
|
className={`w-full text-left px-4 py-3 text-sm font-medium transition-colors border-l-2 ${
|
||||||
active
|
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'
|
: '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 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 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 = {
|
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]",
|
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",
|
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"
|
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">
|
<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>}
|
{label && <label className="text-[11px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1 block">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -60,109 +58,6 @@ export default function ConfigView() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState(null);
|
const [editingItem, setEditingItem] = useState(null);
|
||||||
const [formData, setFormData] = useState({});
|
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
|
// Fetch Config Options on Mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -186,8 +81,6 @@ export default function ConfigView() {
|
||||||
// Fetch Items when Selected Route changes
|
// Fetch Items when Selected Route changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedRoute) return;
|
if (!selectedRoute) return;
|
||||||
// Don't fetch generic items if we are in credentials mode
|
|
||||||
if (selectedRoute === 'credentials') return;
|
|
||||||
|
|
||||||
const loadItems = async () => {
|
const loadItems = async () => {
|
||||||
setLoadingItems(true);
|
setLoadingItems(true);
|
||||||
|
|
@ -241,18 +134,6 @@ export default function ConfigView() {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const handleOpenModal = (item = null) => {
|
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) {
|
if (item) {
|
||||||
setEditingItem(item);
|
setEditingItem(item);
|
||||||
setFormData({ ...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 (
|
return (
|
||||||
<div className="flex h-screen bg-slate-50 dark:bg-[#0f0f0f] overflow-hidden">
|
<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="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]">
|
<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">
|
<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
|
Configurações
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-slate-500 mt-1">Gerencie as listas do sistema</p>
|
<p className="text-xs text-slate-500 mt-1">Gerencie as listas do sistema</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto py-2 custom-scrollbar">
|
<div className="flex-1 overflow-y-auto py-2 custom-scrollbar">
|
||||||
<SidebarItem
|
{Object.entries(configOptions).map(([key, opt]) => (
|
||||||
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]) => (
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
key={key}
|
key={key}
|
||||||
active={selectedRoute === opt.rota}
|
active={selectedRoute === opt.rota}
|
||||||
|
|
@ -382,16 +257,14 @@ export default function ConfigView() {
|
||||||
{/* Header */}
|
{/* 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 className="bg-white dark:bg-[#141414] border-b border-slate-200 dark:border-[#2a2a2a] px-8 py-5 flex justify-between items-center">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Gerenciamento de opções para {selectedLabel?.toLowerCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{selectedRoute !== 'credentials' && (
|
|
||||||
<>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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"
|
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..."
|
placeholder="Pesquisar..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -400,24 +273,17 @@ export default function ConfigView() {
|
||||||
<DarkButton onClick={() => handleOpenModal()}>
|
<DarkButton onClick={() => handleOpenModal()}>
|
||||||
<Plus size={18} /> Novo Item
|
<Plus size={18} /> Novo Item
|
||||||
</DarkButton>
|
</DarkButton>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedRoute === 'credentials' && (
|
|
||||||
<DarkButton onClick={() => handleOpenModal()}>
|
|
||||||
<Plus size={18} /> Nova Credencial
|
|
||||||
</DarkButton>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table or Content */}
|
{/* Table */}
|
||||||
<div className="flex-1 p-8 overflow-hidden">
|
<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">
|
<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 ? (
|
{loadingItems ? (
|
||||||
<div className="flex-1 flex items-center justify-center text-slate-500">
|
<div className="flex-1 flex items-center justify-center text-slate-500">
|
||||||
Carregando...
|
Carregando...
|
||||||
</div>
|
</div>
|
||||||
) : (selectedRoute !== 'credentials' && (
|
) : (
|
||||||
<ExcelTable
|
<ExcelTable
|
||||||
data={filteredItems}
|
data={filteredItems}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
@ -431,21 +297,6 @@ export default function ConfigView() {
|
||||||
return idKey ? row[idKey] : Math.random();
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -456,47 +307,12 @@ 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">
|
<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">
|
<DialogHeader className="border-b border-slate-200 dark:border-[#2a2a2a] pb-4">
|
||||||
<DialogTitle className="text-slate-800 dark:text-white">
|
<DialogTitle className="text-slate-800 dark:text-white">
|
||||||
{selectedRoute === 'credentials'
|
{editingItem ? `Editar ${selectedLabel}` : `Novo Item em ${selectedLabel}`}
|
||||||
? (editingItem ? 'Editar Credencial' : 'Nova Credencial')
|
|
||||||
: (editingItem ? `Editar ${selectedLabel}` : `Novo Item em ${selectedLabel}`)
|
|
||||||
}
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={selectedRoute === 'credentials' ? handleSaveCredentialItem : handleSave} className="py-4 grid grid-cols-1 gap-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
|
<form onSubmit={handleSave} className="py-4 grid grid-cols-1 gap-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
|
||||||
{selectedRoute === 'credentials' ? (
|
{/* Dynamic Form Generation */}
|
||||||
<>
|
|
||||||
<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 => {
|
{Object.keys(formData).map(key => {
|
||||||
// Hide ID fields completely from form
|
// Hide ID fields completely from form
|
||||||
const isId = key.toLowerCase().startsWith('id') || key === 'created_at' || key === 'updated_at';
|
const isId = key.toLowerCase().startsWith('id') || key === 'created_at' || key === 'updated_at';
|
||||||
|
|
@ -530,14 +346,12 @@ export default function ConfigView() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="border-t border-slate-200 dark:border-[#2a2a2a] pt-4">
|
<DialogFooter className="border-t border-slate-200 dark:border-[#2a2a2a] pt-4">
|
||||||
<DarkButton variant="ghost" type="button" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
|
<DarkButton variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
|
||||||
<DarkButton type="submit" onClick={selectedRoute === 'credentials' ? handleSaveCredentialItem : handleSave}>
|
<DarkButton onClick={handleSave}>
|
||||||
{isSavingCreds ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />} Salvar
|
<Save size={16} /> Salvar
|
||||||
</DarkButton>
|
</DarkButton>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
@ -545,3 +359,6 @@ export default function ConfigView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const StatCard = ({ title, value, subtext, icon: Icon, color, trend }) => (
|
||||||
</div>
|
</div>
|
||||||
{trend && (
|
{trend && (
|
||||||
<div className="mt-4 flex items-center gap-2">
|
<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} />}
|
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
||||||
{Math.abs(trend)}%
|
{Math.abs(trend)}%
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -37,7 +37,7 @@ export default function DashboardView() {
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Monitoramento em tempo real da operação.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ export default function DashboardView() {
|
||||||
value="94.2%"
|
value="94.2%"
|
||||||
subtext="Meta: 95%"
|
subtext="Meta: 95%"
|
||||||
icon={CheckCircle2}
|
icon={CheckCircle2}
|
||||||
color="bg-emerald-500"
|
color="bg-orange-500"
|
||||||
trend={1.8}
|
trend={1.8}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
|
|
@ -89,7 +89,7 @@ export default function DashboardView() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{[
|
||||||
{ label: 'SP - Capital (SRJ10)', val: 450, tot: 1240, col: 'bg-blue-600' },
|
{ 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: 'MG - Belo Horizonte', val: 210, tot: 1240, col: 'bg-yellow-500' },
|
||||||
{ label: 'Outras Bases', val: 260, tot: 1240, col: 'bg-slate-600' },
|
{ label: 'Outras Bases', val: 260, tot: 1240, col: 'bg-slate-600' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
|
|
@ -134,3 +134,6 @@ export default function DashboardView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const DispatcherView = () => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col p-4 gap-4">
|
<div className="h-full flex flex-col p-4 gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<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>
|
<p className="text-sm text-slate-500 dark:text-slate-400">Visualização de dados do Dispatcher.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -54,3 +54,6 @@ const DispatcherView = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DispatcherView;
|
export default DispatcherView;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -11,7 +11,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -20,7 +20,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...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 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 = {
|
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]",
|
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"
|
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="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar motorista..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -71,7 +71,7 @@ export default function DriversView() {
|
||||||
<ExcelTable
|
<ExcelTable
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
columns={[
|
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: 'CPF/CNPJ', field: 'CPF_CNPJ_FAVORECIDO', width: '180px' },
|
||||||
{ header: 'TELEFONE', field: 'TELEFONE', width: '150px' },
|
{ header: 'TELEFONE', field: 'TELEFONE', width: '150px' },
|
||||||
{ header: 'ENDEREÇO', field: 'ENDERECO', width: '250px' },
|
{ header: 'ENDEREÇO', field: 'ENDERECO', width: '250px' },
|
||||||
|
|
@ -87,3 +87,6 @@ export default function DriversView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -2,22 +2,22 @@ import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuthContext } from '@/components/shared/AuthProvider';
|
import { useAuthContext } from '@/components/shared/AuthProvider';
|
||||||
import { motion } from 'framer-motion';
|
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 { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
|
||||||
|
import logoOestePan from '@/assets/Img/Util/Oeste_Pan/Logo Oeste Pan.png';
|
||||||
|
|
||||||
export default function LoginView() {
|
export default function LoginView() {
|
||||||
useDocumentMetadata('Login | Prafrota', 'prafrot');
|
useDocumentMetadata('Login | Oeste Pan', 'oest-pan');
|
||||||
const [formData, setFormData] = useState({ email: '', password: '' });
|
const [formData, setFormData] = useState({ email: '', password: '' });
|
||||||
const { login, loading, error } = useAuthContext();
|
const { login, loading, error } = useAuthContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Simulate login for prafrot environment
|
// Login for auth_oestepan environment
|
||||||
const success = await login(formData, 'prafrot');
|
const success = await login(formData, 'auth_oestepan');
|
||||||
// Note: 'prafrot' might need to be added to authorized environments in useAuth or mocked
|
|
||||||
if (success) {
|
if (success) {
|
||||||
navigate('/plataforma/prafrot/estatisticas');
|
navigate('/plataforma/oest-pan/estatisticas');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -27,18 +27,18 @@ export default function LoginView() {
|
||||||
|
|
||||||
{/* Visual Side */}
|
{/* Visual Side */}
|
||||||
<div className="hidden md:flex flex-1 bg-[#1c1c1c] relative items-center justify-center p-12">
|
<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 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="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">
|
<div className="relative z-10 text-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.8, opacity: 0 }}
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
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"
|
className="flex items-center justify-center mx-auto mb-6"
|
||||||
>
|
>
|
||||||
<Truck size={40} className="text-[#141414]" strokeWidth={2.5} />
|
<img src={logoOestePan} alt="Oeste Pan Logo" className="w-64 h-auto drop-shadow-2xl" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<h1 className="text-4xl font-medium text-white tracking-tighter mb-2">PRA <span className="text-emerald-500">FROTA</span></h1>
|
<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>
|
<p className="text-slate-500 font-medium tracking-widest uppercase text-xs">Gestão Inteligente de Ativos</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,7 +47,7 @@ export default function LoginView() {
|
||||||
<div className="flex-1 bg-[#18181b] flex items-center justify-center p-10">
|
<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="w-full max-w-sm space-y-8">
|
||||||
<div className="text-center md:text-left">
|
<div className="text-center md:text-left">
|
||||||
<h2 className="text-2xl font-medium text-white mb-2">Acesso ao Monitoramento</h2>
|
<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>
|
<p className="text-slate-400 text-sm">Entre com suas credenciais de gestor.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -60,8 +60,8 @@ export default function LoginView() {
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={e => setFormData({...formData, email: e.target.value})}
|
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"
|
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@prafrota.com"
|
placeholder="gestor@Oeste_Pan.com"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -75,7 +75,7 @@ export default function LoginView() {
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={e => setFormData({...formData, password: e.target.value})}
|
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"
|
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="••••••••"
|
placeholder="••••••••"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -91,25 +91,15 @@ export default function LoginView() {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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"
|
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} /></>}
|
{loading ? 'Acessando...' : <>Acessar Painel <ArrowRight size={18} /></>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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>
|
</form>
|
||||||
|
|
||||||
<div className="text-center">
|
{/* <div className="text-center">
|
||||||
<span className="text-xs text-slate-600 font-medium">© 2026 Prafrot System</span>
|
<span className="text-xs text-slate-600 font-medium">© 2024 Oeste Pan System v2.0</span>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -117,3 +107,6 @@ export default function LoginView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,9 @@ import { useMaintenance } from '../hooks/useMaintenance';
|
||||||
import { useVehicles } from '../hooks/useVehicles';
|
import { useVehicles } from '../hooks/useVehicles';
|
||||||
import { useWorkshops } from '../hooks/useWorkshops';
|
import { useWorkshops } from '../hooks/useWorkshops';
|
||||||
import { useFleetLists } from '../hooks/useFleetLists';
|
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 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 {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
|
||||||
} from "@/components/ui/dialog";
|
} 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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<input
|
||||||
readOnly={readOnly}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DarkSelect = ({ label, options, value, onChange, disabled, className = '', ...props }) => (
|
const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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-orange-500 focus:ring-1 focus:ring-orange-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-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}
|
|
||||||
>
|
>
|
||||||
<option value="">Selecione...</option>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -101,9 +98,9 @@ const DarkSelect = ({ label, options, value, onChange, disabled, className = '',
|
||||||
);
|
);
|
||||||
|
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
|
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 = {
|
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]",
|
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"
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="decimal"
|
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"
|
placeholder="R$ 0,00"
|
||||||
value={display}
|
value={display}
|
||||||
onFocus={() => { setFocused(true); setLocal(isEmpty ? '' : formatCurrencyForInput(value)); }}
|
onFocus={() => { setFocused(true); setLocal(isEmpty ? '' : formatCurrencyForInput(value)); }}
|
||||||
|
|
@ -169,7 +166,7 @@ const MaintenanceStatusCell = ({ currentStatus, id, options, onUpdate }) => {
|
||||||
value={tempValue}
|
value={tempValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<option value="">Selecione...</option>
|
<option value="">Selecione...</option>
|
||||||
|
|
@ -185,8 +182,8 @@ const MaintenanceStatusCell = ({ currentStatus, id, options, onUpdate }) => {
|
||||||
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
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 ${
|
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?.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 === 'Aprovado' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
|
||||||
currentStatus === 'Concluído' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-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'
|
'bg-slate-700/30 text-slate-400 border-slate-600/30'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -230,7 +227,7 @@ const isInternalFileUrl = (url) => {
|
||||||
|
|
||||||
const DetailSection = ({ title, children }) => (
|
const DetailSection = ({ title, children }) => (
|
||||||
<div className="space-y-4">
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -240,14 +237,14 @@ function FileLink({ label, url }) {
|
||||||
const isInternal = isInternalFileUrl(url);
|
const isInternal = isInternalFileUrl(url);
|
||||||
|
|
||||||
return (
|
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>
|
<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 ? (
|
{isInternal ? (
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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" />
|
<FileText size={14} className="group-hover:scale-110 transition-transform" />
|
||||||
Abrir documento
|
Abrir documento
|
||||||
|
|
@ -257,7 +254,7 @@ function FileLink({ label, url }) {
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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}
|
title={url}
|
||||||
>
|
>
|
||||||
{url}
|
{url}
|
||||||
|
|
@ -289,7 +286,6 @@ export default function MaintenanceView() {
|
||||||
fecharManutencao,
|
fecharManutencao,
|
||||||
abrirManutencao,
|
abrirManutencao,
|
||||||
getAbertoFechado,
|
getAbertoFechado,
|
||||||
abertoFechadoData,
|
|
||||||
getHistoricoCompleto,
|
getHistoricoCompleto,
|
||||||
getHistoricoDetalhado,
|
getHistoricoDetalhado,
|
||||||
getHistoricoEstatisticas,
|
getHistoricoEstatisticas,
|
||||||
|
|
@ -301,7 +297,6 @@ export default function MaintenanceView() {
|
||||||
const { fetchListsConfig, statusManutencaoOptions, motivoAtendimentoOptions, responsaveisOptions, validacaoOptions, aprovacaoOptions } = useFleetLists();
|
const { fetchListsConfig, statusManutencaoOptions, motivoAtendimentoOptions, responsaveisOptions, validacaoOptions, aprovacaoOptions } = useFleetLists();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('total'); // 'total', 'fechada', 'aberta'
|
const [statusFilter, setStatusFilter] = useState('total'); // 'total', 'fechada', 'aberta'
|
||||||
const [viewMode, setViewMode] = useState('maintenance'); // 'maintenance', 'payment_pending'
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState(null);
|
const [editingItem, setEditingItem] = useState(null);
|
||||||
const [statusStats, setStatusStats] = useState([]);
|
const [statusStats, setStatusStats] = useState([]);
|
||||||
|
|
@ -318,13 +313,14 @@ export default function MaintenanceView() {
|
||||||
});
|
});
|
||||||
const [loadingHistorico, setLoadingHistorico] = useState(false);
|
const [loadingHistorico, setLoadingHistorico] = useState(false);
|
||||||
const [topMaintenances, setTopMaintenances] = useState([]);
|
const [topMaintenances, setTopMaintenances] = useState([]);
|
||||||
|
const [abertoFechadoData, setAbertoFechadoData] = useState(null);
|
||||||
|
|
||||||
const initialFormState = {
|
const initialFormState = {
|
||||||
ano_entrada: '', ano_saida: '', atendimento: '', base_frota: '', cidade: '',
|
ano_entrada: '', ano_saida: '', atendimento: '', base_frota: '', cidade: '',
|
||||||
condicao_pagamento: '', data_agendamento: '', data_finalizacao: '', data_parada_veiculo: '',
|
condicao_pagamento: '', data_agendamento: '', data_finalizacao: '', data_parada_veiculo: '',
|
||||||
data_retirada: '', data_solicitacao: '', dif_orcamento: '', endereco_prestador: '',
|
data_retirada: '', data_solicitacao: '', dif_orcamento: '', endereco_prestador: '',
|
||||||
idmanutencao_frota: '', manutencao: '', mes_entrada: '', mes_saida: '', modelo: '',
|
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: '',
|
pdf_orcamento: '', nota_fiscal: '', placa: '', placa_reserva: '', previsao_entrega: '', proprietario: '',
|
||||||
resp_aprovacao: '', responsavel: '',
|
resp_aprovacao: '', responsavel: '',
|
||||||
status: 'Pendente', uf: '', validacao_financeiro: '', qtd_parcelas_condicao_pag: ''
|
status: 'Pendente', uf: '', validacao_financeiro: '', qtd_parcelas_condicao_pag: ''
|
||||||
|
|
@ -356,28 +352,24 @@ export default function MaintenanceView() {
|
||||||
}, [isModalOpen, formData.orcamento_inicial, formData.orcamento_final]);
|
}, [isModalOpen, formData.orcamento_inicial, formData.orcamento_final]);
|
||||||
|
|
||||||
|
|
||||||
// Carrega dados iniciais
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Se estiver no modo manutenções, usamos o filtro de status (aberta/fechada/total)
|
// Carrega a rota inicial baseada no filtro
|
||||||
if (viewMode === 'maintenance') {
|
|
||||||
if (statusFilter === 'total') {
|
if (statusFilter === 'total') {
|
||||||
fetchMaintenances('total');
|
fetchMaintenances('total');
|
||||||
} else {
|
} else {
|
||||||
getAbertoFechado();
|
getAbertoFechado();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Modo Pendências Pagamento: usa filtro nativo do backend
|
|
||||||
fetchMaintenances('Pendente Pagamento');
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchVehicles();
|
fetchVehicles();
|
||||||
fetchWorkshops();
|
fetchWorkshops();
|
||||||
fetchListsConfig();
|
fetchListsConfig();
|
||||||
|
|
||||||
|
// Fetch Status Stats
|
||||||
prafrotStatisticsService.getPlacasPorStatus().then(data => {
|
prafrotStatisticsService.getPlacasPorStatus().then(data => {
|
||||||
if (Array.isArray(data)) setStatusStats(data);
|
if (Array.isArray(data)) setStatusStats(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Top manutenções (visão geral, cards na tela)
|
||||||
getHistoricoTop().then(data => {
|
getHistoricoTop().then(data => {
|
||||||
const lista = Array.isArray(data) ? data : (data?.data || []);
|
const lista = Array.isArray(data) ? data : (data?.data || []);
|
||||||
const comPosicao = lista.map((item, index) => ({
|
const comPosicao = lista.map((item, index) => ({
|
||||||
|
|
@ -385,8 +377,34 @@ export default function MaintenanceView() {
|
||||||
posicao: index + 1,
|
posicao: index + 1,
|
||||||
}));
|
}));
|
||||||
setTopMaintenances(comPosicao);
|
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) => {
|
const getStatusData = (status) => {
|
||||||
return statusStats.find(item => {
|
return statusStats.find(item => {
|
||||||
|
|
@ -477,11 +495,6 @@ export default function MaintenanceView() {
|
||||||
return;
|
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
|
// Limpeza do payload: remove strings vazias que podem causar erro no backend
|
||||||
const payload = {};
|
const payload = {};
|
||||||
Object.keys(formData).forEach(key => {
|
Object.keys(formData).forEach(key => {
|
||||||
|
|
@ -563,50 +576,23 @@ export default function MaintenanceView() {
|
||||||
|
|
||||||
const [selectedIds, setSelectedIds] = useState([]);
|
const [selectedIds, setSelectedIds] = useState([]);
|
||||||
|
|
||||||
const handleStatusUpdate = async (id, newStatus, extraPayload = null) => {
|
const handleStatusUpdate = async (id, newStatus) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (extraPayload) {
|
// Se o item que está sendo editado faz parte da seleção, aplica em massa
|
||||||
// Validação para status Aprovado
|
if (selectedIds.includes(id)) {
|
||||||
if (newStatus === 'Aprovado' && (!extraPayload.validacao_financeiro || extraPayload.validacao_financeiro.trim() === '')) {
|
// Confirmação simples
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmUpdate = window.confirm(`Você selecionou ${selectedIds.length} itens. Deseja atualizar o status de TODOS eles para "${newStatus}"?`);
|
const confirmUpdate = window.confirm(`Você selecionou ${selectedIds.length} itens. Deseja atualizar o status de TODOS eles para "${newStatus}"?`);
|
||||||
|
|
||||||
if (confirmUpdate) {
|
if (confirmUpdate) {
|
||||||
const success = await updateMaintenanceBatch(selectedIds, newStatus);
|
const success = await updateMaintenanceBatch(selectedIds, newStatus);
|
||||||
if (success) {
|
if (success) {
|
||||||
setSelectedIds([]);
|
setSelectedIds([]); // Limpa a seleção
|
||||||
await refreshMaintenances(statusFilter);
|
await refreshMaintenances(statusFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update individual simples (apenas status)
|
// Single Update (using batch route as requested)
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await updateMaintenanceBatch([id], newStatus);
|
await updateMaintenanceBatch([id], newStatus);
|
||||||
await refreshMaintenances(statusFilter);
|
await refreshMaintenances(statusFilter);
|
||||||
}
|
}
|
||||||
|
|
@ -623,10 +609,8 @@ export default function MaintenanceView() {
|
||||||
// - total: usa a rota padrão /manutencao_frota/apresentar
|
// - total: usa a rota padrão /manutencao_frota/apresentar
|
||||||
// - fechada/aberta: usa /manutencao_frota/aberto_fechado/apresentar (grupos separados)
|
// - fechada/aberta: usa /manutencao_frota/aberto_fechado/apresentar (grupos separados)
|
||||||
let baseList = maintenances;
|
let baseList = maintenances;
|
||||||
if (viewMode === 'payment_pending') {
|
|
||||||
// Safety Filter: Garante que apenas Pendente Pagamento apareça neste modo
|
if (statusFilter !== 'total' && abertoFechadoData) {
|
||||||
baseList = maintenances.filter(m => (m.status || '').trim() === 'Pendente Pagamento');
|
|
||||||
} else if (statusFilter !== 'total' && abertoFechadoData) {
|
|
||||||
const lower = {};
|
const lower = {};
|
||||||
Object.keys(abertoFechadoData || {}).forEach((key) => {
|
Object.keys(abertoFechadoData || {}).forEach((key) => {
|
||||||
lower[key.toLowerCase()] = abertoFechadoData[key];
|
lower[key.toLowerCase()] = abertoFechadoData[key];
|
||||||
|
|
@ -657,7 +641,7 @@ export default function MaintenanceView() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}, [maintenances, searchTerm, statusFilter, abertoFechadoData, viewMode]);
|
}, [maintenances, searchTerm, statusFilter, abertoFechadoData]);
|
||||||
|
|
||||||
// Get status options for open/close
|
// Get status options for open/close
|
||||||
const getOpenStatus = () => {
|
const getOpenStatus = () => {
|
||||||
|
|
@ -825,14 +809,14 @@ export default function MaintenanceView() {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Controle de oficinas, orçamentos e agendamentos.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto flex-wrap">
|
<div className="flex items-center gap-3 w-full md:w-auto flex-wrap">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar por ID, placa ou oficina..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -908,7 +892,7 @@ export default function MaintenanceView() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline justify-between mb-3">
|
<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 || '-'}
|
{item.placa || '-'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-slate-500 dark:text-slate-400">
|
<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 */}
|
{/* 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="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 flex-col md:flex-row items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-emerald-500/20 rounded-xl">
|
<div className="p-2 bg-orange-500/20 rounded-xl">
|
||||||
<Database size={20} className="text-emerald-600 dark:text-emerald-400" />
|
<Lock size={20} className="text-orange-600 dark:text-orange-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-300 block mb-1">
|
<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>
|
</label>
|
||||||
<p className="text-[10px] text-slate-500 dark:text-slate-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={e => setStatusFilter(e.target.value)}
|
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]"
|
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="total">📊 Total</option>
|
||||||
<option value="aberta">🔓 Aberta</option>
|
<option value="aberta">🔓 Aberta</option>
|
||||||
<option value="fechada">🔒 Fechada</option>
|
<option value="fechada">🔒 Fechada</option>
|
||||||
</select>
|
</select>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
|
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
|
||||||
{/* Usando Conector Universal (SmartTable) conforme solicitado */}
|
{/* Colunas alinhadas ao contrato: docs/PADROES_ROTAS_APRESENTACAO.md § GET /manutencao_frota/apresentar */}
|
||||||
<React.Suspense fallback={<div className="flex items-center justify-center h-full"><LoadingOverlay isLoading={true} message="Carregando dados..." variant="minimal" /></div>}>
|
<ExcelTable
|
||||||
<SmartTable
|
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onSelectionChange={setSelectedIds}
|
onSelectionChange={setSelectedIds}
|
||||||
|
|
@ -998,7 +947,7 @@ export default function MaintenanceView() {
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idmanutencao_frota', width: '80px' },
|
{ header: 'ID', field: 'idmanutencao_frota', width: '80px' },
|
||||||
{ header: 'ATENDIMENTO', field: 'atendimento', width: '100px' },
|
{ 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: 'PLACA RESERVA', field: 'placa_reserva', width: '100px' },
|
||||||
{ header: 'MODELO', field: 'modelo', width: '110px' },
|
{ header: 'MODELO', field: 'modelo', width: '110px' },
|
||||||
{ header: 'OFICINA', field: 'oficina', width: '160px' },
|
{ header: 'OFICINA', field: 'oficina', width: '160px' },
|
||||||
|
|
@ -1024,14 +973,14 @@ export default function MaintenanceView() {
|
||||||
{ header: 'PREV. ENTREGA', field: 'previsao_entrega', width: '100px' },
|
{ header: 'PREV. ENTREGA', field: 'previsao_entrega', width: '100px' },
|
||||||
{ header: 'DATA FINAL.', field: 'data_finalizacao', width: '100px' },
|
{ header: 'DATA FINAL.', field: 'data_finalizacao', width: '100px' },
|
||||||
{ header: 'DATA RETIRADA', field: 'data_retirada', 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Ç. 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-emerald-600 dark:text-emerald-400', render: (row) => formatCurrency(row.orcamento_final) },
|
{ 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: '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) => (
|
{ header: 'COND. PAG.', field: 'condicao_pagamento', width: '120px', render: (row) => (
|
||||||
<span>
|
<span>
|
||||||
{row.condicao_pagamento}
|
{row.condicao_pagamento}
|
||||||
{row.condicao_pagamento === 'Parcelado' && row.qtd_parcelas_condicao_pag && (
|
{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>
|
</span>
|
||||||
)},
|
)},
|
||||||
|
|
@ -1057,7 +1006,6 @@ export default function MaintenanceView() {
|
||||||
// onEdit={handleOpenModal}
|
// onEdit={handleOpenModal}
|
||||||
// onDelete={(item) => deleteMaintenance(item.idmanutencao_frota)}
|
// onDelete={(item) => deleteMaintenance(item.idmanutencao_frota)}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
|
|
@ -1079,9 +1027,9 @@ export default function MaintenanceView() {
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={tab}
|
key={tab}
|
||||||
value={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>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
@ -1148,7 +1096,7 @@ export default function MaintenanceView() {
|
||||||
label="Dif. Orçamento"
|
label="Dif. Orçamento"
|
||||||
readOnly
|
readOnly
|
||||||
value={formData.dif_orcamento === '' || formData.dif_orcamento == null ? '' : formatCurrency(formData.dif_orcamento)}
|
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>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -1205,9 +1153,9 @@ export default function MaintenanceView() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{valorParcela != null && (
|
{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>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1217,15 +1165,12 @@ export default function MaintenanceView() {
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{editingItem && (
|
|
||||||
<DarkSelect
|
<DarkSelect
|
||||||
label="Validação Financeiro"
|
label="Validação Financeiro"
|
||||||
options={validacaoOptions}
|
options={validacaoOptions}
|
||||||
value={formData.validacao_financeiro}
|
value={formData.validacao_financeiro}
|
||||||
onChange={v => setFormData({...formData, validacao_financeiro: v})}
|
onChange={v => setFormData({...formData, validacao_financeiro: v})}
|
||||||
disabled={true} // Bloqueado estritamente conforme solicitado
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<DarkSelect
|
<DarkSelect
|
||||||
label="Resp. Aprovação"
|
label="Resp. Aprovação"
|
||||||
options={aprovacaoOptions}
|
options={aprovacaoOptions}
|
||||||
|
|
@ -1239,7 +1184,7 @@ export default function MaintenanceView() {
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,application/pdf"
|
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)}
|
onChange={(e) => setFilePdfOrcamento(e.target.files?.[0] ?? null)}
|
||||||
/>
|
/>
|
||||||
{(filePdfOrcamento || formData.pdf_orcamento) && (
|
{(filePdfOrcamento || formData.pdf_orcamento) && (
|
||||||
|
|
@ -1259,7 +1204,7 @@ export default function MaintenanceView() {
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,application/pdf,image/*"
|
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)}
|
onChange={(e) => setFileNotaFiscal(e.target.files?.[0] ?? null)}
|
||||||
/>
|
/>
|
||||||
{(fileNotaFiscal || formData.nota_fiscal) && (
|
{(fileNotaFiscal || formData.nota_fiscal) && (
|
||||||
|
|
@ -1315,7 +1260,7 @@ export default function MaintenanceView() {
|
||||||
<div className="gap-4">
|
<div className="gap-4">
|
||||||
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-400 tracking-wider ml-1">Descrição da Manutenção</label>
|
<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
|
<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}
|
value={formData.manutencao}
|
||||||
onChange={e => setFormData({...formData, manutencao: e.target.value})}
|
onChange={e => setFormData({...formData, manutencao: e.target.value})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1323,55 +1268,15 @@ export default function MaintenanceView() {
|
||||||
<div className="gap-4">
|
<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>
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-400 tracking-wider ml-1">Observações</label>
|
||||||
<textarea
|
<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}
|
value={formData.obs}
|
||||||
onChange={e => setFormData({...formData, obs: e.target.value})}
|
onChange={e => setFormData({...formData, obs: e.target.value})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="bg-slate-50 dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
|
<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="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
|
||||||
<DarkButton type="submit" onClick={handleSubmit}>
|
<DarkButton type="submit" onClick={handleSubmit}>
|
||||||
{editingItem ? 'Salvar Alterações' : 'Criar Solicitação'}
|
{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]">
|
<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 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="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" />
|
<Wrench size={24} className="md:w-7 md:h-7" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|
@ -1396,7 +1301,7 @@ export default function MaintenanceView() {
|
||||||
</DialogTitle>
|
</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">
|
<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">
|
<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>
|
||||||
<span className="hidden md:inline text-slate-400">|</span>
|
<span className="hidden md:inline text-slate-400">|</span>
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
|
|
@ -1410,7 +1315,7 @@ export default function MaintenanceView() {
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${
|
className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${
|
||||||
(selectedMaintenance.manutencao || '').toLowerCase().startsWith('abert')
|
(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')
|
: (selectedMaintenance.manutencao || '').toLowerCase().startsWith('fech')
|
||||||
? 'bg-slate-500/10 text-slate-300 border border-slate-500/40'
|
? 'bg-slate-500/10 text-slate-300 border border-slate-500/40'
|
||||||
: 'bg-slate-700/20 text-slate-300 border border-slate-600/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</>;
|
return <><Lock size={14} /> Fechar Manutenção</>;
|
||||||
})()}
|
})()}
|
||||||
</DarkButton>
|
</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
|
<DarkButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -1450,17 +1366,6 @@ export default function MaintenanceView() {
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
Excluir
|
Excluir
|
||||||
</DarkButton>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
@ -1468,26 +1373,26 @@ export default function MaintenanceView() {
|
||||||
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
|
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
|
||||||
<Tabs defaultValue="detalhes" className="w-full h-full flex flex-col">
|
<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">
|
<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
|
Detalhes
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="completo"
|
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')}
|
onClick={() => handleLoadHistorico('completo')}
|
||||||
>
|
>
|
||||||
Histórico Completo
|
Histórico Completo
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="detalhado"
|
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')}
|
onClick={() => handleLoadHistorico('detalhado')}
|
||||||
>
|
>
|
||||||
Histórico Detalhado
|
Histórico Detalhado
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="estatisticas"
|
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')}
|
onClick={() => handleLoadHistorico('estatisticas')}
|
||||||
>
|
>
|
||||||
Estatísticas
|
Estatísticas
|
||||||
|
|
@ -1495,7 +1400,7 @@ export default function MaintenanceView() {
|
||||||
{(selectedMaintenance?.pdf_orcamento || selectedMaintenance?.nota_fiscal) && (
|
{(selectedMaintenance?.pdf_orcamento || selectedMaintenance?.nota_fiscal) && (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="documentos"
|
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} />
|
<FileText size={14} />
|
||||||
Documentos
|
Documentos
|
||||||
|
|
@ -1518,7 +1423,7 @@ export default function MaintenanceView() {
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-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">Placa</label>
|
<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>
|
||||||
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-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">Placa Reserva</label>
|
<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">
|
<p className="text-sm font-bold text-slate-800 dark:text-white mt-1">
|
||||||
{selectedMaintenance.condicao_pagamento ?? '-'}
|
{selectedMaintenance.condicao_pagamento ?? '-'}
|
||||||
{selectedMaintenance.condicao_pagamento === 'Parcelado' && selectedMaintenance.qtd_parcelas_condicao_pag && (
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1613,20 +1518,20 @@ export default function MaintenanceView() {
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<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">
|
<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>
|
<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>
|
||||||
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-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 Final</label>
|
<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>
|
||||||
<div className="bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl p-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">Dif. Orçamento</label>
|
<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>
|
</div>
|
||||||
{selectedMaintenance.condicao_pagamento === 'Parcelado' && selectedMaintenance.qtd_parcelas_condicao_pag && (
|
{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">
|
<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-emerald-600 dark:text-emerald-400 tracking-wider">Valor da Parcela</label>
|
<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-emerald-600 dark:text-emerald-400 mt-1 font-mono">
|
<p className="text-sm font-bold text-orange-600 dark:text-orange-400 mt-1 font-mono">
|
||||||
{(() => {
|
{(() => {
|
||||||
const total = parseCurrency(selectedMaintenance.orcamento_final);
|
const total = parseCurrency(selectedMaintenance.orcamento_final);
|
||||||
const parcelas = parseInt(selectedMaintenance.qtd_parcelas_condicao_pag, 10);
|
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>
|
<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>
|
<p className="text-sm text-slate-800 dark:text-white mt-1 whitespace-pre-wrap">{selectedMaintenance.obs ?? '-'}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1723,9 +1620,9 @@ export default function MaintenanceView() {
|
||||||
{historicoData.completo.total_manutencoes ?? 0}
|
{historicoData.completo.total_manutencoes ?? 0}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-xl p-4">
|
<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-emerald-700 dark:text-emerald-300 tracking-wider">Concluídas</span>
|
<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-emerald-700 dark:text-emerald-300 mt-1">
|
<p className="text-2xl font-bold text-orange-700 dark:text-orange-300 mt-1">
|
||||||
{historicoData.completo.manutencoes_concluidas ?? 0}
|
{historicoData.completo.manutencoes_concluidas ?? 0}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1781,11 +1678,11 @@ export default function MaintenanceView() {
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{historicoData.detalhado.length > 0 ? (
|
{historicoData.detalhado.length > 0 ? (
|
||||||
<SmartTable
|
<ExcelTable
|
||||||
data={historicoData.detalhado}
|
data={historicoData.detalhado}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID Manutenção', field: 'idmanutencao_frota', width: '120px' },
|
{ 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: 'Modelo', field: 'modelo', width: '140px' },
|
||||||
{ header: 'Oficina', field: 'oficina', width: '180px' },
|
{ header: 'Oficina', field: 'oficina', width: '180px' },
|
||||||
{ header: 'Motivo', field: 'motivo_atendimento', width: '140px' },
|
{ 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">
|
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">
|
||||||
Valor Total Gasto
|
Valor Total Gasto
|
||||||
</span>
|
</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)}
|
{formatCurrency(historicoData.estatisticas.valor_total_gasto)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">
|
||||||
Valor Médio Manutenção
|
Valor Médio Manutenção
|
||||||
</span>
|
</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)}
|
{formatCurrency(historicoData.estatisticas.valor_medio_manutencao)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1890,11 +1787,11 @@ export default function MaintenanceView() {
|
||||||
<TabsContent value="documentos" className="flex-1 overflow-hidden flex flex-col m-0 pt-2">
|
<TabsContent value="documentos" className="flex-1 overflow-hidden flex flex-col m-0 pt-2">
|
||||||
{selectedMaintenance && (selectedMaintenance.pdf_orcamento || selectedMaintenance.nota_fiscal) && (
|
{selectedMaintenance && (selectedMaintenance.pdf_orcamento || selectedMaintenance.nota_fiscal) && (
|
||||||
<Tabs defaultValue={selectedMaintenance.pdf_orcamento ? "pdf" : "nota"} className="w-full h-full flex flex-col">
|
<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 && (
|
{selectedMaintenance.pdf_orcamento && (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="pdf"
|
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
|
PDF Orçamento
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|
@ -1902,7 +1799,7 @@ export default function MaintenanceView() {
|
||||||
{selectedMaintenance.nota_fiscal && (
|
{selectedMaintenance.nota_fiscal && (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="nota"
|
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
|
Nota Fiscal
|
||||||
</TabsTrigger>
|
</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="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="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">
|
<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>
|
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Visualização: PDF Orçamento</span>
|
||||||
</div>
|
</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>
|
<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="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="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">
|
<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>
|
<span className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider">Visualização: Nota Fiscal</span>
|
||||||
</div>
|
</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>
|
<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}
|
Veículos: {selectedStatusRecords?.title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
|
<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>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</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="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">
|
<div className="h-full w-full rounded-2xl border border-slate-200 dark:border-[#2a2a2a] overflow-hidden shadow-sm">
|
||||||
<SmartTable
|
<ExcelTable
|
||||||
data={selectedStatusRecords?.records || []}
|
data={selectedStatusRecords?.records || []}
|
||||||
columns={[
|
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: 'Modelo', field: 'modelo', width: '150px' },
|
||||||
{ header: 'Unidade', field: 'base', width: '120px' },
|
{ header: 'Unidade', field: 'base', width: '120px' },
|
||||||
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
||||||
|
|
@ -2034,3 +1931,6 @@ export default function MaintenanceView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -11,7 +11,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,7 +23,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -36,7 +36,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
|
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 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 = {
|
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]",
|
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"
|
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="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 className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Inspeções e vistorias realizadas.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar checklist..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -132,12 +132,12 @@ export default function MokiView() {
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idmoki_frota', width: '80px' },
|
{ 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: '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 CHECKLIST', field: 'status_checklist', width: '150px' },
|
||||||
{ header: 'STATUS', field: 'status', width: '120px', render: (row) => (
|
{ 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 ${
|
<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 === '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'
|
'bg-amber-500/10 text-amber-500 border-amber-500/20'
|
||||||
}`}>
|
}`}>
|
||||||
{row.status || 'Pendente'}
|
{row.status || 'Pendente'}
|
||||||
|
|
@ -175,7 +175,7 @@ export default function MokiView() {
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="px-6 py-4 space-y-4 max-h-[70vh] overflow-y-auto custom-scrollbar">
|
<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
|
<DarkInput
|
||||||
type="number"
|
type="number"
|
||||||
label="ID do Moki (Obrigatório)"
|
label="ID do Moki (Obrigatório)"
|
||||||
|
|
@ -184,7 +184,7 @@ export default function MokiView() {
|
||||||
required
|
required
|
||||||
placeholder="Ex: 123456"
|
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>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|
@ -219,3 +219,6 @@ export default function MokiView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -39,7 +39,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...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 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 = {
|
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]",
|
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"
|
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) => {
|
const handleOpenModal = (item = null) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
setEditingItem(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 {
|
} else {
|
||||||
setEditingItem(null);
|
setEditingItem(null);
|
||||||
setFormData(initialFormState);
|
setFormData(initialFormState);
|
||||||
|
|
@ -143,14 +153,14 @@ export default function MonitoringView() {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Acompanhamento em tempo real e rastreabilidade.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar unidade..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -165,7 +175,7 @@ export default function MonitoringView() {
|
||||||
{/* Status Highlights */}
|
{/* Status Highlights */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<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' },
|
{ 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) => (
|
].map((card, i) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -195,7 +205,7 @@ export default function MonitoringView() {
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idmonitoramento_frota', width: '80px' },
|
{ header: 'ID', field: 'idmonitoramento_frota', width: '80px' },
|
||||||
{ header: 'ID EXTERNO', field: 'id_externo', width: '120px' },
|
{ 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: 'UNIDADE', field: 'unidade', width: '150px' },
|
||||||
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
|
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
|
||||||
{ header: 'DATA CARGA', field: 'data_carga', width: '150px' },
|
{ header: 'DATA CARGA', field: 'data_carga', width: '150px' },
|
||||||
|
|
@ -246,7 +256,7 @@ export default function MonitoringView() {
|
||||||
valueKey="NOME_FAVORECIDO"
|
valueKey="NOME_FAVORECIDO"
|
||||||
placeholder="Buscar motorista..."
|
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>
|
||||||
<div className="grid grid-cols-1 gap-4 pb-4">
|
<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})} />
|
<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">
|
<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>
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações de Monitoramento</label>
|
||||||
<textarea
|
<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}
|
value={formData.obs}
|
||||||
onChange={e => setFormData({...formData, obs: e.target.value})}
|
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">
|
<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]">
|
<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="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} />
|
<Truck size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -282,7 +292,7 @@ export default function MonitoringView() {
|
||||||
Veículos: {selectedStatusRecords?.title}
|
Veículos: {selectedStatusRecords?.title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
|
<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>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -293,7 +303,7 @@ export default function MonitoringView() {
|
||||||
<ExcelTable
|
<ExcelTable
|
||||||
data={selectedStatusRecords?.records || []}
|
data={selectedStatusRecords?.records || []}
|
||||||
columns={[
|
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: 'Modelo', field: 'modelo', width: '150px' },
|
||||||
{ header: 'Unidade', field: 'base', width: '120px' },
|
{ header: 'Unidade', field: 'base', width: '120px' },
|
||||||
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
||||||
|
|
@ -324,3 +334,6 @@ export default function MonitoringView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -113,7 +113,7 @@ export default function StatisticsView() {
|
||||||
label: 'Frota Total',
|
label: 'Frota Total',
|
||||||
value: totalVeiculos.toLocaleString(),
|
value: totalVeiculos.toLocaleString(),
|
||||||
icon: <Truck />,
|
icon: <Truck />,
|
||||||
color: 'bg-emerald-500/10 text-emerald-600'
|
color: 'bg-orange-500/10 text-orange-600'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Em Manutenção (Aberta)',
|
label: 'Em Manutenção (Aberta)',
|
||||||
|
|
@ -152,8 +152,8 @@ export default function StatisticsView() {
|
||||||
{/* Header com Design Premium */}
|
{/* Header com Design Premium */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-3xl font-medium text-white tracking-tight">
|
<h1 className="text-4xl font-bold text-slate-900 dark:text-white tracking-tight">
|
||||||
Dashboard <span className="text-emerald-500">Estatístico</span>
|
Dashboard <span className="text-orange-500">Estatístico</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-slate-500 dark:text-slate-400 font-medium text-lg">
|
<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.
|
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" : ""} />
|
<RefreshCw size={18} className={statsLoading ? "animate-spin" : ""} />
|
||||||
Sincronizar Dados
|
Sincronizar Dados
|
||||||
</button>
|
</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} />
|
<Download size={18} />
|
||||||
Exportar BI
|
Exportar BI
|
||||||
</button> */}
|
</button> */}
|
||||||
|
|
@ -176,7 +176,7 @@ export default function StatisticsView() {
|
||||||
|
|
||||||
{/* KPI Section */}
|
{/* KPI Section */}
|
||||||
<div className="relative">
|
<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} />
|
<StatsGrid stats={stats} />
|
||||||
</div>
|
</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">
|
<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]">
|
<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="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} />
|
<Truck size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -275,7 +275,7 @@ export default function StatisticsView() {
|
||||||
Veículos: {selectedStatus?.status}
|
Veículos: {selectedStatus?.status}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
|
<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>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -286,7 +286,7 @@ export default function StatisticsView() {
|
||||||
<ExcelTable
|
<ExcelTable
|
||||||
data={selectedStatus?.registros ? JSON.parse(selectedStatus.registros).filter(r => r !== null) : []}
|
data={selectedStatus?.registros ? JSON.parse(selectedStatus.registros).filter(r => r !== null) : []}
|
||||||
columns={[
|
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: 'Modelo', field: 'modelo', width: '150px' },
|
||||||
{ header: 'Unidade', field: 'base', width: '120px' },
|
{ header: 'Unidade', field: 'base', width: '120px' },
|
||||||
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
{ 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]">
|
<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">
|
<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="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} />
|
<LineChartIcon size={20} />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl font-bold">Fluxo de Manutenção Mensal</CardTitle>
|
<CardTitle className="text-xl font-bold">Fluxo de Manutenção Mensal</CardTitle>
|
||||||
|
|
@ -391,7 +391,7 @@ export default function StatisticsView() {
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Regional</span>
|
<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>
|
</div>
|
||||||
<CardTitle className="text-lg font-bold">Veículos por Base</CardTitle>
|
<CardTitle className="text-lg font-bold">Veículos por Base</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -490,7 +490,7 @@ export default function StatisticsView() {
|
||||||
<CardTitle className="text-xl font-bold">Disponibilidade Detalhada</CardTitle>
|
<CardTitle className="text-xl font-bold">Disponibilidade Detalhada</CardTitle>
|
||||||
<p className="text-sm text-slate-500">Breakdown por status de operação.</p>
|
<p className="text-sm text-slate-500">Breakdown por status de operação.</p>
|
||||||
</div>
|
</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} />
|
<Gauge size={24} strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -500,7 +500,7 @@ export default function StatisticsView() {
|
||||||
<div key={i} className="group cursor-default">
|
<div key={i} className="group cursor-default">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex flex-col min-w-0">
|
<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>
|
<span className="text-[10px] font-bold text-slate-400 tracking-widest uppercase truncate">{item.disponibilidade}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 shrink-0">
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
|
@ -512,7 +512,7 @@ export default function StatisticsView() {
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-2 bg-slate-100 dark:bg-[#2a2a2a] rounded-full overflow-hidden">
|
<div className="w-full h-2 bg-slate-100 dark:bg-[#2a2a2a] rounded-full overflow-hidden">
|
||||||
<div
|
<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}%` }}
|
style={{ width: `${(item.total / (totalVeiculos || 1)) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -556,3 +556,6 @@ export default function StatisticsView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -27,7 +27,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -40,7 +40,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
|
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 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 = {
|
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]",
|
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"
|
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}
|
value={tempValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<option value="">Selecione...</option>
|
<option value="">Selecione...</option>
|
||||||
|
|
@ -94,7 +94,7 @@ const StatusCell = ({ currentStatus, id, options, onUpdate }) => {
|
||||||
<div
|
<div
|
||||||
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
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 ${
|
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' :
|
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'
|
'bg-slate-500/10 text-slate-500 border-slate-500/20'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -203,7 +203,7 @@ export default function StatusView() {
|
||||||
{/* ... Header ... */}
|
{/* ... Header ... */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Histórico de movimentação e estados dos veículos.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<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">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar placa..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -232,7 +232,7 @@ export default function StatusView() {
|
||||||
rowKey="idstatus_frota"
|
rowKey="idstatus_frota"
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idstatus_frota', width: '80px' },
|
{ 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) => (
|
{ header: 'STATUS', field: 'status_frota', width: '150px', render: (row) => (
|
||||||
<StatusCell
|
<StatusCell
|
||||||
currentStatus={row.status_frota}
|
currentStatus={row.status_frota}
|
||||||
|
|
@ -327,7 +327,7 @@ export default function StatusView() {
|
||||||
<div className="gap-4 pb-4">
|
<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>
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações / Motivo</label>
|
||||||
<textarea
|
<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}
|
value={formData.obs}
|
||||||
onChange={e => setFormData({...formData, obs: e.target.value})}
|
onChange={e => setFormData({...formData, obs: e.target.value})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -344,3 +344,6 @@ export default function StatusView() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,3 +106,6 @@ const TableDebug = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TableDebug;
|
export default TableDebug;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -33,7 +33,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -46,7 +46,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...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 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 = {
|
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]",
|
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"
|
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}
|
value={tempValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
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
|
onClick={(e) => e.stopPropagation()} // Prevent row selection if applied
|
||||||
>
|
>
|
||||||
<option value="">Selecione...</option>
|
<option value="">Selecione...</option>
|
||||||
|
|
@ -102,7 +102,7 @@ const StatusCell = ({ currentStatus, idVehicle, options, onUpdate }) => {
|
||||||
<div
|
<div
|
||||||
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
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 ${
|
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'
|
'bg-slate-500/10 text-slate-500 border-slate-500/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -234,14 +234,14 @@ export default function VehiclesView() {
|
||||||
{/* Header Actions */}
|
{/* Header Actions */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Gerencie os veículos cadastrados na plataforma.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar placa..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -273,10 +273,10 @@ export default function VehiclesView() {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => handleStatusClick('Em Operação')}
|
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="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} />
|
<CheckCircle size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
@ -297,7 +297,7 @@ export default function VehiclesView() {
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idveiculo_frota', width: '80px' },
|
{ 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: 'MODELO', field: 'modelo', width: '140px' },
|
||||||
{ header: ' FABRICANTE', field: 'fabricante', width: '120px' },
|
{ header: ' FABRICANTE', field: 'fabricante', width: '120px' },
|
||||||
{ header: 'CORES', field: 'cor', width: '100px' },
|
{ header: 'CORES', field: 'cor', width: '100px' },
|
||||||
|
|
@ -347,7 +347,7 @@ export default function VehiclesView() {
|
||||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
<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">
|
<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]">
|
<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'}
|
{editingVehicle ? `Editando: ${formData.placa}` : 'Cadastro de Novo Veículo'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500 dark:text-stone-500">
|
<DialogDescription className="text-slate-500 dark:text-stone-500">
|
||||||
|
|
@ -366,7 +366,7 @@ export default function VehiclesView() {
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={tab}
|
key={tab}
|
||||||
value={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}
|
{tab === 'tecnico' ? 'RASTREADOR' : tab}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|
@ -472,7 +472,7 @@ export default function VehiclesView() {
|
||||||
<div className="gap-4 pt-2">
|
<div className="gap-4 pt-2">
|
||||||
<label className="text-[10px] uppercase font-bold text-slate-500 tracking-wider ml-1">Observações Gerais</label>
|
<label className="text-[10px] uppercase font-bold text-slate-500 tracking-wider ml-1">Observações Gerais</label>
|
||||||
<textarea
|
<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}
|
value={formData.observacoes}
|
||||||
onChange={e => setFormData({...formData, observacoes: e.target.value})}
|
onChange={e => setFormData({...formData, observacoes: e.target.value})}
|
||||||
/>
|
/>
|
||||||
|
|
@ -498,7 +498,7 @@ export default function VehiclesView() {
|
||||||
<Truck size={28} />
|
<Truck size={28} />
|
||||||
</div>
|
</div>
|
||||||
<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}
|
Veículos: {selectedStatusRecords?.title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
|
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
|
||||||
|
|
@ -513,7 +513,7 @@ export default function VehiclesView() {
|
||||||
<ExcelTable
|
<ExcelTable
|
||||||
data={selectedStatusRecords?.records || []}
|
data={selectedStatusRecords?.records || []}
|
||||||
columns={[
|
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: 'Modelo', field: 'modelo', width: '150px' },
|
||||||
{ header: 'Unidade', field: 'base', width: '120px' },
|
{ header: 'Unidade', field: 'base', width: '120px' },
|
||||||
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
{ header: 'Motorista', field: 'motorista', width: '180px' },
|
||||||
|
|
@ -543,3 +543,6 @@ export default function VehiclesView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const DarkInput = ({ label, ...props }) => (
|
||||||
<div className="space-y-1.5">
|
<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>}
|
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
|
||||||
<input
|
<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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,7 +23,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => onChange(e.target.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>
|
<option value="">Selecione...</option>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -36,7 +36,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
|
||||||
const DarkButton = ({ children, variant = 'primary', className = '', ...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 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 = {
|
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]",
|
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"
|
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="space-y-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-slate-500 text-sm">Rede credenciada e prestadores de serviços.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 w-full md:w-auto">
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
<div className="relative flex-1 md:flex-none">
|
<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} />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
|
||||||
<input
|
<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..."
|
placeholder="Buscar por nome, CNPJ ou cidade..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
|
@ -134,7 +134,7 @@ export default function WorkshopsView() {
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'ID', field: 'idoficinas_frota', width: '80px' },
|
{ header: 'ID', field: 'idoficinas_frota', width: '80px' },
|
||||||
{ header: 'Cod. Estab.', field: 'cod_estabelecimento', width: '100px' },
|
{ 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: 'Razão Social', field: 'razao_social', width: '250px' },
|
||||||
{ header: 'CNPJ', field: 'cnpj', width: '150px' },
|
{ header: 'CNPJ', field: 'cnpj', width: '150px' },
|
||||||
{ header: 'Tipo', field: 'tipo_estabelecimento', width: '180px' },
|
{ 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">
|
<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">
|
<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">
|
<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'}
|
{editingItem ? 'Editar Oficina' : 'Cadastrar Nova Oficina'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-slate-500 dark:text-slate-400">
|
<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">
|
<form onSubmit={handleSubmit} className="px-6 py-6 space-y-6 max-h-[70vh] overflow-y-auto custom-scrollbar">
|
||||||
{/* Seção: Identificação */}
|
{/* Seção: Identificação */}
|
||||||
<div className="space-y-4">
|
<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="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="md:col-span-1">
|
<div className="md:col-span-1">
|
||||||
<DarkInput label="Cod. Estabelecimento" value={formData.cod_estabelecimento} onChange={e => setFormData({...formData, cod_estabelecimento: e.target.value})} />
|
<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 */}
|
{/* Seção: Localização e Contato */}
|
||||||
<div className="space-y-4">
|
<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})} />
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
|
@ -216,3 +216,6 @@ export default function WorkshopsView() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue