Cliente/OestPan #2

Manually merged
daivid.alves merged 2 commits from Cliente/OestPan into frontend_React 2026-03-02 17:15:10 +00:00
96 changed files with 597 additions and 14380 deletions
Showing only changes of commit c872473589 - Show all commits

View File

@ -1,138 +0,0 @@
import React from 'react';
import { X, Filter, Trash2, Check, ChevronDown } from 'lucide-react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
const AdvancedFiltersModal = ({ isOpen, onClose, onApply, options = {}, initialFilters = {}, config = [] }) => {
const [filters, setFilters] = React.useState(initialFilters);
// Sync with parent when modal opens or initialFilters change
React.useEffect(() => {
if (isOpen) {
setFilters(initialFilters);
}
}, [isOpen, initialFilters]);
const handleChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleApply = () => {
onApply(filters);
};
const handleClear = () => {
setFilters({});
};
const activeCount = Object.values(filters).filter(Boolean).length;
return (
<DialogPrimitive.Root open={isOpen} onOpenChange={onClose}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 bg-black/70 z-50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<DialogPrimitive.Content className="fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-0 bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl md:w-full overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 dark:border-[#2a2a2a] bg-white dark:bg-[#1c1c1c]">
<div className="flex items-center gap-3">
<Filter className="w-5 h-5 text-gray-500 dark:text-stone-400 fill-gray-500/20" />
<DialogPrimitive.Title asChild>
<h2 className="text-lg font-bold text-gray-800 dark:text-white uppercase tracking-tight">Filtros Avançados</h2>
</DialogPrimitive.Title>
<DialogPrimitive.Description className="sr-only">
Selecione os filtros detalhados para refinar a busca de veículos.
</DialogPrimitive.Description>
{activeCount > 0 && (
<span className="bg-blue-500 text-white text-xs font-bold px-3 py-1 rounded-full">
{activeCount} filtros ativos
</span>
)}
</div>
<div className="flex items-center gap-4">
<button
onClick={handleClear}
className="flex items-center gap-1 text-rose-500 hover:text-rose-600 font-bold text-[10px] uppercase tracking-wider transition-colors"
>
Limpar Tudo
</button>
<DialogPrimitive.Close className="rounded-full p-1.5 hover:bg-gray-100 dark:hover:bg-[#2a2a2a] transition-colors">
<X className="w-5 h-5 text-gray-400 dark:text-stone-500" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
</div>
{/* Body - Scrollable Form */}
<div className="px-6 py-6 max-h-[60vh] overflow-y-auto space-y-5 bg-white dark:bg-[#1c1c1c] custom-scrollbar">
{config.map((filter) => {
const label = filter.label || filter.field.replace(/_/g, ' ').toUpperCase();
if (filter.type === 'select') {
return (
<div key={filter.field} className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-500 tracking-widest ml-1">{label}</label>
<div className="relative w-full">
<select
className="w-full appearance-none border border-gray-200 dark:border-[#2a2a2a] rounded-xl py-3 pl-4 pr-10 bg-gray-50 dark:bg-[#141414] hover:border-gray-300 dark:hover:border-[#333] transition-all text-left text-sm text-gray-700 dark:text-stone-300 font-bold focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20"
value={filters[filter.field] || ''}
onChange={(e) => handleChange(filter.field, e.target.value)}
>
<option value="" className="text-gray-400">TODOS</option>
{options[filter.field]?.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
<ChevronDown className="w-4 h-4 text-gray-400 dark:text-stone-600" />
</div>
</div>
</div>
);
}
if (filter.type === 'text') {
return (
<div key={filter.field} className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-stone-500 tracking-widest ml-1">{label}</label>
<input
type="text"
placeholder={filter.placeholder || "Digite para buscar..."}
className="w-full border border-gray-200 dark:border-[#2a2a2a] rounded-xl py-3 px-4 bg-gray-50 dark:bg-[#141414] text-sm text-gray-700 dark:text-white font-bold focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 placeholder:text-gray-400 dark:placeholder:text-stone-700 transition-all"
value={filters[filter.field] || ''}
onChange={(e) => handleChange(filter.field, e.target.value)}
/>
</div>
);
}
return null;
})}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-100 dark:border-[#2a2a2a] bg-gray-50/50 dark:bg-[#1c1c1c] rounded-b-xl">
<button
onClick={onClose}
className="px-6 py-2.5 text-xs font-bold uppercase tracking-widest text-gray-500 dark:text-stone-400 hover:text-gray-700 dark:hover:text-white transition-all"
>
Cancelar
</button>
<button
onClick={handleApply}
className="px-8 py-2.5 text-xs font-bold uppercase tracking-widest text-gray-900 bg-[#fbbf24] hover:bg-[#f59e0b] rounded-xl flex items-center gap-2 transition-all shadow-lg shadow-amber-500/10 active:scale-95"
>
<Check className="w-4 h-4" />
Aplicar Filtros
</button>
</div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
};
export default AdvancedFiltersModal;

View File

@ -1,123 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { Search, ChevronDown, Check } from 'lucide-react';
const AutocompleteInput = ({
label,
value,
onChange,
options = [],
onSelect,
placeholder = "Pesquisar...",
displayKey = "label", // key to show in list
valueKey = "value", // key to use for value
searchKeys = [], // keys to search in. if empty, searches displayKey
className = "",
disabled = false,
required = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(value || '');
const wrapperRef = useRef(null);
useEffect(() => {
setSearchTerm(value || '');
}, [value]);
useEffect(() => {
const handleClickOutside = (event) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const filteredOptions = options.filter(option => {
if (!searchTerm) return true;
const term = searchTerm.toLowerCase();
// If searchKeys provided, search in those
if (searchKeys.length > 0) {
return searchKeys.some(key => {
const val = option[key];
return val && String(val).toLowerCase().includes(term);
});
}
// Otherwise search in displayKey or direct string
const label = typeof option === 'object' ? option[displayKey] : option;
return String(label).toLowerCase().includes(term);
});
const handleSelect = (option) => {
const displayVal = typeof option === 'object' ? option[displayKey] : option;
const actualVal = typeof option === 'object' ? (option[valueKey] || option[displayKey]) : option;
setSearchTerm(displayVal);
onChange(displayVal); // Update the input value
if (onSelect) onSelect(option); // Trigger specific selection logic
setIsOpen(false);
};
return (
<div className={`space-y-1.5 relative ${className}`} ref={wrapperRef}>
{label && (
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">
{label} {required && <span className="text-red-500">*</span>}
</label>
)}
<div className="relative">
<input
type="text"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-3 pr-8 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
placeholder={placeholder}
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
onChange(e.target.value);
setIsOpen(true);
}}
onFocus={() => setIsOpen(true)}
disabled={disabled}
/>
<ChevronDown
size={16}
className={`absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
{isOpen && filteredOptions.length > 0 && (
<ul className="absolute z-50 w-full mt-1 bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#333] rounded-lg shadow-xl max-h-60 overflow-auto custom-scrollbar">
{filteredOptions.map((option, index) => {
const display = typeof option === 'object' ? option[displayKey] : option;
const isSelected = display === value;
return (
<li
key={index}
className={`px-3 py-2 text-sm cursor-pointer flex items-center justify-between hover:bg-slate-100 dark:hover:bg-[#2a2a2a] ${
isSelected ? 'bg-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)}
>
<span>{display}</span>
{isSelected && <Check size={14} />}
</li>
);
})}
</ul>
)}
{isOpen && filteredOptions.length === 0 && searchTerm && (
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#333] rounded-lg shadow-xl p-3 text-sm text-slate-500 text-center">
Sem resultados
</div>
)}
</div>
</div>
);
};
export default AutocompleteInput;

View File

@ -1,459 +0,0 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Edit2, Trash2, Filter, Columns, Layers, Download, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import AdvancedFiltersModal from './AdvancedFiltersModal';
const ExcelTable = ({
data,
columns,
filterDefs = [],
onEdit,
onDelete,
onRowClick,
loading = false,
pageSize: initialPageSize = 50,
selectedIds = [],
onSelectionChange,
rowKey = 'id'
}) => {
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({});
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
// Pagination State
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(initialPageSize);
// ... (existing useMemo logic) ...
// Helper para lidar com seleção
// 1. Extract Unique Values...
const options = React.useMemo(() => {
const opts = {};
const getUnique = (key) => [...new Set(data.map(item => item[key]).filter(Boolean))].sort();
filterDefs.forEach(def => {
if (def.type === 'select') {
opts[def.field] = getUnique(def.field);
}
});
return opts;
}, [data, filterDefs]);
// 2. Process Data...
const processedData = React.useMemo(() => {
let result = [...data];
// Filter
Object.keys(filters).forEach(key => {
if (filters[key]) {
result = result.filter(item => {
const itemValue = String(item[key] || '').toLowerCase();
const filterValue = String(filters[key]).toLowerCase();
return itemValue.includes(filterValue);
});
}
});
// Sort
if (sortConfig.key) {
result.sort((a, b) => {
const valA = a[sortConfig.key] || '';
const valB = b[sortConfig.key] || '';
if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1;
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return result;
}, [data, filters, sortConfig]);
// 3. Paginate...
const paginatedData = React.useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return processedData.slice(startIndex, startIndex + pageSize);
}, [processedData, currentPage, pageSize]);
// Reset page...
useEffect(() => {
setCurrentPage(1);
}, [filters, sortConfig, pageSize]);
// Helper para lidar com seleção (Movido para cá para ter acesso a processedData)
const handleSelectAll = (e) => {
if (!onSelectionChange) return;
if (e.target.checked) {
const allIds = processedData.map(item => item[rowKey]);
onSelectionChange(allIds);
} else {
onSelectionChange([]);
}
};
const handleSelectRow = (id) => {
if (!onSelectionChange) return;
const newSelected = selectedIds.includes(id)
? selectedIds.filter(x => x !== id)
: [...selectedIds, id];
onSelectionChange(newSelected);
};
const isAllSelected = processedData && processedData.length > 0 && processedData.every(item => selectedIds.includes(item[rowKey]));
const isIndeterminate = processedData && processedData.some(item => selectedIds.includes(item[rowKey])) && !isAllSelected;
const totalPages = Math.ceil(processedData.length / pageSize);
const handleSort = (key) => {
setSortConfig(current => ({
key,
direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc'
}));
};
const handleApplyFilters = (newFilters) => {
setFilters(newFilters);
setShowFilters(false);
};
return (
<>
<div className="bg-white dark:bg-[#1b1b1b] border border-slate-200 dark:border-[#2a2a2a] rounded-xl flex flex-col h-full w-full max-w-full min-w-0 text-xs font-sans antialiased text-slate-700 dark:text-[#e0e0e0] relative overflow-hidden transition-colors">
{/* Loading Overlay */}
{loading && (
<div className="absolute inset-0 z-50 bg-white/80 dark:bg-[#1b1b1b]/80 backdrop-blur-sm flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="w-12 h-12 border-4 border-slate-200 dark:border-[#252525] border-t-orange-500 rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
</div>
</div>
<span className="text-orange-500 font-bold uppercase tracking-widest text-[10px] animate-pulse">Carregando Dados...</span>
</div>
</div>
)}
{/* 1. Top Toolbar */}
<div className="flex items-center justify-between px-3 py-2 bg-white dark:bg-[#1b1b1b] border-b border-slate-200 dark:border-[#2a2a2a]">
{/* Left Actions */}
<div className="flex items-center gap-3">
{/* Filter Button */}
<button
onClick={() => setShowFilters(true)}
className={`flex items-center gap-1.5 px-3 py-1.5 border rounded font-semibold transition-colors ${
Object.keys(filters).length > 0
? 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50'
: 'bg-slate-50 dark:bg-[#252525] hover:bg-slate-100 dark:hover:bg-[#333] border-slate-200 dark:border-[#333] text-slate-600 dark:text-stone-300'
}`}
>
<Filter size={14} strokeWidth={2.5} />
<span className="uppercase tracking-wide text-[11px]">
{Object.keys(filters).length > 0 ? `${Object.keys(filters).length} Filtros` : 'Filtros Avançados'}
</span>
</button>
{/* Actions Button (Green Highlight) - Commented as requested
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-orange-50 dark:bg-[#1f2824] hover:bg-orange-100 dark:hover:bg-[#25302b] border border-orange-200 dark:border-[#2b3832] rounded text-orange-600 dark:text-orange-400 font-semibold transition-colors">
<span className="uppercase tracking-wide text-[11px]">Ações</span>
<span className="bg-orange-100 dark:bg-[#15201b] text-orange-600 px-1 rounded text-[9px]">
<span className="w-2 h-2 rounded-full bg-orange-500 inline-block"></span>
</span>
</button>
*/}
</div>
{/* Right Actions - Commented out as requested
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-[#252525] border border-transparent hover:border-[#333] rounded text-stone-400 transition-colors">
<Columns size={12} />
<span className="text-[10px] font-semibold">Colunas</span>
</button>
<button className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-[#252525] border border-transparent hover:border-[#333] rounded text-stone-400 transition-colors">
<Layers size={12} />
<span className="text-[10px] font-semibold">Agrupar</span>
</button>
<button className="flex items-center gap-1.5 px-3 py-1.5 hover:bg-[#252525] border border-transparent hover:border-[#333] rounded text-stone-400 transition-colors">
<Download size={12} />
<span className="text-[10px] font-semibold">Exportar</span>
</button>
</div>
*/}
</div>
{/* 2. Table Header (The Complex Part) */}
<div className="overflow-x-auto overflow-y-auto flex-1 relative custom-scrollbar w-full min-w-0">
<style>{`
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f8fafc;
}
.dark .custom-scrollbar {
scrollbar-color: #334155 #0f172a;
}
.custom-scrollbar::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f8fafc;
}
.dark .custom-scrollbar::-webkit-scrollbar-track {
background: #0f172a;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border: 2px solid #f8fafc;
border-radius: 6px;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: #334155;
border: 2px solid #0f172a;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #475569;
}
.custom-scrollbar::-webkit-scrollbar-corner {
background: #f8fafc;
}
.dark .custom-scrollbar::-webkit-scrollbar-corner {
background: #0f172a;
}
`}</style>
<table className="border-collapse table-fixed custom-scrollbar w-full min-w-full">
<thead className="sticky top-0 z-20 bg-slate-50 dark:bg-[#1b1b1b] shadow-lg shadow-black/5 dark:shadow-black/20">
<tr className="border-b border-slate-200 dark:border-[#333]">
{/* Checkbox Header */}
<th className="sticky left-0 z-30 bg-slate-50 dark:bg-[#1b1b1b] w-[40px] min-w-[40px] h-[40px] border-r border-[#e2e8f0] dark:border-[#333]">
<div className="flex items-center justify-center h-full w-full">
<input
type="checkbox"
checked={isAllSelected}
ref={input => { if (input) input.indeterminate = isIndeterminate; }}
onChange={handleSelectAll}
disabled={!onSelectionChange}
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-orange-500 checked:border-orange-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
/>
</div>
</th>
{onEdit || onDelete ? (
<th className="sticky left-[40px] z-30 bg-slate-50 dark:bg-[#1b1b1b] w-[60px] min-w-[60px] h-[40px] border-r border-slate-200 dark:border-[#333] px-2 text-left">
<div className="flex items-center h-full w-full uppercase font-bold text-slate-500 dark:text-[#e0e0e0] tracking-wider text-[11px]">
Ações
</div>
</th>
) : null}
{/* Dynamic Columns */}
{columns.map((col, idx) => (
<th
key={idx}
onClick={() => handleSort(col.field)}
className="h-[40px] border-r border-slate-200 dark:border-[#333] bg-slate-50 dark:bg-[#1b1b1b] px-3 relative group hover:bg-slate-100 dark:hover:bg-[#222] transition-colors cursor-pointer select-none"
style={{ width: col.width, minWidth: col.width }}
>
<div className="flex items-center justify-between h-full w-full">
<div className="flex items-center gap-1.5">
<span className={`uppercase font-bold tracking-wider text-[11px] ${sortConfig.key === col.field ? 'text-orange-600 dark:text-orange-500' : 'text-slate-500 dark:text-[#e0e0e0]'}`}>
{col.header}
</span>
</div>
{/* Sort/Menu Icons */}
<div className={`flex flex-col gap-0.5 ${sortConfig.key === col.field ? 'opacity-100' : 'opacity-40 group-hover:opacity-100'}`}>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'asc' ? 'text-orange-600 dark:text-orange-500' : 'text-slate-400 dark:text-stone-400'} rotate-180`}>
<path d="M4 4L0 0H8L4 4Z" />
</svg>
<svg width="8" height="4" viewBox="0 0 8 4" fill="currentColor" className={`${sortConfig.key === col.field && sortConfig.direction === 'desc' ? 'text-orange-600 dark:text-orange-500' : 'text-slate-400 dark:text-stone-400'}`}>
<path d="M4 4L0 0H8L4 4Z" />
</svg>
</div>
</div>
{/* Column Resizer Handle */}
<div className="absolute right-0 top-0 bottom-0 w-[4px] cursor-col-resize hover:bg-orange-500/50 z-10 translate-x-1/2" onClick={(e) => e.stopPropagation()} />
</th>
))}
{/* Spacer for scrollbar */}
<th className="bg-slate-50 dark:bg-[#1b1b1b] border-b border-slate-200 dark:border-[#333]"></th>
</tr>
</thead>
{/* 3. Table Body - Displaying Paginated Data */}
<tbody className="bg-white dark:bg-[#151515]">
{paginatedData.map((row, idx) => (
<tr
key={idx}
onClick={(e) => {
// Não disparar onRowClick se clicar em checkbox, botões ou links
if (e.target.closest('input[type="checkbox"]') ||
e.target.closest('button') ||
e.target.closest('a')) {
return;
}
onRowClick && onRowClick(row);
}}
className={`h-[44px] border-b border-slate-100 dark:border-[#252525] hover:bg-slate-50 dark:hover:bg-[#202020] transition-colors group text-sm ${idx % 2 === 0 ? 'bg-white dark:bg-[#151515]' : 'bg-slate-50/50 dark:bg-[#181818]'} ${idx === 0 ? '!bg-slate-100 dark:!bg-[#2b2b2b]' : ''} ${onRowClick ? 'cursor-pointer' : ''}`}>
{/* Checkbox Cell */}
<td className={`sticky left-0 z-10 border-r border-slate-100 dark:border-[#252525] px-0 text-center ${idx % 2 === 0 ? 'bg-white dark:bg-[#151515]' : 'bg-slate-50/50 dark:bg-[#181818]'} ${idx === 0 ? '!bg-slate-100 dark:!bg-[#2b2b2b]' : ''} group-hover:bg-slate-50 dark:group-hover:bg-[#202020]`}>
<div className="flex items-center justify-center h-full w-full">
<input
type="checkbox"
checked={selectedIds.includes(row[rowKey])}
onChange={() => handleSelectRow(row[rowKey])}
disabled={!onSelectionChange}
className="appearance-none w-3.5 h-3.5 border-2 border-slate-300 dark:border-slate-600 rounded-sm bg-white dark:bg-[#2a2a2a] checked:bg-orange-500 checked:border-orange-500 transition-all cursor-pointer relative checked:bg-[url('data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M2.5%207L5.5%2010L11.5%204%22%20stroke%3D%22%23FFF%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E')] bg-center bg-no-repeat bg-[length:70%]"
/>
</div>
</td>
{/* Actions Cell */}
{(onEdit || onDelete) ? (
<td className={`sticky left-[40px] z-10 border-r border-slate-100 dark:border-[#252525] px-0 text-center ${idx % 2 === 0 ? 'bg-white dark:bg-[#151515]' : 'bg-slate-50/50 dark:bg-[#181818]'} ${idx === 0 ? '!bg-slate-100 dark:!bg-[#2b2b2b]' : ''} group-hover:bg-slate-50 dark:group-hover:bg-[#202020]`}>
<div className="flex items-center justify-center h-full w-full gap-2">
<button
onClick={() => onEdit && onEdit(row)}
className="text-slate-500 dark:text-stone-500 hover:text-orange-600 dark:hover:text-stone-300 transition-colors p-1"
>
<Edit2 size={16} strokeWidth={2} />
</button>
{onDelete && (
<button
onClick={() => onDelete(row)}
className="text-slate-500 dark:text-stone-500 hover:text-rose-600 dark:hover:text-rose-400 transition-colors p-1"
>
<Trash2 size={16} strokeWidth={2} />
</button>
)}
</div>
</td>
) : null}
{/* Data Cells */}
{columns.map((col, cIdx) => (
<td key={cIdx} className="border-r border-slate-100 dark:border-[#252525] px-3 whitespace-nowrap overflow-hidden text-ellipsis text-slate-700 dark:text-stone-300">
{col.render ? col.render(row) : (
<span className={col.className}>{row[col.field] || '-'}</span>
)}
</td>
))}
<td className=""></td>
</tr>
))}
{/* Empty rows filler if needed */}
{!loading && paginatedData.length === 0 && (
<tr>
<td colSpan={columns.length + 3} className="h-24 text-center text-stone-500 italic">
{filters && Object.keys(filters).length > 0 ? 'Nenhum registro encontrado para os filtros selecionados.' : 'Nenhum registro disponível.'}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* 4. Footer Steps (Pagination Controls) */}
<div className="bg-slate-50 dark:bg-[#1b1b1b] border-t border-slate-200 dark:border-[#333] h-[40px] flex items-center justify-between px-2 overflow-hidden select-none">
{/* Left: Totals */}
<div className="flex h-full items-center gap-4">
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[120px]">
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-orange-500 rounded-r-sm"></div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Total:</span>
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{processedData.length}</span>
<div className="absolute right-0 top-2 bottom-2 w-[1px] bg-slate-200 dark:bg-[#333]"></div>
</div>
<div className="flex items-center gap-2 px-4 h-full relative group min-w-[100px]">
<div className="absolute left-0 top-2 bottom-2 w-[3px] bg-orange-500 rounded-r-sm opacity-0 group-hover:opacity-100 transition-opacity"></div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Página:</span>
<span className="text-sm font-bold text-slate-800 dark:text-stone-200">{currentPage} / {totalPages || 1}</span>
</div>
</div>
{/* Right: Pagination Controls */}
<div className="flex items-center gap-1 pr-2">
<div className="text-[11px] text-slate-500 mr-3 hidden md:block">
Exibindo <span className="font-bold text-slate-700 dark:text-stone-300">{Math.min((currentPage - 1) * pageSize + 1, processedData.length)} a {Math.min(currentPage * pageSize, processedData.length)}</span>
</div>
{/* First Page */}
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronsLeft size={14} />
</button>
{/* Prev Page */}
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={14} />
</button>
{/* Manual Page Buttons (Simple logic) */}
<div className="flex gap-1 mx-1">
<button className="w-7 h-7 flex items-center justify-center rounded bg-orange-500 text-white dark:text-black font-bold text-[11px] transition-colors shadow-lg shadow-orange-500/10">
{currentPage}
</button>
</div>
{/* Next Page */}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight size={14} />
</button>
{/* Last Page */}
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="w-7 h-7 flex items-center justify-center rounded bg-white dark:bg-[#151515] border border-slate-200 dark:border-[#333] hover:border-orange-500 dark:hover:border-[#555] text-slate-500 dark:text-stone-400 group disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronsRight size={14} />
</button>
</div>
</div>
</div>
<AdvancedFiltersModal
isOpen={showFilters}
onClose={() => setShowFilters(false)}
onApply={handleApplyFilters}
options={options}
initialFilters={filters}
config={filterDefs}
/>
</>
);
};
export default ExcelTable;

View File

@ -1,115 +0,0 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle2, AlertCircle, Info, XCircle, X } from 'lucide-react';
import { create } from 'zustand';
import { extractFriendlyMessage } from '../utils/errorMessages';
// Store to manage global feedback state
export const useFeedbackStore = create((set) => ({
notifications: [],
notify: (type, title, message, duration = 5000) => {
const id = Date.now();
set((state) => ({
notifications: [...state.notifications, { id, type, title, message, duration }]
}));
setTimeout(() => {
set((state) => ({
notifications: state.notifications.filter(n => n.id !== id)
}));
}, duration);
},
remove: (id) => set((state) => ({
notifications: state.notifications.filter(n => n.id !== id)
}))
}));
const icons = {
success: <CheckCircle2 className="w-6 h-6 text-orange-500" />,
error: <XCircle className="w-6 h-6 text-rose-500" />,
warning: <AlertCircle className="w-6 h-6 text-amber-500" />,
info: <Info className="w-6 h-6 text-blue-500" />
};
const colors = {
success: "border-orange-500/20 bg-orange-50/50 dark:bg-orange-500/10",
error: "border-rose-500/20 bg-rose-50/50 dark:bg-rose-500/10",
warning: "border-amber-500/20 bg-amber-50/50 dark:bg-amber-500/10",
info: "border-blue-500/20 bg-blue-50/50 dark:bg-blue-500/10"
};
export const FeedbackContainer = () => {
const { notifications, remove } = useFeedbackStore();
return (
<div className="fixed top-6 right-6 z-[9999] flex flex-col gap-3 pointer-events-none">
<AnimatePresence>
{notifications.map((n) => (
<motion.div
key={n.id}
initial={{ opacity: 0, x: 50, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
className={`pointer-events-auto min-w-[320px] max-w-[400px] p-4 rounded-2xl border backdrop-blur-md shadow-2xl flex gap-4 relative overflow-hidden group ${colors[n.type]}`}
>
{/* Background Glow */}
<div className={`absolute -right-4 -top-4 w-24 h-24 blur-3xl opacity-20 group-hover:opacity-40 transition-opacity ${n.type === 'success' ? 'bg-orange-500' : n.type === 'error' ? 'bg-rose-500' : 'bg-blue-500'}`}></div>
<div className="flex-shrink-0 mt-0.5">
{icons[n.type]}
</div>
<div className="flex-1 flex flex-col gap-1 pr-6">
<h4 className="text-sm font-bold uppercase tracking-tight text-slate-800 dark:text-white">
{n.title}
</h4>
<p className="text-xs font-medium text-slate-500 dark:text-stone-400 leading-relaxed">
{n.message}
</p>
</div>
<button
onClick={() => remove(n.id)}
className="absolute top-3 right-3 p-1 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
>
<X className="w-3 h-3 text-slate-400" />
</button>
{/* Progress Bar (Auto-expire) */}
<motion.div
initial={{ scaleX: 1 }}
animate={{ scaleX: 0 }}
transition={{ duration: Math.min((n.duration || 5000) / 1000, 30), ease: "linear" }}
className={`absolute bottom-0 left-0 right-0 h-1 origin-left ${n.type === 'success' ? 'bg-orange-500' : n.type === 'error' ? 'bg-rose-500' : 'bg-blue-500'}`}
/>
</motion.div>
))}
</AnimatePresence>
</div>
);
};
export const useFeedback = () => {
const notify = useFeedbackStore(s => s.notify);
return {
success: (title, message) => notify('success', title || 'Sucesso!', message),
error: (title, message) => notify('error', title || 'Ops!', message),
warning: (title, message) => notify('warning', title || 'Atenção!', message),
info: (title, message) => notify('info', title || 'Informação', message),
// Friendly back-end error parser
handleBackendError: (error) => {
console.error(error);
// Usa a função utilitária para obter mensagem amigável
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Ops!', friendlyMsg);
},
notifyFields: (fields) => {
notify('warning', 'Campos Obrigatórios', `Por favor, preencha: ${fields.join(', ')}`);
}
};
};

View File

@ -1,174 +0,0 @@
import React, { useState } from 'react';
import { FinesCard } from './FinesCard';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { RefreshCcw } from 'lucide-react';
const FinesCardDebug = (initialProps) => {
const sampleData = [
{ name: 'Excesso de Velocidade', value: 450.00, color: '#ef4444' }, // Red
{ name: 'Estacionamento Irregular', value: 250.00, color: '#f59e0b' }, // Amber
{ name: 'Documentação', value: 150.00, color: '#3b82f6' }, // Blue
];
const totalSampleValue = sampleData.reduce((acc, curr) => acc + curr.value, 0);
// Helper to generate consistent data
const generateChartData = (period, current, previous) => {
if (period === '30d') {
return [
{ name: 'Sem 1', value: previous * 0.8 },
{ name: 'Sem 2', value: previous * 1.1 },
{ name: 'Sem 3', value: previous * 0.9 },
{ name: 'Atual', value: current, active: true } // Highlighted
];
}
if (period === '60d') {
return [
{ name: 'Mês -1', value: previous * 0.9 },
{ name: 'Anterior', value: previous },
{ name: 'Atual', value: current, active: true }
];
}
if (period === '120d') {
return [
{ name: 'Mês -3', value: previous * 1.2 },
{ name: 'Mês -2', value: previous * 0.8 },
{ name: 'Anterior', value: previous },
{ name: 'Atual', value: current, active: true }
];
}
return [];
};
const [state, setState] = useState(() => {
const initialCurrent = 850;
const initialPrev = 1100;
return {
currentValue: initialCurrent,
currentCount: 3,
previousValue: initialPrev,
previousCount: 6,
hasData: true,
isLoading: false,
period: '30d',
data: sampleData,
monthlyData: generateChartData('30d', initialCurrent, initialPrev),
...initialProps
};
});
const toggleData = () => {
setState(prev => ({
...prev,
hasData: !prev.hasData,
currentValue: !prev.hasData ? totalSampleValue : 0,
currentCount: !prev.hasData ? 3 : 0
}));
};
const handlePeriodChange = (p) => {
console.log('Period Changed:', p);
// Simulate API logic: distinct values for each period
let newCurrent, newPrev, newCount;
if (p === '30d') { newCurrent = 850; newPrev = 1100; newCount = 3; }
else if (p === '60d') { newCurrent = 1450; newPrev = 1200; newCount = 5; }
else if (p === '120d') { newCurrent = 2100; newPrev = 1800; newCount = 8; }
else { newCurrent = 0; newPrev = 0; newCount = 0; }
setState(prev => ({
...prev,
period: p, // Update the period in state
currentValue: newCurrent,
previousValue: newPrev,
currentCount: newCount,
monthlyData: generateChartData(p, newCurrent, newPrev)
}));
};
const updatePrevValue = (e) => {
const val = Number(e.target.value);
setState(prev => ({
...prev,
previousValue: val,
monthlyData: generateChartData(prev.period, prev.currentValue, val)
}));
};
return (
<div className="flex gap-8 items-start w-full">
{/* Preview Area */}
<div className="w-[350px] h-[450px] shrink-0">
<FinesCard
currentValue={state.currentValue}
currentCount={state.currentCount}
previousValue={state.previousValue}
previousCount={state.previousCount}
data={state.hasData ? sampleData : []}
monthlyData={state.hasData ? state.monthlyData : []}
isLoading={state.isLoading}
onPeriodChange={handlePeriodChange}
/>
</div>
{/* Controls Area */}
<div className="flex-1 bg-slate-50 dark:bg-slate-900/50 p-6 rounded-xl border border-slate-200 dark:border-white/10 space-y-6">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-wider">Debug Controls</h4>
<Button variant="ghost" size="icon" onClick={() => setState(prev => ({ ...prev, isLoading: !prev.isLoading }))} className={state.isLoading ? "text-orange-500" : "text-slate-500 hover:text-slate-900 dark:hover:text-slate-300"}>
<RefreshCcw className={state.isLoading ? "animate-spin" : ""} size={16} />
</Button>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-white dark:bg-white/5 border border-slate-200 dark:border-transparent rounded-lg shadow-sm dark:shadow-none">
<div className="space-y-1">
<Label className="text-slate-700 dark:text-slate-300">Simular Dados</Label>
<p className="text-[10px] text-slate-500">Alterna entre estado vazio e populado</p>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={state.hasData}
onChange={toggleData}
className="w-5 h-5 rounded border-slate-300 dark:border-slate-600 bg-slate-100 dark:bg-slate-700 text-orange-500 focus:ring-orange-500 focus:ring-offset-white dark:focus:ring-offset-slate-900"
/>
</div>
</div>
<div className="space-y-3 pt-2">
<div className="flex justify-between text-xs text-slate-500 dark:text-slate-400">
<span>Valor Mês Anterior</span>
<span className="font-mono text-slate-900 dark:text-white">R$ {state.previousValue}</span>
</div>
<input
type="range"
min="0"
max="5000"
step="100"
value={state.previousValue}
onChange={updatePrevValue}
className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-orange-500"
/>
</div>
</div>
<div className="p-4 rounded-lg bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/5 text-[10px] font-mono text-slate-500 dark:text-slate-400 space-y-2">
<div><span className="text-purple-600 dark:text-purple-400">Current Value:</span> {state.currentValue}</div>
<div><span className="text-purple-600 dark:text-purple-400">Reduction:</span> {state.previousValue > 0 ? ((state.currentValue - state.previousValue) / state.previousValue * 100).toFixed(1) : 0}%</div>
</div>
</div>
</div>
);
};
export default FinesCardDebug;

View File

@ -1,267 +0,0 @@
import React, { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { PieChart as PieChartIcon, Ban, AlertCircle, TrendingDown, TrendingUp, Minus, BarChart3, PieChart as PieIcon } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, BarChart, XAxis, YAxis, CartesianGrid, Bar } from 'recharts';
import { cn } from '@/lib/utils';
/**
* FinesCard (Componente de Multas)
*
* Exibe o resumo de multas do mês atual comparado com o mês anterior.
* Permite alternar entre visualização por Categoria (Pie) e Histórico Mensal (Bar).
*/
export const FinesCard = ({
currentDate = 'Mês',
currentValue = 0,
currentCount = 0,
previousValue = 0,
previousCount = 0,
data = [], // [{ name: 'Type', value: 100, color: '#f00' }]
monthlyData = [], // [{ name: 'Jan', value: 500, count: 2 }]
isLoading = false,
onPeriodChange,
className
}) => {
const [activeView, setActiveView] = useState('category'); // 'category' | 'history'
const [period, setPeriod] = useState('30d'); // '30d' | '60d' | '120d'
const handlePeriodChange = (p) => {
setPeriod(p);
if (onPeriodChange) onPeriodChange(p);
};
// Formatters
const formatCurrency = (val) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(val);
const formatCompact = (val) => Intl.NumberFormat('pt-BR', { notation: "compact", maximumFractionDigits: 1 }).format(val);
// Calculation for reduction/increase
const hasPrevious = previousValue > 0;
const diff = currentValue - previousValue;
const percentage = hasPrevious ? (diff / previousValue) * 100 : 0;
const isReduction = percentage < 0;
const isIncrease = percentage > 0;
const trendColor = isReduction ? 'text-orange-500' : isIncrease ? 'text-rose-500' : 'text-slate-500';
const badgeColor = isReduction ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' : isIncrease ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' : 'bg-slate-500/10 text-slate-500 border-slate-500/20';
const TrendIcon = isReduction ? TrendingDown : isIncrease ? TrendingUp : Minus;
const isEmpty = !data || data.length === 0 || currentValue === 0;
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
const pData = payload[0];
return (
<div className="bg-white dark:bg-[#18181b] border border-slate-200 dark:border-white/10 p-3 rounded-lg shadow-xl min-w-[120px]">
<p className="text-[10px] uppercase font-bold text-slate-500 mb-1 tracking-wider">{label || pData.name}</p>
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: pData.payload.color || '#3b82f6' }} />
<span className="text-sm font-bold text-slate-900 dark:text-white font-mono">
{formatCurrency(pData.value)}
</span>
</div>
</div>
);
}
return null;
};
return (
<Card className={cn("flex flex-col h-full bg-white dark:bg-[#18181b] border-slate-200 dark:border-white/10 shadow-sm dark:shadow-xl overflow-hidden relative", className)}>
{/* Header */}
<div className="p-5 pb-2 shrink-0">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-amber-500/10 flex items-center justify-center">
<PieChartIcon className="text-amber-600 dark:text-amber-500" size={20} />
</div>
<div>
<h3 className="font-bold text-lg text-slate-900 dark:text-white leading-none">Multas</h3>
<span className="text-[10px] text-slate-500 font-medium uppercase tracking-wider">Visão Geral</span>
</div>
</div>
<div className="text-right">
<div className={cn("text-2xl font-bold tracking-tight leading-none", currentValue > 0 ? "text-slate-900 dark:text-white" : "text-orange-600 dark:text-orange-500")}>
{formatCurrency(currentValue)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 font-medium mt-1">{currentCount} registros</div>
</div>
</div>
{/* Comparison Box */}
<div className="bg-slate-50 dark:bg-white/5 rounded-xl p-3 flex items-center justify-between border border-slate-200 dark:border-white/5 mb-4 relative overflow-hidden">
<div className="flex flex-col relative z-10">
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Mês Anterior</span>
<div className="flex items-baseline gap-1.5 mt-0.5">
<span className="text-sm font-bold text-slate-700 dark:text-slate-200">{hasPrevious ? `R$ ${formatCompact(previousValue)}` : '-'}</span>
<span className="text-[10px] text-slate-400 dark:text-slate-500 italic">({previousCount} registros)</span>
</div>
</div>
{hasPrevious && (
<Badge variant="outline" className={cn("gap-1.5 pl-2 pr-3 py-1 relative z-10", badgeColor)}>
<TrendIcon size={12} />
{Math.abs(percentage).toFixed(1)}%
</Badge>
)}
{/* Background gradient for decoration */}
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-white/50 dark:from-white/5 to-transparent pointer-events-none" />
</div>
{/* Filters & Toggles Row */}
{!isEmpty && !isLoading && (
<div className="flex items-center gap-2">
{/* Time Period Selector */}
<div className="flex-1 bg-slate-100 dark:bg-black/20 p-1 rounded-lg border border-slate-200 dark:border-white/5 flex">
{['30d', '60d', '120d'].map((p) => (
<button
key={p}
onClick={() => handlePeriodChange(p)}
className={cn(
"flex-1 py-1.5 rounded-md text-[10px] uppercase font-bold tracking-wider transition-all",
period === p
? "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"
)}
>
{p.replace('d', ' dias')}
</button>
))}
</div>
{/* View Type Toggle (Icon Only) */}
<div className="flex bg-slate-100 dark:bg-black/20 p-1 rounded-lg border border-slate-200 dark:border-white/5">
<button
onClick={() => setActiveView('category')}
className={cn(
"p-1.5 rounded-md transition-all",
activeView === 'category'
? "bg-white dark:bg-white/10 text-amber-600 dark:text-amber-500 shadow-sm ring-1 ring-black/5 dark:ring-0"
: "text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300"
)}
title="Agrupado"
>
<PieIcon size={14} />
</button>
<button
onClick={() => setActiveView('history')}
className={cn(
"p-1.5 rounded-md transition-all",
activeView === 'history'
? "bg-white dark:bg-white/10 text-blue-600 dark:text-blue-500 shadow-sm ring-1 ring-black/5 dark:ring-0"
: "text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300"
)}
title="Histórico"
>
<BarChart3 size={14} />
</button>
</div>
</div>
)}
</div>
<div className="flex-1 min-h-[px] relative px-4 pb-4 pt-2 flex flex-col">
{isLoading ? (
<div className="flex-1 flex flex-col items-center justify-center gap-4 animate-pulse">
<div className="w-24 h-24 rounded-full bg-slate-200 dark:bg-white/5" />
<div className="h-4 w-32 bg-slate-200 dark:bg-white/5 rounded" />
</div>
) : isEmpty ? (
// Empty State
<div className="flex-1 flex flex-col items-center justify-center text-center gap-4 py-8">
<div className="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-2">
<PieChartIcon className="text-slate-300 dark:text-slate-600 opacity-50" size={32} />
</div>
<div>
<h4 className="font-bold text-slate-900 dark:text-white">Sem dados</h4>
<p className="text-[10px] text-slate-500 leading-relaxed max-w-[150px] mx-auto mt-1">
Nenhum registro encontrado para o período.
</p>
</div>
</div>
) : (
// Charts Area
<div className="flex-1 w-full h-full min-h-0 flex flex-col">
{activeView === 'category' ? (
<>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius="60%"
outerRadius="80%"
paddingAngle={5}
dataKey="value"
stroke="none"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-2 justify-center pb-2 shrink-0">
{data.map((item, i) => (
<div key={i} className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }} />
<span className="text-[10px] text-slate-500 dark:text-slate-400 font-medium">{item.name}</span>
</div>
))}
</div>
</>
) : (
<div className="flex-1 min-h-0 pt-2">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(148, 163, 184, 0.1)" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 10 }}
dy={10}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 10 }}
tickFormatter={(val) => `R$${val/1000}k`}
/>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(148, 163, 184, 0.1)' }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} maxBarSize={40}>
{monthlyData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.active ? '#3b82f6' : '#94a3b8'} // Blue active, Slate-400 others
className="dark:fill-slate-700"
style={{ fill: entry.active ? '#3b82f6' : undefined }} // Override inline for active
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<div className="text-center mt-2 pb-2">
<span className="text-[10px] text-slate-400 dark:text-slate-500 italic">Evolução últimos 6 meses</span>
</div>
</div>
)}
</div>
)}
</div>
</Card>
);
};

View File

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

View File

@ -1,404 +0,0 @@
/* PrafrotSidebar CSS - Adaptive Theme (Light/Dark) */
:root {
/* Light Theme Variables (Default) */
--pfs-bg: #ffffff;
--pfs-text: #475569;
--pfs-text-active: #0f172a;
--pfs-text-muted: #64748b;
--pfs-border: #f1f5f9;
--pfs-bg-search: #f8fafc;
--pfs-bg-hover: rgba(0, 0, 0, 0.03);
--pfs-tree-line: #e2e8f0;
--pfs-footer-bg: #f8fafc;
--pfs-user-card-bg: rgba(0, 0, 0, 0.01);
--pfs-toggle-bg: #ffffff;
--pfs-toggle-border: #e2e8f0;
--pfs-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
}
.dark .pfs-container {
/* Dark Theme Variables */
--pfs-bg: #1a1a1a;
--pfs-text: #ffffff;
--pfs-text-active: #141414;
--pfs-text-muted: #a0a0a0;
--pfs-border: rgba(255, 255, 255, 0.03);
--pfs-bg-search: #252525;
--pfs-bg-hover: rgba(255, 255, 255, 0.03);
--pfs-tree-line: #262626;
--pfs-footer-bg: #181818;
--pfs-user-card-bg: rgba(255, 255, 255, 0.02);
--pfs-toggle-bg: #262626;
--pfs-toggle-border: #333;
--pfs-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.pfs-container {
width: 260px;
height: calc(100vh - 20px);
background: var(--pfs-bg);
color: var(--pfs-text);
display: flex;
flex-direction: column;
position: fixed;
top: 10px;
left: 10px;
z-index: 40;
border-radius: 16px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
box-shadow: var(--pfs-shadow);
border: 1px solid var(--pfs-border);
}
/* Subtle Top Glow - orange */
.pfs-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, #f97316, transparent);
opacity: 0.3;
}
.pfs-container.collapsed {
width: 80px;
}
/* Toggle Area */
.pfs-toggle-container {
padding: 1rem;
display: flex;
justify-content: flex-end;
min-height: 56px;
align-items: center;
}
.collapsed .pfs-toggle-container {
justify-content: center;
}
.pfs-toggle-btn {
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--pfs-toggle-bg);
border: 1px solid var(--pfs-toggle-border);
color: var(--pfs-text-muted);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.pfs-toggle-btn:hover {
background: var(--pfs-bg-search);
color: #f97316;
border-color: #f97316;
transform: scale(1.05);
}
/* Search Box */
.pfs-search-wrapper {
padding: 0.5rem 1.25rem 1.5rem;
transition: all 0.3s ease;
}
.collapsed .pfs-search-wrapper {
opacity: 0;
pointer-events: none;
height: 0;
padding: 0;
}
.pfs-search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: var(--pfs-bg-search);
border: 1px solid var(--pfs-border);
border-radius: 12px;
color: var(--pfs-text);
font-size: 0.85rem;
outline: none;
transition: all 0.2s ease;
}
.pfs-search-input:focus {
border-color: #f97316;
background: var(--pfs-bg);
box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.1);
}
/* Navigation Content */
.pfs-nav-content {
flex: 1;
padding: 0.5rem 1rem;
overflow-y: auto;
overflow-x: hidden;
}
.pfs-nav-content::-webkit-scrollbar {
width: 3px;
}
.pfs-nav-content::-webkit-scrollbar-thumb {
background: var(--pfs-border);
border-radius: 10px;
}
/* Menu Item Wrapper */
.pfs-item-wrapper {
margin-bottom: 6px;
}
/* Links & Tree Structure */
.pfs-link {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-radius: 12px;
color: var(--pfs-text-muted);
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
gap: 14px;
}
.pfs-link:hover:not(.active) {
background: var(--pfs-bg-hover);
color: var(--pfs-text);
}
.pfs-link.active {
background: #f97316 !important;
color: var(--pfs-text-active) !important;
font-weight: 700;
box-shadow: 0 4px 15px rgba(249, 115, 22, 0.25);
}
.pfs-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
.pfs-link:hover .pfs-icon:not(.active) {
transform: scale(1.1);
}
.pfs-label {
flex: 1;
font-size: 0.9rem;
white-space: nowrap;
letter-spacing: -0.01em;
}
.collapsed .pfs-label,
.collapsed .pfs-chevron {
display: none;
}
.pfs-chevron {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--pfs-text-muted);
}
.pfs-chevron.expanded {
transform: rotate(180deg);
}
/* Submenu & Tree Lines */
.pfs-submenu {
margin-left: 0.5rem;
padding-left: 1.25rem;
border-left: 2px solid var(--pfs-tree-line);
margin-top: 4px;
margin-bottom: 8px;
}
.pfs-sublink {
display: flex;
align-items: center;
padding: 0.65rem 1rem;
border-radius: 10px;
color: var(--pfs-text-muted);
font-size: 0.85rem;
transition: all 0.2s ease;
text-decoration: none;
margin-bottom: 2px;
position: relative;
gap: 12px;
}
.pfs-sublink::before {
content: '';
position: absolute;
left: -1.25rem;
top: 50%;
width: 0.75rem;
height: 2px;
background: var(--pfs-tree-line);
}
.pfs-sublink:hover {
color: var(--pfs-text);
background: var(--pfs-bg-hover);
}
.pfs-sublink.active {
color: #f97316;
font-weight: 700;
}
/* Icons in sublink */
.pfs-sublink .pfs-icon {
opacity: 0.7;
}
.pfs-sublink.active .pfs-icon {
opacity: 1;
color: #f97316;
}
/* Footer Section */
.pfs-footer {
padding: 1.25rem;
border-top: 1px solid var(--pfs-border);
background: var(--pfs-footer-bg);
display: flex;
flex-direction: column;
gap: 1rem;
}
.collapsed .pfs-footer {
padding: 1.25rem 0.5rem;
align-items: center;
}
/* Brand Area in Footer */
.pfs-brand {
display: flex;
align-items: center;
gap: 12px;
}
.pfs-brand-logo {
width: 36px;
height: 36px;
background: #f97316;
color: #141414;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
box-shadow: 0 0 15px rgba(249, 115, 22, 0.2);
flex-shrink: 0;
}
.pfs-brand-info {
display: flex;
flex-direction: column;
}
.pfs-brand-name {
font-weight: 800;
font-size: 0.95rem;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--pfs-text);
}
.pfs-brand-name span {
color: #f97316;
}
.pfs-app-sub {
font-size: 0.7rem;
color: var(--pfs-text-muted);
font-weight: 600;
}
/* User Card */
.pfs-user-card {
display: flex;
align-items: center;
gap: 12px;
padding: 0.625rem;
background: var(--pfs-user-card-bg);
border-radius: 12px;
border: 1px solid var(--pfs-border);
}
.pfs-user-data {
display: flex;
flex-direction: column;
min-width: 0;
}
.pfs-user-name {
font-size: 0.85rem;
font-weight: 700;
color: var(--pfs-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pfs-logout-link {
font-size: 0.75rem;
color: var(--pfs-text-muted);
display: flex;
align-items: center;
gap: 4px;
transition: color 0.2s;
background: none;
border: none;
padding: 0;
cursor: pointer;
width: fit-content;
}
.pfs-logout-link:hover {
color: #ef4444;
}
/* Legal & Version */
.pfs-legal {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 0.75rem;
border-top: 1px solid var(--pfs-border);
}
.pfs-legal-item, .pfs-version {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.7rem;
color: var(--pfs-text-muted);
}
.pfs-legal-item svg, .pfs-version svg {
color: #f97316;
opacity: 0.6;
}
/* Locks */
.pfs-locked {
opacity: 0.4;
cursor: not-allowed !important;
}
.pfs-lock-icon {
margin-left: auto;
color: #f97316 !important;
}

View File

@ -1,249 +0,0 @@
import React, { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import {
ChevronLeft,
ChevronRight,
Search,
ChevronDown,
LayoutDashboard,
Car,
Users,
Wrench,
Activity,
Radio,
AlertTriangle,
Store,
ClipboardList,
LogOut,
ShieldAlert,
Building2,
Award,
GitBranch,
Lock,
Settings
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAuthContext } from '@/components/shared/AuthProvider';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import logoOestePan from '@/assets/Img/Util/Oeste_Pan/Logo Oeste Pan.png';
import './PrafrotSidebar.css';
const MENU_ITEMS = [
{
id: 'dashboard',
label: 'Estatísticas',
icon: LayoutDashboard,
path: '/plataforma/oest-pan/estatisticas'
},
{
id: 'cadastros',
label: 'Cadastros',
icon: ClipboardList,
children: [
{ id: 'c-veiculos', label: 'Veículos', path: '/plataforma/oest-pan/veiculos', icon: Car },
{ id: 'c-dispatcher', label: 'Dispatcher', path: '/plataforma/oest-pan/dispatcher', icon: ClipboardList },
// { id: 'c-motoristas', label: 'Motoristas', path: '/plataforma/oest-pan/motoristas', icon: Users, disabled: true, disabledReason: 'Funcionalidade em manutenção' },
{ id: 'c-oficinas', label: 'Oficinas', path: '/plataforma/oest-pan/oficinas', icon: Store }
]
},
{
id: 'gerencia',
label: 'Gerência',
icon: Activity,
children: [
{ id: 'g-monitoramento', label: 'Monitoramento', path: '/plataforma/oest-pan/monitoramento', icon: Radio },
{ id: 'g-status', label: 'Status Frota', path: '/plataforma/oest-pan/status', icon: Activity },
{ id: 'g-manutencao', label: 'Manutenção', path: '/plataforma/oest-pan/manutencao', icon: Wrench },
{ id: 'g-sinistros', label: 'Sinistros', path: '/plataforma/oest-pan/sinistros', icon: AlertTriangle }
]
},
{
id: 'config',
label: 'Configurações',
icon: Settings,
path: '/plataforma/oest-pan/configuracoes'
}
];
// Optimized MenuItem component outside main render loop
const MenuItem = React.memo(({ item, isSub = false, isCollapsed, expandedItems, toggleExpand, pathname, searchTerm }) => {
const Icon = item.icon;
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedItems[item.id];
const isActive = pathname.startsWith(item.path) || (hasChildren && item.children.some(c => pathname.startsWith(c.path)));
const isLocked = item.disabled;
// Filter sub-items if searching
const subItems = item.children?.filter(child =>
!searchTerm || child.label.toLowerCase().includes(searchTerm.toLowerCase())
);
const content = (
<div
className={cn(
isSub ? "pfs-sublink" : "pfs-link",
isActive && !hasChildren && "active",
isLocked && "pfs-locked"
)}
onClick={() => {
if (isLocked) return;
if (hasChildren) toggleExpand(item.id);
}}
title={isLocked ? item.disabledReason : (isCollapsed ? item.label : '')}
>
<Icon size={isSub ? 16 : 20} className="pfs-icon" />
{(!isCollapsed || isSub) && <span className="pfs-label">{item.label}</span>}
{hasChildren && !isCollapsed && (
<ChevronDown
size={14}
className={cn("pfs-chevron", isExpanded && "expanded")}
/>
)}
{isLocked && !isCollapsed && (
<Lock size={12} className="pfs-lock-icon" />
)}
</div>
);
return (
<div className="pfs-item-wrapper">
{item.path && !hasChildren && !isLocked ? (
<Link to={item.path} style={{ textDecoration: 'none' }}>
{content}
</Link>
) : content}
<AnimatePresence>
{hasChildren && isExpanded && !isCollapsed && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="pfs-submenu"
>
{subItems?.map(child => (
<MenuItem
key={child.id}
item={child}
isSub
isCollapsed={isCollapsed}
expandedItems={expandedItems}
toggleExpand={toggleExpand}
pathname={pathname}
searchTerm={searchTerm}
/>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
});
MenuItem.displayName = 'MenuItem';
export const OestPanSidebar = ({ isCollapsed, onToggle }) => {
const [searchTerm, setSearchTerm] = useState('');
const [expandedItems, setExpandedItems] = useState({ cadastros: true, gerencia: true });
const location = useLocation();
const { user, logout } = useAuthContext();
const toggleExpand = React.useCallback((id) => {
setExpandedItems(prev => ({
...prev,
[id]: !prev[id]
}));
}, []);
const filteredItems = useMemo(() => {
if (!searchTerm) return MENU_ITEMS;
return MENU_ITEMS.filter(item => {
const matchParent = item.label.toLowerCase().includes(searchTerm.toLowerCase());
const matchChildren = item.children?.some(child =>
child.label.toLowerCase().includes(searchTerm.toLowerCase())
);
return matchParent || matchChildren;
});
}, [searchTerm]);
const handleLogout = () => {
logout('auth_oestepan');
window.location.href = '/plataforma/oest-pan/login';
};
return (
<aside className={cn("pfs-container", isCollapsed && "collapsed")}>
<div className="pfs-toggle-container">
<button className="pfs-toggle-btn" onClick={onToggle}>
{isCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
</button>
</div>
<div className="pfs-search-wrapper">
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--pfs-text-muted)]" />
<input
type="text"
className="pfs-search-input"
placeholder="Buscar..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<nav className="pfs-nav-content custom-scrollbar">
{filteredItems.map(item => (
<MenuItem
key={item.id}
item={item}
isCollapsed={isCollapsed}
expandedItems={expandedItems}
toggleExpand={toggleExpand}
pathname={location.pathname}
searchTerm={searchTerm}
/>
))}
</nav>
<footer className="pfs-footer">
<div className="pfs-brand">
<img src={logoOestePan} alt="OP" className="w-12 h-12 object-contain" />
{!isCollapsed && (
<div className="pfs-brand-info">
<span className="pfs-brand-name">Oeste <span>Pan</span></span>
<span className="pfs-app-sub">Fleet Management</span>
</div>
)}
</div>
<div className="pfs-user-section">
<div className={cn("pfs-user-card", isCollapsed && "justify-center")}>
<Avatar className="h-8 w-8 border border-[var(--pfs-border)]">
<AvatarImage src={`https://ui-avatars.com/api/?name=${user?.name || 'User'}&background=f97316&color=141414`} />
<AvatarFallback className="bg-orange-500 text-zinc-950 font-bold text-xs">
{(user?.name || 'U').charAt(0)}
</AvatarFallback>
</Avatar>
{!isCollapsed && (
<div className="pfs-user-data">
<span className="pfs-user-name">{user?.name || 'Usuário'}</span>
<button onClick={handleLogout} className="pfs-logout-link">
<LogOut size={10} /> Sair
</button>
</div>
)}
</div>
</div>
</footer>
</aside>
);
};

View File

@ -1,76 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useAvailability = create((set, get) => ({
availabilities: [],
loading: false,
error: null,
fetchAvailabilities: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getAvailability();
set({ availabilities: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
},
createAvailability: async (payload) => {
set({ loading: true });
try {
await prafrotService.createAvailability(payload);
toast.success('Disponibilidade registrada com sucesso!');
get().fetchAvailabilities();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateAvailability: async (id, payload) => {
set({ loading: true });
try {
await prafrotService.updateAvailability(id, payload);
toast.success('Disponibilidade atualizada com sucesso!');
get().fetchAvailabilities();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
deleteAvailability: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este registro de disponibilidade?')) return;
set({ loading: true });
try {
await prafrotService.deleteAvailability(id);
toast.success('Disponibilidade excluída com sucesso!');
get().fetchAvailabilities();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,78 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { useFeedbackStore } from '../components/FeedbackNotification';
import { extractFriendlyMessage } from '../utils/errorMessages';
const notify = (type, title, message) => useFeedbackStore.getState().notify(type, title, message);
export const useClaims = create((set, get) => ({
claims: [],
loading: false,
error: null,
fetchClaims: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getClaims();
set({ claims: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
notify('error', 'Falha na Busca', friendlyMsg);
} finally {
set({ loading: false });
}
},
createClaim: async (payload) => {
set({ loading: true });
try {
await prafrotService.createClaims(payload);
notify('success', 'Evento Registrado', 'O sinistro/devolução foi salvo com sucesso.');
get().fetchClaims();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Falha no Registro', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateClaim: async (id, payload) => {
set({ loading: true });
try {
await prafrotService.updateClaims(id, payload);
notify('success', 'Registro Atualizado', 'As informações do evento foram modificadas.');
get().fetchClaims();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro na Atualização', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
deleteClaim: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este registro de sinistro/devolução?')) return;
set({ loading: true });
try {
await prafrotService.deleteClaim(id);
notify('success', 'Registro Removido', 'O registro foi excluído do sistema.');
get().fetchClaims();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Falha na Exclusão', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,31 +0,0 @@
import { create } from 'zustand';
import { dispatcherService } from '../services/dispatcherService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useDispatcher = create((set) => ({
data: [],
loading: false,
error: null,
fetchData: async () => {
set({ loading: true, error: null });
try {
const result = await dispatcherService.getDispatcherData();
// Handle potential API variations (e.g. { data: [...] } vs [...])
const list = Array.isArray(result) ? result : (result?.data || []);
set({ data: list });
} catch (error) {
console.error('Error fetching dispatcher data:', error);
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,27 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useDrivers = create((set, get) => ({
drivers: [],
loading: false,
error: null,
fetchDrivers: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getDrivers();
set({ drivers: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,110 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useFleetLists = create((set, get) => ({
listsConfig: null,
statusFrotaOptions: [],
statusManutencaoOptions: [],
motivoAtendimentoOptions: [],
responsaveisOptions: [],
validacaoOptions: [],
aprovacaoOptions: [],
loading: false,
error: null,
fetchListsConfig: async () => {
set({ loading: true, error: null });
try {
const config = await prafrotService.getListasConfig();
set({ listsConfig: config });
// Se tivermos a configuração para status_frota, já buscamos as opções
if (config && config.status_frota && config.status_frota.rota) {
await get().fetchListOptions('status_frota', config.status_frota.rota);
}
// Se tivermos a configuração para status_manutencao, buscamos as opções
if (config && config.status_manutencao && config.status_manutencao.rota) {
await get().fetchListOptions('status_manutencao', config.status_manutencao.rota);
}
// Se tivermos a configuração para motivo_atendimento, buscamos as opções
if (config && config.motivo_atendimento && config.motivo_atendimento.rota) {
await get().fetchListOptions('motivo_atendimento', config.motivo_atendimento.rota);
}
// Se tivermos a configuração para responsaveis, buscamos as opções
if (config && config.responsaveis && config.responsaveis.rota) {
await get().fetchListOptions('responsaveis', config.responsaveis.rota);
}
// Se tivermos a configuração para validacao, buscamos as opções
if (config && config.validacao && config.validacao.rota) {
await get().fetchListOptions('validacao', config.validacao.rota);
}
// Se tivermos a configuração para aprovacao, buscamos as opções
if (config && config.aprovacao && config.aprovacao.rota) {
await get().fetchListOptions('aprovacao', config.aprovacao.rota);
}
} catch (error) {
console.error('Erro ao buscar configuração de listas:', error);
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
} finally {
set({ loading: false });
}
},
fetchListOptions: async (listName, route) => {
try {
const data = await prafrotService.getListasByRoute(route);
let options = Array.isArray(data) ? data : (data.data || []);
if (options.length > 0 && typeof options[0] === 'object') {
if (listName === 'status_frota') {
options = options.map(item => item.status_frota).filter(Boolean);
} else if (listName === 'status_manutencao') {
options = options.map(item => item.status_manutencao || item.status || Object.values(item)[0]).filter(Boolean);
} else if (listName === 'motivo_atendimento') {
options = options.map(item => item.motivo_atendimento || item.motivo || Object.values(item)[0]).filter(Boolean);
} else if (listName === 'responsaveis') {
options = options.map(item => item.responsaveis || item.responsavel || item.nome || Object.values(item)[0]).filter(Boolean);
} else if (listName === 'validacao') {
options = options.map(item => item.validacao || item.validacao_financeiro || Object.values(item)[0]).filter(Boolean);
} else if (listName === 'aprovacao') {
options = options.map(item => item.aprovacao || item.nome || item.responsavel || Object.values(item)[0]).filter(Boolean);
} else {
options = options.map(item => Object.values(item)[0] || JSON.stringify(item));
}
} else {
options = options.map(String);
}
if (listName === 'status_frota') {
set({ statusFrotaOptions: options });
} else if (listName === 'status_manutencao') {
set({ statusManutencaoOptions: options });
} else if (listName === 'motivo_atendimento') {
set({ motivoAtendimentoOptions: options });
} else if (listName === 'responsaveis') {
set({ responsaveisOptions: options });
} else if (listName === 'validacao') {
set({ validacaoOptions: options });
} else if (listName === 'aprovacao') {
set({ aprovacaoOptions: options });
}
return options;
} catch (error) {
console.error(`Erro ao buscar opções para ${listName}:`, error);
return [];
}
}
}));

View File

@ -1,262 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { useFeedbackStore } from '../components/FeedbackNotification';
import { extractFriendlyMessage } from '../utils/errorMessages';
const notify = (type, title, message) => useFeedbackStore.getState().notify(type, title, message);
export const useMaintenance = create((set, get) => ({
maintenances: [],
loading: false,
error: null,
fetchMaintenances: async (type = 'total') => {
set({ loading: true, error: null });
try {
let list = [];
if (type === 'total') {
const data = await prafrotService.getMaintenance();
list = Array.isArray(data) ? data : (data.data || []);
} else {
const data = await prafrotService.getAbertoFechado();
// Concatena abertas e fechadas para manter o estado global se necessário,
// mas a tela de manutenção gerencia o estado `abertoFechadoData` via hook.
// No entanto, para padronizar as ações (save/update), vamos manter o fetchMaintenances funcional.
const abertas = data.abertas || data.aberto || data.abertos || [];
const fechadas = data.fechadas || data.fechado || data.fechados || [];
list = [...abertas, ...fechadas];
}
const normalized = list.map(m => ({ ...m, previsao_entrega: m.previsao_entrega ?? m.previcao_entrega }));
set({ maintenances: normalized });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
notify('error', 'Falha na Busca', friendlyMsg);
} finally {
set({ loading: false });
}
},
// Helper para decidir qual rota atualizar baseado no contexto (geralmente chamado após mutações)
refreshMaintenances: async (currentFilter = 'total') => {
const fetch = get().fetchMaintenances;
const getGroups = get().getAbertoFechado;
if (currentFilter === 'total') {
await fetch('total');
} else {
// Atualiza ambos para garantir consistência
await fetch('total');
await getGroups();
}
},
createMaintenance: async (payload, files = null) => {
set({ loading: true });
try {
await prafrotService.createMaintenance(payload, files);
notify('success', 'Cadastro Concluído', 'Manutenção registrada com sucesso no sistema.');
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Falha no Registro', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateMaintenance: async (id, payload, files = null) => {
set({ loading: true });
try {
await prafrotService.updateMaintenance(id, payload, files);
notify('success', 'Atualização!', 'Dados da manutenção atualizados.');
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro na Atualização', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateMaintenanceBatch: async (ids, status) => {
set({ loading: true });
try {
await prafrotService.updateMaintenanceBatch(ids, status);
notify('success', 'Lote Atualizado', `${ids.length} manutenções foram atualizadas.`);
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Falha em Massa', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
calcularDifOrcamento: async (orcamento_inicial, orcamento_final) => {
try {
const data = await prafrotService.calcularDifOrcamento(orcamento_inicial, orcamento_final);
const valor = data?.dif_orcamento ?? data?.valor ?? (typeof data === 'number' ? data : null);
return valor;
} catch (error) {
return null;
}
},
deleteMaintenance: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir esta manutenção?')) return;
set({ loading: true });
try {
await prafrotService.deleteMaintenance(id);
notify('success', 'Removido', 'Registro de manutenção excluído.');
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro na Remoção', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
fecharManutencao: async (id) => {
set({ loading: true });
try {
await prafrotService.fecharManutencao(id);
notify('success', 'Manutenção Fechada', 'Manutenção fechada com sucesso.');
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Fechar', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
abrirManutencao: async (id) => {
set({ loading: true });
try {
await prafrotService.abrirManutencao(id);
notify('success', 'Manutenção Aberta', 'Manutenção aberta com sucesso.');
get().fetchMaintenances();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Abrir', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
getAbertoFechado: async () => {
set({ loading: true });
try {
const data = await prafrotService.getAbertoFechado();
// Atualiza também a lista principal 'maintenances' para refletir as mudanças
const abertas = data.abertas || data.aberto || data.abertos || [];
const fechadas = data.fechadas || data.fechado || data.fechados || [];
const normalized = [...abertas, ...fechadas].map(m => ({ ...m, previsao_entrega: m.previsao_entrega ?? m.previcao_entrega }));
set({ maintenances: normalized });
return data;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro na Busca', friendlyMsg);
return null;
} finally {
set({ loading: false });
}
},
// Histórico de manutenção
getHistoricoCompleto: async () => {
try {
const data = await prafrotService.getHistoricoCompleto();
return Array.isArray(data) ? data : (data.data || []);
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Histórico', friendlyMsg);
return [];
}
},
getHistoricoDetalhado: async () => {
try {
const data = await prafrotService.getHistoricoDetalhado();
return Array.isArray(data) ? data : (data.data || []);
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Histórico', friendlyMsg);
return [];
}
},
getHistoricoResumido: async () => {
try {
const data = await prafrotService.getHistoricoResumido();
return Array.isArray(data) ? data : (data.data || []);
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Histórico', friendlyMsg);
return [];
}
},
getHistoricoEstatisticas: async () => {
try {
const data = await prafrotService.getHistoricoEstatisticas();
return data;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Estatísticas', friendlyMsg);
return null;
}
},
getHistoricoPeriodo: async (params) => {
try {
const data = await prafrotService.getHistoricoPeriodo(params);
return Array.isArray(data) ? data : (data.data || []);
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Histórico', friendlyMsg);
return [];
}
},
getHistoricoTop: async () => {
try {
const data = await prafrotService.getHistoricoTop();
return Array.isArray(data) ? data : (data.data || []);
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Histórico', friendlyMsg);
return [];
}
},
getHistoricoPorPlaca: async (placa) => {
try {
const data = await prafrotService.getHistoricoPorPlaca(placa);
return Array.isArray(data) ? data : (data.data || []);
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro ao Buscar Histórico', friendlyMsg);
return [];
}
}
}));

View File

@ -1,76 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useMoki = create((set, get) => ({
mokis: [],
loading: false,
error: null,
fetchMokis: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getMoki();
set({ mokis: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
},
createMoki: async (payload) => {
set({ loading: true });
try {
await prafrotService.createMoki(payload);
toast.success('Moki registrado com sucesso!');
get().fetchMokis();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateMoki: async (id, payload) => {
set({ loading: true });
try {
await prafrotService.updateMoki(id, payload);
toast.success('Moki atualizado com sucesso!');
get().fetchMokis();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
deleteMoki: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este checklist?')) return;
set({ loading: true });
try {
await prafrotService.deleteMoki(id);
toast.success('Checklist excluído com sucesso!');
get().fetchMokis();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,76 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useMonitoring = create((set, get) => ({
monitorings: [],
loading: false,
error: null,
fetchMonitoring: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getMonitoring();
set({ monitorings: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
},
createMonitoring: async (payload) => {
set({ loading: true });
try {
await prafrotService.createMonitoring(payload);
toast.success('Monitoramento registrado com sucesso!');
get().fetchMonitoring();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateMonitoring: async (id, payload) => {
set({ loading: true });
try {
await prafrotService.updateMonitoring(id, payload);
toast.success('Monitoramento atualizado com sucesso!');
get().fetchMonitoring();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
deleteMonitoring: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este monitoramento?')) return;
set({ loading: true });
try {
await prafrotService.deleteMonitoring(id);
toast.success('Monitoramento excluído com sucesso!');
get().fetchMonitoring();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,95 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { prafrotStatisticsService } from '../services/prafrotStatisticsService';
import { toast } from 'sonner';
export const usePrafrotStatistics = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState({
totalPlacas: 0,
placasPorBase: [],
placasPorAno: [],
placasPorCategoria: [],
placasPorModelo: [],
placasPorTipo: [],
placasPorStatus: [],
placasPorManutencao: [],
placasPorProprietario: [],
// placasPorDisponibilidade: [],
placasPorUnidade: [],
placasPorSinistro: [],
placasPorManutencaoStatus: [],
quantitativoManutencao: []
});
const fetchStatistics = useCallback(async () => {
setLoading(true);
try {
const [
totalRes,
baseRes,
anoRes,
catRes,
modRes,
tipoRes,
statusRes,
manutRes,
propRes,
// dispRes,
unidRes,
sinRes,
mStatusRes,
qManutRes
] = await Promise.all([
prafrotStatisticsService.getTotalPlacas(),
prafrotStatisticsService.getPlacasPorBase(),
prafrotStatisticsService.getPlacasPorAno(),
prafrotStatisticsService.getPlacasPorCategoria(),
prafrotStatisticsService.getPlacasPorModelo(),
prafrotStatisticsService.getPlacasPorTipo(),
prafrotStatisticsService.getPlacasPorStatus(),
prafrotStatisticsService.getPlacasPorManutencao(),
prafrotStatisticsService.getPlacasPorProprietario(),
// prafrotStatisticsService.getPlacasPorDisponibilidade(),
prafrotStatisticsService.getPlacasPorUnidade(),
prafrotStatisticsService.getPlacasPorSinistro(),
prafrotStatisticsService.getPlacasPorManutencaoStatus(),
prafrotStatisticsService.getQuantitativoManutencao()
]);
setData({
totalPlacas: totalRes?.total_placas || totalRes?.[0]?.total_placas || 0,
placasPorBase: Array.isArray(baseRes) ? baseRes : [],
placasPorAno: Array.isArray(anoRes) ? anoRes : [],
placasPorCategoria: Array.isArray(catRes) ? catRes : [],
placasPorModelo: Array.isArray(modRes) ? modRes : [],
placasPorTipo: Array.isArray(tipoRes) ? tipoRes : [],
placasPorStatus: Array.isArray(statusRes) ? statusRes : [],
placasPorManutencao: Array.isArray(manutRes) ? manutRes : [],
placasPorProprietario: Array.isArray(propRes) ? propRes : [],
// placasPorDisponibilidade: Array.isArray(dispRes) ? dispRes : [],
placasPorUnidade: Array.isArray(unidRes) ? unidRes : [],
placasPorSinistro: Array.isArray(sinRes) ? sinRes : [],
placasPorManutencaoStatus: Array.isArray(mStatusRes) ? mStatusRes : [],
quantitativoManutencao: Array.isArray(qManutRes) ? qManutRes : []
});
} catch (error) {
console.error('Erro ao buscar estatísticas:', error);
toast.error('Erro ao carregar dados estatísticos');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStatistics();
}, [fetchStatistics]);
return {
loading,
data,
refresh: fetchStatistics
};
};

View File

@ -1,106 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useStatus = create((set, get) => ({
statusList: [],
loading: false,
error: null,
fetchStatus: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getStatus();
set({ statusList: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
},
createStatus: async (payload) => {
set({ loading: true });
try {
await prafrotService.createStatus(payload);
toast.success('Status registrado com sucesso!');
get().fetchStatus();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateStatus: async (id, payload) => {
set({ loading: true });
try {
await prafrotService.updateStatus(id, payload);
toast.success('Status atualizado com sucesso!');
get().fetchStatus();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateStatusInline: async (id, statusFrota) => {
// Agora utiliza o endpoint em lote mesmo para uma única atualização inline
try {
await prafrotService.updateStatusBatch([id], statusFrota);
toast.success('Status atualizado!');
get().fetchStatus();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
}
},
updateStatusBatch: async (ids, statusFrota) => {
set({ loading: true });
try {
await prafrotService.updateStatusBatch(ids, statusFrota);
toast.success(`${ids.length} itens atualizados com sucesso!`);
get().fetchStatus();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
deleteStatus: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este registro de status?')) return;
set({ loading: true });
try {
await prafrotService.deleteStatus(id);
toast.success('Status excluído com sucesso!');
get().fetchStatus();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,92 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { useFeedbackStore } from '../components/FeedbackNotification';
import { extractFriendlyMessage } from '../utils/errorMessages';
const notify = (type, title, message) => useFeedbackStore.getState().notify(type, title, message);
export const useVehicles = create((set, get) => ({
vehicles: [],
loading: false,
error: null,
fetchVehicles: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getVehicles();
// Ensure data is an array
set({ vehicles: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
notify('error', 'Falha na Busca', friendlyMsg);
} finally {
set({ loading: false });
}
},
createVehicle: async (vehicleData) => {
set({ loading: true });
try {
await prafrotService.createVehicle(vehicleData);
notify('success', 'Cadastro Concluído', 'Veículo registrado com sucesso no sistema.');
get().fetchVehicles(); // Refresh list
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Falha no Cadastro', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateVehicle: async (id, vehicleData) => {
set({ loading: true });
try {
await prafrotService.updateVehicle(id, vehicleData);
notify('success', 'Atualização!', 'Os dados do veículo foram atualizados.');
get().fetchVehicles();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro na Atualização', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateVehicleStatus: async (id, statusFrota) => {
try {
await prafrotService.updateVehicleStatus(id, statusFrota);
notify('success', 'Status Alterado', 'O status do veículo foi modificado com sucesso.');
get().fetchVehicles();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro de Status', friendlyMsg);
return false;
}
},
deleteVehicle: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este veículo?')) return;
set({ loading: true });
try {
await prafrotService.deleteVehicle(id);
notify('success', 'Registro Removido', 'O veículo foi removido permanentemente.');
get().fetchVehicles();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
notify('error', 'Erro na Remoção', friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,76 +0,0 @@
import { create } from 'zustand';
import { prafrotService } from '../services/prafrotService';
import { toast } from 'sonner';
import { extractFriendlyMessage } from '../utils/errorMessages';
export const useWorkshops = create((set, get) => ({
workshops: [],
loading: false,
error: null,
fetchWorkshops: async () => {
set({ loading: true, error: null });
try {
const data = await prafrotService.getWorkshops();
set({ workshops: Array.isArray(data) ? data : (data.data || []) });
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
set({ error: friendlyMsg });
toast.error(friendlyMsg);
} finally {
set({ loading: false });
}
},
createWorkshop: async (payload) => {
set({ loading: true });
try {
await prafrotService.createWorkshop(payload);
toast.success('Oficina registrada com sucesso!');
get().fetchWorkshops();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
updateWorkshop: async (id, payload) => {
set({ loading: true });
try {
await prafrotService.updateWorkshop(id, payload);
toast.success('Oficina atualizada com sucesso!');
get().fetchWorkshops();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
},
deleteWorkshop: async (id) => {
if (!window.confirm('Tem certeza que deseja excluir esta oficina?')) return;
set({ loading: true });
try {
await prafrotService.deleteWorkshop(id);
toast.success('Oficina excluída com sucesso!');
get().fetchWorkshops();
return true;
} catch (error) {
const friendlyMsg = extractFriendlyMessage(error);
toast.error(friendlyMsg);
return false;
} finally {
set({ loading: false });
}
}
}));

View File

@ -1,92 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import {
Menu,
Sun,
Moon,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useDocumentMetadata } from '@/hooks/useDocumentMetadata';
import { OestPanSidebar } from '../components/PrafrotSidebar';
import { FeedbackContainer } from '../components/FeedbackNotification';
export const OestPanLayout = () => {
useDocumentMetadata('Oeste Pan', 'oest-pan');
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(true);
useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDarkMode]);
return (
<div className={cn(
"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"
)}>
<FeedbackContainer />
{/* New Sidebar Component */}
<OestPanSidebar
isCollapsed={isSidebarCollapsed}
onToggle={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
/>
{/* Main Content Area */}
<main
className={cn(
"flex-1 transition-all duration-300 min-h-screen flex flex-col min-w-0",
isSidebarCollapsed ? "ml-[100px]" : "ml-[280px]"
)}
>
{/* Header */}
<header className={cn(
"h-16 px-8 sticky top-0 z-40 backdrop-blur-md border-b flex items-center justify-between",
isDarkMode ? "bg-[#141414]/80 border-[#2a2a2a]" : "bg-slate-50/80 border-slate-200"
)}>
{/* <div className="flex items-center gap-4">
<button
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
className="text-slate-400 hover:text-orange-500 transition-colors p-2 rounded-xl hover:bg-orange-500/10"
>
<Menu size={20} />
</button>
</div> */}
<div className="flex items-center gap-4">
{/* Theme Toggle */}
<button
onClick={() => setIsDarkMode(!isDarkMode)}
className={cn(
"p-2 rounded-full transition-colors",
isDarkMode ? "text-slate-400 hover:text-yellow-400" : "text-slate-500 hover:text-orange-500"
)}
>
{isDarkMode ? <Sun size={20} /> : <Moon size={20} />}
</button>
</div>
</header>
{/* Content */}
<div className="flex-1 p-6 min-w-0 overflow-hidden">
<div className={cn(
"h-full w-full rounded-2xl border overflow-hidden flex flex-col shadow-sm",
isDarkMode
? "bg-[#1b1b1b] border-zinc-800 shadow-black/20"
: "bg-white border-slate-200 shadow-slate-200/50"
)}>
<div className="flex-1 overflow-y-auto custom-scrollbar p-6">
<Outlet />
</div>
</div>
</div>
</main>
</div>
);
};

View File

@ -1,69 +0,0 @@
import { lazy, Suspense } from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';
import { OestPanLayout } from './layout/PrafrotLayout';
import { Zap } from 'lucide-react';
// Lazy loading views
const VehiclesView = lazy(() => import('./views/VehiclesView'));
const MaintenanceView = lazy(() => import('./views/MaintenanceView'));
const AvailabilityView = lazy(() => import('./views/AvailabilityView'));
const MokiView = lazy(() => import('./views/MokiView'));
const StatusView = lazy(() => import('./views/StatusView'));
const MonitoringView = lazy(() => import('./views/MonitoringView'));
const WorkshopsView = lazy(() => import('./views/WorkshopsView'));
const ClaimsView = lazy(() => import('./views/ClaimsView'));
const DriversView = lazy(() => import('./views/DriversView'));
const LoginView = lazy(() => import('./views/LoginView'));
const StatisticsView = lazy(() => import('./views/StatisticsView'));
const DispatcherView = lazy(() => import('./views/DispatcherView'));
const ConfigView = lazy(() => import('./views/ConfigView'));
// Loading component matching Prafrot theme
const OestPanLoader = () => (
<div className="flex h-screen w-screen items-center justify-center bg-[#141414]">
<div className="flex flex-col items-center gap-6">
<div className="relative">
<div className="w-16 h-16 bg-orange-500 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/20 animate-bounce">
<Zap size={32} className="text-[#1c1c1c]" strokeWidth={2.5} />
</div>
<div className="absolute -inset-4 border-2 border-orange-500/20 border-t-orange-500 rounded-full animate-spin" />
</div>
<div className="flex flex-col items-center gap-1">
<span className="text-orange-500 text-[10px] font-bold uppercase tracking-[0.3em] animate-pulse">Oeste Pan System</span>
<span className="text-slate-600 text-[8px] font-bold uppercase tracking-widest">Carregando...</span>
</div>
</div>
</div>
);
export const OestPanRoutes = () => {
return (
<Suspense fallback={<OestPanLoader />}>
<Routes>
<Route element={<OestPanLayout />}>
<Route path="veiculos" element={<VehiclesView />} />
<Route path="manutencao" element={<MaintenanceView />} />
<Route path="disponibilidade" element={<AvailabilityView />} />
<Route path="moki" element={<MokiView />} />
<Route path="status" element={<StatusView />} />
<Route path="monitoramento" element={<MonitoringView />} />
<Route path="oficinas" element={<WorkshopsView />} />
<Route path="sinistros" element={<ClaimsView />} />
<Route path="motoristas" element={<DriversView />} />
<Route path="estatisticas" element={<StatisticsView />} />
<Route path="dispatcher" element={<DispatcherView />} />
<Route path="configuracoes" element={<ConfigView />} />
<Route index element={<Navigate to="/plataforma/oest-pan/estatisticas" replace />} />
<Route path="*" element={<Navigate to="/plataforma/oest-pan/estatisticas" replace />} />
</Route>
</Routes>
</Suspense>
);
};
// Export LoginView separately for use in App.jsx
export { LoginView };

View File

@ -1,17 +0,0 @@
import api from '@/services/api';
import { handleRequest } from '@/services/serviceUtils';
const ENDPOINT = '/dispatcher';
export const dispatcherService = {
getDispatcherData: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINT}/apresentar`);
return data;
}
})
};

View File

@ -1,410 +0,0 @@
import api from '@/services/api';
import { handleRequest } from '@/services/serviceUtils';
// Endpoints definidos pelo usuário
const ENDPOINTS = {
VEHICLES: '/cadastro_frota',
MAINTENANCE: '/manutencao_frota',
AVAILABILITY: '/disponibilidade_frota',
MOKI: '/moki_frota',
STATUS: '/status_frota',
MONITORING: '/monitoramento_frota',
CLAIMS: '/sinistro_devolucao_venda_frota',
WORKSHOPS: '/oficinas_frota',
AUTH: '/auth_monitoramento'
};
export const prafrotService = {
// --- Autenticação ---
login: (credentials) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.AUTH, credentials);
return data;
}
}),
// --- Cadastro de Frota (Veículos) ---
getVehicles: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.VEHICLES}/apresentar`);
return data;
}
}),
createVehicle: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.VEHICLES, payload);
return data;
}
}),
updateVehicle: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.VEHICLES}/${id}`, payload);
return data;
}
}),
deleteVehicle: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.VEHICLES}/delete/${id}`);
return data;
}
}),
// --- Manutenção ---
getMaintenance: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.MAINTENANCE}/apresentar`);
return data;
}
}),
calcularDifOrcamento: (orcamento_inicial, orcamento_final) => handleRequest({
apiFn: async () => {
const { data } = await api.post(`${ENDPOINTS.MAINTENANCE}/calcular_dif_orcamento`, {
orcamento_inicial,
orcamento_final
});
return data;
}
}),
createMaintenance: (payload, files = null) => handleRequest({
apiFn: async () => {
// Envia previsao_entrega diretamente
const body = { ...payload };
if (files && (files.pdf_orcamento || files.nota_fiscal)) {
const form = new FormData();
form.append('data_json', JSON.stringify(body));
if (files.pdf_orcamento instanceof File) form.append('pdf_orcamento', files.pdf_orcamento);
if (files.nota_fiscal instanceof File) form.append('nota_fiscal', files.nota_fiscal);
const { data } = await api.post(ENDPOINTS.MAINTENANCE, form, {
headers: { 'Content-Type': 'multipart/form-data' }
});
return data;
}
const { data } = await api.post(ENDPOINTS.MAINTENANCE, body);
return data;
}
}),
updateMaintenance: (id, payload, files = null) => handleRequest({
apiFn: async () => {
// Envia previsao_entrega diretamente (o backend deve estar atualizado ou o frontend deve padronizar)
const body = { ...payload };
if (files && (files.pdf_orcamento || files.nota_fiscal)) {
const form = new FormData();
form.append('data_json', JSON.stringify(body));
if (files.pdf_orcamento instanceof File) form.append('pdf_orcamento', files.pdf_orcamento);
if (files.nota_fiscal instanceof File) form.append('nota_fiscal', files.nota_fiscal);
const { data } = await api.put(`${ENDPOINTS.MAINTENANCE}/${id}`, form, {
headers: { 'Content-Type': 'multipart/form-data' }
});
return data;
}
const { data } = await api.put(`${ENDPOINTS.MAINTENANCE}/${id}`, body);
return data;
}
}),
updateMaintenanceBatch: (ids, status) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MAINTENANCE}/edit/em_lote`, { ids, status });
return data;
}
}),
deleteMaintenance: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MAINTENANCE}/delete/${id}`);
return data;
}
}),
// Fechar/Abrir manutenção
fecharManutencao: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MAINTENANCE}/fechar_manutencao/${id}`);
return data;
}
}),
abrirManutencao: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MAINTENANCE}/abrir_manutencao/${id}`);
return data;
}
}),
getAbertoFechado: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.MAINTENANCE}/aberto_fechado/apresentar`);
return data;
}
}),
// Histórico de manutenção
getHistoricoCompleto: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/historico/completo');
return data;
}
}),
getHistoricoDetalhado: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/historico/detalhado');
return data;
}
}),
getHistoricoResumido: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/historico/resumido');
return data;
}
}),
getHistoricoEstatisticas: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/historico/estatisticas');
return data;
}
}),
getHistoricoPeriodo: (params) => handleRequest({
apiFn: async () => {
const { data } = await api.get('/historico/periodo', { params });
return data;
}
}),
getHistoricoTop: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/historico/top');
return data;
}
}),
getHistoricoPorPlaca: (placa) => handleRequest({
apiFn: async () => {
const { data } = await api.get(`/historico/placa/${placa}`);
return data;
}
}),
// --- Disponibilidade ---
getAvailability: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.AVAILABILITY}/apresentar`);
return data;
}
}),
createAvailability: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.AVAILABILITY, payload);
return data;
}
}),
updateAvailability: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.AVAILABILITY}/${id}`, payload);
return data;
}
}),
deleteAvailability: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.AVAILABILITY}/delete/${id}`);
return data;
}
}),
// --- Moki (Checklists) ---
getMoki: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.MOKI}/apresentar`);
return data;
}
}),
createMoki: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.MOKI, payload);
return data;
}
}),
updateMoki: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MOKI}/${id}`, payload);
return data;
}
}),
deleteMoki: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MOKI}/delete/${id}`);
return data;
}
}),
// --- Status Frota ---
getStatus: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.STATUS}/apresentar`);
return data;
}
}),
createStatus: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.STATUS, payload);
return data;
}
}),
updateStatus: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.STATUS}/${id}`, payload);
return data;
}
}),
updateStatusBatch: (ids, statusFrota) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.STATUS}/edit/em_lote`, { ids, status_frota: statusFrota });
return data;
}
}),
deleteStatus: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.STATUS}/delete/${id}`);
return data;
}
}),
// --- Monitoramento ---
getMonitoring: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.MONITORING}/apresentar`);
return data;
}
}),
createMonitoring: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.MONITORING, payload);
return data;
}
}),
updateMonitoring: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MONITORING}/${id}`, payload);
return data;
}
}),
deleteMonitoring: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.MONITORING}/delete/${id}`);
return data;
}
}),
// --- Sinistro / Devolução ---
getClaims: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.CLAIMS}/apresentar`);
return data;
}
}),
createClaims: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.CLAIMS, payload);
return data;
}
}),
updateClaims: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.CLAIMS}/${id}`, payload);
return data;
}
}),
deleteClaim: (id) => handleRequest({
apiFn: async () => {
// Usando a rota específica fornecida pelo usuário para exclusão de sinistro
const { data } = await api.put(`/sinistro_frota/delete/${id}`);
return data;
}
}),
// --- Oficinas ---
getWorkshops: () => handleRequest({
apiFn: async () => {
const { data } = await api.get(`${ENDPOINTS.WORKSHOPS}/apresentar`);
return data;
}
}),
createWorkshop: (payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(ENDPOINTS.WORKSHOPS, payload);
return data;
}
}),
updateWorkshop: (id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.WORKSHOPS}/${id}`, payload);
return data;
}
}),
deleteWorkshop: (id) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`${ENDPOINTS.WORKSHOPS}/delete/${id}`);
return data;
}
}),
// --- Motoristas (Drivers) ---
getDrivers: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/motoristas/listagem');
return data;
}
}),
// --- Listas de Configuração (Config Lists) ---
getListasConfig: () => handleRequest({
apiFn: async () => {
const { data } = await api.get('/listas_frota/config');
return data;
}
}),
getListasByRoute: (route) => handleRequest({
apiFn: async () => {
const { data } = await api.get(`/listas_frota/${route}`);
return data;
}
}),
createLista: (route, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.post(`/listas_frota/${route}`, payload);
return data;
}
}),
updateLista: (route, id, payload) => handleRequest({
apiFn: async () => {
const { data } = await api.put(`/listas_frota/${route}/${id}`, payload);
return data;
}
}),
deleteLista: (route, id) => handleRequest({
apiFn: async () => {
const { data } = await api.delete(`/listas_frota/${route}/${id}`);
return data;
}
}),
// --- Update Vehicle Status (Inline Edit) ---
updateVehicleStatus: (idVehicle, statusFrota) => handleRequest({
apiFn: async () => {
const { data } = await api.put('/status_frota/edit/em_lote', {
ids: [idVehicle],
status_frota: statusFrota
});
return data;
}
}),
// --- Update Status Batch (Bulk Edit) ---
updateStatusBatch: (ids, statusFrota) => handleRequest({
apiFn: async () => {
const { data } = await api.put('/status_frota/edit/em_lote', {
ids,
status_frota: statusFrota
});
return data;
}
})
};

View File

@ -1,80 +0,0 @@
import api from '@/services/api';
import { handleRequest } from '@/services/serviceUtils';
const ENDPOINTS = {
TOTAL_PLACAS: '/dashs_frota/cadastro_frota/total_placas',
PLACAS_POR_BASE: '/dashs_frota/cadastro_frota/placas_por_base',
PLACAS_POR_ANO: '/dashs_frota/cadastro_frota/placas_por_ano',
PLACAS_POR_CATEGORIA: '/dashs_frota/cadastro_frota/placas_por_categoria',
PLACAS_POR_MODELO: '/dashs_frota/cadastro_frota/placas_por_modelo',
PLACAS_POR_TIPO: '/dashs_frota/cadastro_frota/placas_por_tipo',
PLACAS_POR_STATUS: '/dashs_frota/status_frota/placas_por_status',
PLACAS_POR_MANUTENCAO: '/dashs_frota/status_frota/placas_por_manutencao',
PLACAS_POR_PROPRIETARIO: '/dashs_frota/status_frota/placas_por_proprietario',
// PLACAS_POR_DISPONIBILIDADE: '/dashs_frota/disponibilidade/placas',
PLACAS_POR_UNIDADE: '/dashs_frota/monitoramento/placas_por_unidade',
PLACAS_POR_SINISTRO: '/dashs_frota/sinistro/placas_por_status',
PLACAS_POR_MANUTENCAO_STATUS: '/dashs_frota/manutencao/placas_por_manutencao_status',
QUANTITATIVO_MANUTENCAO: '/quantitativo_manutencao'
};
export const prafrotStatisticsService = {
getTotalPlacas: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.TOTAL_PLACAS)
}),
getPlacasPorBase: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_BASE)
}),
getPlacasPorAno: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_ANO)
}),
getPlacasPorCategoria: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_CATEGORIA)
}),
getPlacasPorModelo: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_MODELO)
}),
getPlacasPorTipo: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_TIPO)
}),
getPlacasPorStatus: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_STATUS)
}),
getPlacasPorManutencao: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_MANUTENCAO)
}),
getPlacasPorProprietario: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_PROPRIETARIO)
}),
/* getPlacasPorDisponibilidade: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_DISPONIBILIDADE)
}), */
getPlacasPorUnidade: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_UNIDADE)
}),
getPlacasPorSinistro: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_SINISTRO)
}),
getPlacasPorManutencaoStatus: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.PLACAS_POR_MANUTENCAO_STATUS)
}),
getQuantitativoManutencao: () => handleRequest({
apiFn: () => api.get(ENDPOINTS.QUANTITATIVO_MANUTENCAO)
})
};

View File

@ -1,129 +0,0 @@
/**
* Utilitário para traduzir erros HTTP em mensagens amigáveis ao usuário
* Sem expor detalhes técnicos ou códigos do servidor
*/
/**
* Traduz um erro HTTP em uma mensagem amigável ao usuário
* @param {Error} error - Objeto de erro do Axios ou erro genérico
* @returns {string} Mensagem amigável ao usuário
*/
export const getFriendlyErrorMessage = (error) => {
// Se o erro não tiver resposta (erro de rede, timeout, etc.)
if (!error.response) {
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
return 'A operação está demorando mais que o esperado. Por favor, tente novamente.';
}
if (error.message?.includes('Network Error') || error.message?.includes('ERR_NETWORK')) {
return 'Não foi possível conectar ao servidor. Verifique sua conexão com a internet.';
}
return 'Não foi possível completar a operação. Por favor, tente novamente.';
}
const status = error.response?.status;
const errorData = error.response?.data;
// Tenta usar mensagem do backend se for amigável
if (errorData?.message && typeof errorData.message === 'string') {
// Verifica se a mensagem não contém códigos técnicos
const technicalPatterns = [
/^HTTP \d{3}/,
/^Error \d{3}/,
/status code/i,
/status: \d{3}/i,
/\[.*\]/,
/at .*\.js/,
/line \d+/i,
/stack trace/i
];
const isTechnical = technicalPatterns.some(pattern => pattern.test(errorData.message));
if (!isTechnical) {
return errorData.message;
}
}
// Traduz códigos de status HTTP em mensagens amigáveis
switch (status) {
case 400:
// Bad Request - Dados inválidos
if (errorData?.errors && Array.isArray(errorData.errors)) {
const errors = errorData.errors.map(e => e.message || e).join(', ');
return `Verifique os dados informados: ${errors}`;
}
return 'Os dados informados estão incorretos. Por favor, verifique e tente novamente.';
case 401:
// Unauthorized - Não autorizado
return 'Sua sessão expirou. Por favor, faça login novamente.';
case 403:
// Forbidden - Acesso negado
return 'Você não tem permissão para realizar esta operação.';
case 404:
// Not Found - Recurso não encontrado
return 'A informação solicitada não foi encontrada.';
case 409:
// Conflict - Conflito
return 'Já existe um registro com essas informações. Verifique os dados e tente novamente.';
case 422:
// Unprocessable Entity - Erro de validação
if (errorData?.errors && Array.isArray(errorData.errors)) {
const errors = errorData.errors.map(e => e.message || e).join(', ');
return `Dados inválidos: ${errors}`;
}
return 'Os dados informados não são válidos. Verifique e tente novamente.';
case 500:
// Internal Server Error
return 'Ocorreu um erro no servidor. Nossa equipe foi notificada. Por favor, tente novamente em alguns instantes.';
case 502:
// Bad Gateway
return 'O servidor está temporariamente indisponível. Tente novamente em alguns instantes.';
case 503:
// Service Unavailable
return 'O serviço está temporariamente indisponível. Tente novamente em alguns instantes.';
case 504:
// Gateway Timeout
return 'A operação está demorando mais que o esperado. Por favor, tente novamente.';
default:
// Outros erros HTTP
if (status >= 500) {
return 'Ocorreu um erro no servidor. Por favor, tente novamente mais tarde.';
}
if (status >= 400) {
return 'Não foi possível processar sua solicitação. Verifique os dados e tente novamente.';
}
return 'Ocorreu um erro inesperado. Por favor, tente novamente.';
}
};
/**
* Extrai uma mensagem de erro amigável, priorizando mensagens do backend
* mas garantindo que sejam amigáveis
* @param {Error} error - Objeto de erro
* @returns {string} Mensagem amigável
*/
export const extractFriendlyMessage = (error) => {
// Se já temos uma mensagem amigável do backend, usa ela
if (error.response?.data?.message && typeof error.response.data.message === 'string') {
const msg = error.response.data.message;
// Verifica se não é uma mensagem técnica
if (!msg.match(/^(HTTP|Error|status|\[|at |line )/i)) {
return msg;
}
}
// Caso contrário, usa a tradução baseada no status
return getFriendlyErrorMessage(error);
};

View File

@ -1,211 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useAvailability } from '../hooks/useAvailability';
import { useVehicles } from '../hooks/useVehicles';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search, Calendar as CalendarIcon } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
import { toast } from 'sonner';
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-lg font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
export default function AvailabilityView() {
const { availabilities, fetchAvailabilities, createAvailability, updateAvailability, deleteAvailability } = useAvailability();
const { vehicles, fetchVehicles } = useVehicles();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const initialFormState = {
iddisponibilidade_frota: '', idveiculo_frota: '', placa: '', disponibilidade: '', status_disponibilidade: 'Disponível'
};
const [formData, setFormData] = useState(initialFormState);
useEffect(() => {
fetchAvailabilities();
fetchVehicles();
}, []);
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...initialFormState, ...item });
} else {
setEditingItem(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const handlePlateChange = (e) => {
const val = e.target.value;
const foundVehicle = vehicles.find(v => v.placa === val);
setFormData({
...formData,
placa: val,
idveiculo_frota: foundVehicle ? foundVehicle.idveiculo_frota : ''
});
};
const handleSubmit = async (e) => {
e.preventDefault();
const payload = {
...formData,
iddisponibilidade_frota: formData.iddisponibilidade_frota ? Number(formData.iddisponibilidade_frota) : undefined,
idveiculo_frota: formData.idveiculo_frota ? Number(formData.idveiculo_frota) : null
};
let success;
if (editingItem) {
success = await updateAvailability(Number(editingItem.iddisponibilidade_frota || editingItem.id), payload);
} else {
success = await createAvailability(payload);
}
if (success) setIsModalOpen(false);
};
const filteredData = availabilities.filter(item =>
item.placa?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Disponibilidade e Agenda</h1>
<p className="text-slate-500 text-sm">Visualização de disponibilidade da frota.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Registro
</DarkButton>
</div>
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
columns={[
{ header: 'ID', field: 'iddisponibilidade_frota', width: '80px' },
{ header: 'VEÍCULO ID', field: 'idveiculo_frota', width: '100px' },
{ header: 'PLACA', field: 'placa', width: '120px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'DATA', field: 'disponibilidade', width: '180px', render: (row) => row.disponibilidade?.split('T')[0] },
{ header: 'STATUS', field: 'status_disponibilidade', width: '150px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border ${
row.status_disponibilidade === 'Disponível' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-red-500/10 text-red-500 border-red-500/20'
}`}>
{row.status_disponibilidade}
</span>
)}
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'Buscar placa...' },
{ field: 'status_disponibilidade', label: 'Status', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteAvailability(item.iddisponibilidade_frota)}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-md bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a]">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold">
{editingItem ? `Editando ID: ${editingItem.iddisponibilidade_frota}` : 'Nova Disponibilidade'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-4 max-h-[75vh] overflow-y-auto custom-scrollbar">
{formData.iddisponibilidade_frota && (
<div className="bg-orange-500/5 p-3 rounded-xl border border-orange-500/10 mb-2">
<p className="text-[10px] uppercase font-bold text-orange-500/60 tracking-widest">ID do Registro</p>
<p className="text-lg font-bold text-orange-500">{formData.iddisponibilidade_frota}</p>
</div>
)}
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Placa (Pesquisar)</label>
<input
list="veiculos-list"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
value={formData.placa}
onChange={handlePlateChange}
placeholder="Digite ou selecione a placa..."
required
/>
<datalist id="veiculos-list">
{vehicles.map(v => (
<option key={v.idveiculo_frota} value={v.placa} />
))}
</datalist>
{formData.idveiculo_frota && (
<span className="text-[10px] text-green-500 ml-1">Veículo vinculado (ID: {formData.idveiculo_frota})</span>
)}
</div>
<DarkInput type="date" label="Data de Disponibilidade" value={formData.disponibilidade?.split('T')[0]} onChange={e => setFormData({...formData, disponibilidade: e.target.value})} />
<DarkSelect label="Status" options={['Disponível', 'Indisponível', 'Reserva']} value={formData.status_disponibilidade} onChange={v => setFormData({...formData, status_disponibilidade: v})} />
<DialogFooter className="bg-slate-50 dark:bg-transparent border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit">Salvar</DarkButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,380 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useClaims } from '../hooks/useClaims';
import { useVehicles } from '../hooks/useVehicles';
import { useDrivers } from '../hooks/useDrivers';
import AutocompleteInput from '../components/AutocompleteInput';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
export default function ClaimsView() {
const { claims, loading, fetchClaims, createClaim, updateClaim, deleteClaim } = useClaims();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const initialFormState = {
agendamento: '', ano_fabricacao: '', ano_modelo: '', atuacao: '', base: '',
categoria: '', chassi: '', cmou: '', combustivel: '', contrato: '',
coordenador: '', cor: '', d: '', data_entrada: '', data_limite: '',
dias_parado: '', dispatcher: '', fabricante: '', fiscal_operacao: '',
geotab: 'NÃO', gestor: '', id_rota: '', idsinistro_devolucao_frota: '',
km_atual: '', km_preventiva: '', melifleet: 'No', modelo: '', motorista: '',
obs: '', placa: '', pooltrack: 'NÃO', previsao: '', primeira_locacao: '',
proprietario: '', prox_preventiva: '', renavan: '', sascar: 'NÃO',
status: '', t4s: 'NÃO', tipo_frota: '', tipo_placa: '', u: 'SIM',
uf: '', ultima_preventiva: '', valor_aluguel: '', valor_fipe: ''
};
const [formData, setFormData] = useState(initialFormState);
const { vehicles, fetchVehicles } = useVehicles();
const { drivers, fetchDrivers } = useDrivers();
useEffect(() => {
fetchClaims();
fetchVehicles();
fetchDrivers();
}, []);
const handleVehicleSelect = (vehicle) => {
if (!vehicle) return;
setFormData(prev => ({
...prev,
placa: vehicle.placa || prev.placa,
chassi: vehicle.chassi || prev.chassi,
renavan: vehicle.renavam || vehicle.renavan || prev.renavan,
modelo: vehicle.modelo || vehicle.mod_veiculo || prev.modelo,
fabricante: vehicle.fabricante || vehicle.marca || prev.fabricante,
cor: vehicle.cor || prev.cor,
ano_fabricacao: vehicle.ano_fabricacao || vehicle.ano_fab || prev.ano_fabricacao,
ano_modelo: vehicle.ano_modelo || vehicle.ano_mod || prev.ano_modelo,
combustivel: vehicle.combustivel || prev.combustivel,
categoria: vehicle.categoria || vehicle.cat_veiculo || prev.categoria,
tipo_placa: vehicle.tipo_placa || prev.tipo_placa,
proprietario: vehicle.proprietario || prev.proprietario,
// Add more fields if available in vehicle object
}));
};
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...initialFormState, ...item });
} else {
setEditingItem(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
let success;
if (editingItem) {
success = await updateClaim(editingItem.idsinistro_devolucao_frota, formData);
} else {
success = await createClaim(formData);
}
if (success) setIsModalOpen(false);
};
const filteredData = Array.isArray(claims) ? claims.filter(item =>
item.placa?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.motorista?.toLowerCase().includes(searchTerm.toLowerCase())
) : [];
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Sinistros e Devoluções</h1>
<p className="text-slate-500 text-sm">Gestão de incidentes e movimentações de frota.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar registro..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Registro
</DarkButton>
</div>
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
loading={loading}
columns={[
{ header: 'ID', field: 'idsinistro_devolucao_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS', field: 'status', width: '120px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border ${
row.status === 'Sinistro' ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' :
row.status === 'Devolução' ? 'bg-blue-500/10 text-blue-500 border-blue-500/20' :
'bg-slate-500/10 text-slate-500 border-slate-500/20'
}`}>
{row.status}
</span>
)},
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
{ header: 'DATA ENTRADA', field: 'data_entrada', width: '120px' },
{ header: 'PREVISÃO', field: 'previsao', width: '120px' },
{ header: 'DIAS PARADO', field: 'dias_parado', width: '100px', className: 'text-red-400 font-bold' },
{ header: 'MODELO', field: 'modelo', width: '120px' },
{ header: 'FABRICANTE', field: 'fabricante', width: '120px' },
{ header: 'COMBUSTÍVEL', field: 'combustivel', width: '120px' },
{ header: 'ANO FAB.', field: 'ano_fabricacao', width: '80px' },
{ header: 'ANO MOD.', field: 'ano_modelo', width: '80px' },
{ header: 'TIPO FROTA', field: 'tipo_frota', width: '120px' },
{ header: 'CATEGORIA', field: 'categoria', width: '120px' },
{ header: 'TIPO PLACA', field: 'tipo_placa', width: '100px' },
{ header: 'BASE', field: 'base', width: '100px' },
{ header: 'UF', field: 'uf', width: '60px' },
{ header: 'GESTOR', field: 'gestor', width: '140px' },
{ header: 'COORDENADOR', field: 'coordenador', width: '140px' },
{ header: 'DISPATCHER', field: 'dispatcher', width: '140px' },
{ header: 'FISCAL OPER.', field: 'fiscal_operacao', width: '140px' },
{ header: 'ATUAÇÃO', field: 'atuacao', width: '140px' },
{ header: 'ID ROTA', field: 'id_rota', width: '120px' },
{ header: 'AGENDAMENTO', field: 'agendamento', width: '120px' },
{ header: 'PROPRIETÁRIO', field: 'proprietario', width: '140px' },
{ header: 'CONTRATO', field: 'contrato', width: '120px' },
{ header: 'DATA LIMITE', field: 'data_limite', width: '110px' },
{ header: '1ª LOCAÇÃO', field: 'primeira_locacao', width: '110px' },
{ header: 'KM ATUAL', field: 'km_atual', width: '100px' },
{ header: 'KM PREV.', field: 'km_preventiva', width: '100px' },
{ header: 'ÚLT. PREV.', field: 'ultima_preventiva', width: '100px' },
{ header: 'PRÓX. PREV.', field: 'prox_preventiva', width: '100px' },
{ header: 'MELIFLEET', field: 'melifleet', width: '100px' },
{ header: 'GEOTAB', field: 'geotab', width: '80px' },
{ header: 'SASCAR', field: 'sascar', width: '80px' },
{ header: 'POOLTRACK', field: 'pooltrack', width: '80px' },
{ header: 'T4S', field: 't4s', width: '80px' },
{ header: 'RENAVAN', field: 'renavan', width: '150px' },
{ header: 'CHASSI', field: 'chassi', width: '180px' },
{ header: 'CMOU', field: 'cmou', width: '80px' },
{ header: 'D', field: 'd', width: '100px' },
{ header: 'U', field: 'u', width: '100px' },
{ header: 'VALOR ALUGUEL', field: 'valor_aluguel', width: '120px' },
{ header: 'VALOR FIPE', field: 'valor_fipe', width: '120px' },
{ header: 'OBS', field: 'obs', width: '200px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'Buscar placa...' },
{ field: 'status', label: 'Status', type: 'select' },
{ field: 'base', label: 'Base', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteClaim(item.idsinistro_devolucao_frota)}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-4xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden shadow-2xl">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a]">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold">
{editingItem ? 'Editar Registro' : 'Novo Registro (Sinistro/Devolução)'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-stone-500">
Informações detalhadas sobre o evento.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-4 max-h-[75vh] overflow-y-auto custom-scrollbar">
<Tabs defaultValue="basicos" className="w-full">
<TabsList className="bg-slate-100 dark:bg-[#141414] border border-slate-200 dark:border-[#2a2a2a] w-full justify-start p-1 h-auto mb-4">
{['basicos', 'ocorrencia', 'operacional', 'financeiro', 'tecnico'].map(tab => (
<TabsTrigger
key={tab}
value={tab}
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
>
{tab}
</TabsTrigger>
))}
</TabsList>
<div className="min-h-[300px]">
<TabsContent value="basicos" className="space-y-4 m-0 pb-4">
<div className="grid grid-cols-3 gap-4">
<AutocompleteInput
label="Placa"
value={formData.placa}
onChange={v => setFormData({...formData, placa: v})}
options={vehicles}
displayKey="placa"
valueKey="placa"
searchKeys={['placa']}
onSelect={handleVehicleSelect}
required
/>
<DarkInput label="Chassi" value={formData.chassi} onChange={e => setFormData({...formData, chassi: e.target.value})} />
<DarkInput label="Renavam" value={formData.renavan} onChange={e => setFormData({...formData, renavan: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Modelo" value={formData.modelo} onChange={e => setFormData({...formData, modelo: e.target.value})} />
<DarkInput label="Fabricante" value={formData.fabricante} onChange={e => setFormData({...formData, fabricante: e.target.value})} />
<DarkInput label="Cor" value={formData.cor} onChange={e => setFormData({...formData, cor: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Ano Fab." value={formData.ano_fabricacao} onChange={e => setFormData({...formData, ano_fabricacao: e.target.value})} />
<DarkInput label="Ano Mod." value={formData.ano_modelo} onChange={e => setFormData({...formData, ano_modelo: e.target.value})} />
<DarkInput label="Combustível" value={formData.combustivel} onChange={e => setFormData({...formData, combustivel: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Categoria" value={formData.categoria} onChange={e => setFormData({...formData, categoria: e.target.value})} />
<DarkInput label="Tipo Placa" value={formData.tipo_placa} onChange={e => setFormData({...formData, tipo_placa: e.target.value})} />
</div>
</TabsContent>
<TabsContent value="ocorrencia" className="space-y-4 m-0 pb-4">
<div className="grid grid-cols-3 gap-4">
<DarkSelect label="Status" options={['Sinistro', 'Devolução', 'Venda', 'Manutenção']} value={formData.status} onChange={v => setFormData({...formData, status: v})} />
<DarkInput type="date" label="Data Entrada" value={formData.data_entrada?.split('T')[0]} onChange={e => setFormData({...formData, data_entrada: e.target.value})} />
<DarkInput type="date" label="Previsão" value={formData.previsao?.split('T')[0]} onChange={e => setFormData({...formData, previsao: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Dias Parado" value={formData.dias_parado} onChange={e => setFormData({...formData, dias_parado: e.target.value})} />
<DarkInput label="Agendamento" value={formData.agendamento} onChange={e => setFormData({...formData, agendamento: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="D (Col)" value={formData.d} onChange={e => setFormData({...formData, d: e.target.value})} />
<DarkInput label="U (Col)" value={formData.u} onChange={e => setFormData({...formData, u: e.target.value})} />
</div>
<div className="gap-1.5 flex flex-col">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 min-h-[80px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
</div>
</TabsContent>
<TabsContent value="operacional" className="space-y-4 m-0 pb-4">
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Base" value={formData.base} onChange={e => setFormData({...formData, base: e.target.value})} />
<DarkInput label="UF" value={formData.uf} onChange={e => setFormData({...formData, uf: e.target.value})} />
<DarkInput label="Proprietário" value={formData.proprietario} onChange={e => setFormData({...formData, proprietario: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<AutocompleteInput
label="Motorista"
value={formData.motorista}
onChange={v => setFormData({...formData, motorista: v})}
options={drivers}
displayKey="NOME_FAVORECIDO"
valueKey="NOME_FAVORECIDO"
placeholder="Buscar motorista..."
/>
<DarkInput label="ID Rota" value={formData.id_rota} onChange={e => setFormData({...formData, id_rota: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Gestor" value={formData.gestor} onChange={e => setFormData({...formData, gestor: e.target.value})} />
<DarkInput label="Coordenador" value={formData.coordenador} onChange={e => setFormData({...formData, coordenador: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Dispatcher" value={formData.dispatcher} onChange={e => setFormData({...formData, dispatcher: e.target.value})} />
<DarkInput label="Fiscal Operação" value={formData.fiscal_operacao} onChange={e => setFormData({...formData, fiscal_operacao: e.target.value})} />
</div>
<DarkInput label="Atuação" value={formData.atuacao} onChange={e => setFormData({...formData, atuacao: e.target.value})} />
</TabsContent>
<TabsContent value="financeiro" className="space-y-4 m-0 pb-4">
<div className="grid grid-cols-2 gap-4">
<DarkInput type="number" label="Valor FIPE" value={formData.valor_fipe} onChange={e => setFormData({...formData, valor_fipe: e.target.value})} />
<DarkInput type="number" label="Valor Aluguel" value={formData.valor_aluguel} onChange={e => setFormData({...formData, valor_aluguel: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Contrato" value={formData.contrato} onChange={e => setFormData({...formData, contrato: e.target.value})} />
<DarkInput type="date" label="Data Limite" value={formData.data_limite?.split('T')[0]} onChange={e => setFormData({...formData, data_limite: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Tipo Frota" value={formData.tipo_frota} onChange={e => setFormData({...formData, tipo_frota: e.target.value})} />
<DarkInput type="date" label="Primeira Locação" value={formData.primeira_locacao?.split('T')[0]} onChange={e => setFormData({...formData, primeira_locacao: e.target.value})} />
</div>
</TabsContent>
<TabsContent value="tecnico" className="space-y-4 m-0 pb-4">
<div className="grid grid-cols-3 gap-4">
<DarkInput label="KM Preventiva" value={formData.km_preventiva} onChange={e => setFormData({...formData, km_preventiva: e.target.value})} />
<DarkInput label="KM Atual" value={formData.km_atual} onChange={e => setFormData({...formData, km_atual: e.target.value})} />
<DarkInput label="CMOU" value={formData.cmou} onChange={e => setFormData({...formData, cmou: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput type="date" label="Última Prev." value={formData.ultima_preventiva?.split('T')[0]} onChange={e => setFormData({...formData, ultima_preventiva: e.target.value})} />
<DarkInput type="date" label="Próx. Prev." value={formData.prox_preventiva?.split('T')[0]} onChange={e => setFormData({...formData, prox_preventiva: e.target.value})} />
</div>
<div className="grid grid-cols-4 gap-4 pt-2">
{['geotab', 'sascar', 'pooltrack', 't4s'].map(tracker => (
<DarkSelect key={tracker} label={tracker.toUpperCase()} options={['SIM', 'NÃO']} value={formData[tracker]} onChange={v => setFormData({...formData, [tracker]: v})} />
))}
</div>
</TabsContent>
</div>
</Tabs>
</form>
<DialogFooter className="sticky bottom-0 bg-white dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit" onClick={handleSubmit}>Salvar Registro</DarkButton>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,364 +0,0 @@
import React, { useEffect, useState, useMemo } from 'react';
import { prafrotService } from '../services/prafrotService';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search, Settings, Save, X, Edit, Trash2 } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
// --- Components Styled ---
const SidebarItem = ({ active, label, onClick }) => (
<button
onClick={onClick}
className={`w-full text-left px-4 py-3 text-sm font-medium transition-colors border-l-2 ${
active
? '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'
}`}
>
{label}
</button>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-lg font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white",
danger: "bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-500 border border-red-500/20"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
const DarkInput = ({ label, ...props }) => (
<div className="space-y-2">
{label && <label className="text-[11px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1 block">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2.5 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
export default function ConfigView() {
const [configOptions, setConfigOptions] = useState({});
const [selectedRoute, setSelectedRoute] = useState(null);
const [items, setItems] = useState([]);
const [loadingItems, setLoadingItems] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [formData, setFormData] = useState({});
// Fetch Config Options on Mount
useEffect(() => {
const loadConfig = async () => {
try {
const data = await prafrotService.getListasConfig();
setConfigOptions(data);
// Select first option by default
const keys = Object.keys(data);
if (keys.length > 0) {
setSelectedRoute(data[keys[0]].rota);
}
} catch (error) {
console.error("Error loading config options:", error);
}
};
loadConfig();
}, []);
// Fetch Items when Selected Route changes
useEffect(() => {
if (!selectedRoute) return;
const loadItems = async () => {
setLoadingItems(true);
try {
const data = await prafrotService.getListasByRoute(selectedRoute);
// Ensure data is array
const list = Array.isArray(data) ? data : (data.results || []);
setItems(list);
} catch (error) {
console.error(`Error loading items for ${selectedRoute}:`, error);
setItems([]);
} finally {
setLoadingItems(false);
}
};
loadItems();
setSearchTerm('');
}, [selectedRoute]);
// Derived columns from first item
const columns = useMemo(() => {
if (!items || items.length === 0) return [];
const firstItem = items[0];
const keys = Object.keys(firstItem);
return keys
.filter(key => {
const lowerKey = key.toLowerCase();
// Hide ID, created_at, updated_at, deleted_at, active
return !lowerKey.startsWith('id') && !['created_at', 'updated_at', 'deleted_at'].includes(lowerKey);
})
.map(key => ({
header: key.toUpperCase().replace(/_/g, ' '),
field: key,
width: '200px'
}));
}, [items]);
const filteredItems = useMemo(() => {
if (!items) return [];
if (!searchTerm) return items;
const lower = searchTerm.toLowerCase();
return items.filter(item =>
Object.values(item).some(val =>
String(val).toLowerCase().includes(lower)
)
);
}, [items, searchTerm]);
// Actions
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...item });
} else {
setEditingItem(null);
const emptyState = {};
if (items.length > 0) {
Object.keys(items[0]).forEach(k => emptyState[k] = '');
} else {
// Fallback default fields
emptyState.label = '';
emptyState.valor = '';
}
setFormData(emptyState);
}
setIsModalOpen(true);
};
const handleSave = async (e) => {
e.preventDefault();
if (!selectedRoute) return;
try {
// Create payload sending info as 'valor'
// We extract all non-ID fields. If there's only one, we map it to 'valor'.
// If there are multiple, we prioritize fields already named 'valor' or 'value'.
const payload = {};
const nonIdKeys = Object.keys(formData).filter(key => {
const lowerKey = key.toLowerCase();
return !lowerKey.startsWith('id') && !['created_at', 'updated_at', 'deleted_at'].includes(lowerKey);
});
if (nonIdKeys.length === 1) {
payload.valor = formData[nonIdKeys[0]];
} else if (nonIdKeys.length > 1) {
// If multiple, send all but ensure 'valor' is present if 'value' was used
nonIdKeys.forEach(k => {
if (k === 'value') payload.valor = formData[k];
else payload[k] = formData[k];
});
if (formData.valor) payload.valor = formData.valor;
} else {
// Fallback for empty schema
payload.label = formData.label;
payload.valor = formData.valor || formData.value;
}
if (editingItem) {
const idKey = Object.keys(editingItem).find(k => k.toLowerCase().startsWith('id'));
const idVal = editingItem[idKey];
if (!idVal) {
alert("Não foi possível identificar o ID deste item.");
return;
}
await prafrotService.updateLista(selectedRoute, idVal, payload);
} else {
await prafrotService.createLista(selectedRoute, payload);
}
const data = await prafrotService.getListasByRoute(selectedRoute);
setItems(Array.isArray(data) ? data : (data.results || []));
setIsModalOpen(false);
} catch (error) {
console.error("Error saving:", error);
alert("Erro ao salvar. Verifique o console.");
}
};
const handleDelete = async (item) => {
if (!window.confirm("Tem certeza que deseja excluir este item?")) return;
const idKey = Object.keys(item).find(k => k.toLowerCase().startsWith('id'));
const idVal = item[idKey];
if (!idVal) {
alert("Não foi possível identificar o ID deste item para exclusão.");
return;
}
try {
await prafrotService.deleteLista(selectedRoute, idVal);
// Refresh list
const data = await prafrotService.getListasByRoute(selectedRoute);
setItems(Array.isArray(data) ? data : (data.results || []));
} catch (error) {
console.error("Erro ao excluir:", error);
alert("Erro ao excluir o item. Verifique o console.");
}
};
const selectedLabel = configOptions[selectedRoute]?.label || selectedRoute;
return (
<div className="flex h-screen bg-slate-50 dark:bg-[#0f0f0f] overflow-hidden">
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-[#141414] border-r border-slate-200 dark:border-[#2a2a2a] flex flex-col">
<div className="p-4 border-b border-slate-200 dark:border-[#2a2a2a]">
<h2 className="text-lg font-bold text-slate-800 dark:text-white flex items-center gap-2">
<Settings className="text-orange-500" size={20} />
Configurações
</h2>
<p className="text-xs text-slate-500 mt-1">Gerencie as listas do sistema</p>
</div>
<div className="flex-1 overflow-y-auto py-2 custom-scrollbar">
{Object.entries(configOptions).map(([key, opt]) => (
<SidebarItem
key={key}
active={selectedRoute === opt.rota}
label={opt.label}
onClick={() => setSelectedRoute(opt.rota)}
/>
))}
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col min-w-0">
{/* Header */}
<div className="bg-white dark:bg-[#141414] border-b border-slate-200 dark:border-[#2a2a2a] px-8 py-5 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">{selectedLabel}</h1>
<p className="text-slate-500 text-sm">Gerenciamento de opções para {selectedLabel?.toLowerCase()}</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-slate-50 dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-orange-500 w-64"
placeholder="Pesquisar..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Item
</DarkButton>
</div>
</div>
{/* Table */}
<div className="flex-1 p-8 overflow-hidden">
<div className="h-full bg-white dark:bg-[#141414] rounded-xl shadow-sm border border-slate-200 dark:border-[#2a2a2a] overflow-hidden flex flex-col">
{loadingItems ? (
<div className="flex-1 flex items-center justify-center text-slate-500">
Carregando...
</div>
) : (
<ExcelTable
data={filteredItems}
columns={columns}
loading={loadingItems}
onEdit={handleOpenModal}
onDelete={handleDelete} // Optional
// Fallback ID key logic usually generic in ExcelTable or pass rowKey
rowKey={(row) => {
// Try to find the primary key dynamically
const idKey = Object.keys(row).find(k => k.toLowerCase().startsWith('id'));
return idKey ? row[idKey] : Math.random();
}}
/>
)}
</div>
</div>
</div>
{/* Modal */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200">
<DialogHeader className="border-b border-slate-200 dark:border-[#2a2a2a] pb-4">
<DialogTitle className="text-slate-800 dark:text-white">
{editingItem ? `Editar ${selectedLabel}` : `Novo Item em ${selectedLabel}`}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSave} className="py-4 grid grid-cols-1 gap-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
{/* Dynamic Form Generation */}
{Object.keys(formData).map(key => {
// Hide ID fields completely from form
const isId = key.toLowerCase().startsWith('id') || key === 'created_at' || key === 'updated_at';
if (isId) return null;
return (
<DarkInput
key={key}
label={key.replace(/_/g, ' ')}
value={formData[key] || ''}
onChange={e => setFormData({...formData, [key]: e.target.value})}
/>
);
})}
{/* Fallback if formData is empty (new item with unknown schema) */}
{Object.keys(formData).length === 0 && (
<div className="text-center text-slate-500 p-4">
Não é possível criar um item sem conhecer a estrutura. Adicione a lógica de campos manuais se necessário.
(Normalmente, campos seriam: Nome, Descrição, etc.)
<div className="mt-4 space-y-2">
<DarkInput
label="Label / Nome"
value={formData['label'] || ''}
onChange={e => setFormData({...formData, label: e.target.value})}
/>
<DarkInput
label="Valor / Código"
value={formData['valor'] || ''}
onChange={e => setFormData({...formData, valor: e.target.value})}
/>
</div>
</div>
)}
</form>
<DialogFooter className="border-t border-slate-200 dark:border-[#2a2a2a] pt-4">
<DarkButton variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton onClick={handleSave}>
<Save size={16} /> Salvar
</DarkButton>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,139 +0,0 @@
import React from 'react';
import {
Truck, AlertCircle, Clock, CheckCircle2,
TrendingUp, TrendingDown, DollarSign, MapPin
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
const StatCard = ({ title, value, subtext, icon: Icon, color, trend }) => (
<Card className="bg-[#1c1c1c] border-[#2a2a2a] text-slate-200 overflow-hidden relative group hover:border-[#333] transition-all">
<div className={`absolute top-0 left-0 w-1 h-full ${color}`} />
<CardContent className="p-6">
<div className="flex justify-between items-start">
<div>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500 mb-1">{title}</p>
<h3 className="text-3xl font-bold text-white tracking-tight">{value}</h3>
{subtext && <p className="text-xs text-slate-500 mt-1 font-medium">{subtext}</p>}
</div>
<div className={`p-3 rounded-xl bg-opacity-10 ${color.replace('bg-', 'bg-').replace('w-1', '')} ${color.replace('bg-', 'text-')}`}>
<Icon size={24} strokeWidth={2.5} />
</div>
</div>
{trend && (
<div className="mt-4 flex items-center gap-2">
<span className={`text-xs font-bold px-1.5 py-0.5 rounded flex items-center gap-1 ${trend > 0 ? 'text-orange-500 bg-orange-500/10' : 'text-red-500 bg-red-500/10'}`}>
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{Math.abs(trend)}%
</span>
<span className="text-[10px] text-slate-600 uppercase font-bold">vs mês anterior</span>
</div>
)}
</CardContent>
</Card>
);
export default function DashboardView() {
return (
<div className="space-y-8">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-white tracking-tight">Visão Geral</h1>
<p className="text-slate-500 text-sm">Monitoramento em tempo real da operação.</p>
</div>
{/* KPI Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="Frota Total"
value="1,240"
subtext="1033 em operação"
icon={Truck}
color="bg-blue-600"
trend={12.5}
/>
<StatCard
title="Manutenção"
value="28"
subtext="4 críticos"
icon={AlertCircle}
color="bg-red-500"
trend={-2.4}
/>
<StatCard
title="Disponibilidade"
value="94.2%"
subtext="Meta: 95%"
icon={CheckCircle2}
color="bg-orange-500"
trend={1.8}
/>
<StatCard
title="Custo Médio"
value="R$ 2.4k"
subtext="Por veículo/mês"
icon={DollarSign}
color="bg-yellow-500"
trend={-0.5}
/>
</div>
{/* Secondary Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="bg-[#1c1c1c] border-[#2a2a2a] lg:col-span-2">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-bold text-white text-lg">Distribuição por Base</h3>
<button className="text-xs font-bold text-yellow-500 hover:text-yellow-400">VER RELATÓRIO</button>
</div>
<div className="space-y-4">
{[
{ label: 'SP - Capital (SRJ10)', val: 450, tot: 1240, col: 'bg-blue-600' },
{ label: 'RJ - Rio de Janeiro (GIG)', val: 320, tot: 1240, col: 'bg-orange-500' },
{ label: 'MG - Belo Horizonte', val: 210, tot: 1240, col: 'bg-yellow-500' },
{ label: 'Outras Bases', val: 260, tot: 1240, col: 'bg-slate-600' },
].map((item, i) => (
<div key={i} className="space-y-1">
<div className="flex justify-between text-xs font-medium text-slate-400">
<span>{item.label}</span>
<span className="text-white">{item.val}</span>
</div>
<div className="h-2 bg-[#2a2a2a] rounded-full overflow-hidden">
<div className={`h-full ${item.col} rounded-full`} style={{ width: `${(item.val / item.tot) * 100}%` }} />
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="bg-[#1c1c1c] border-[#2a2a2a]">
<CardContent className="p-6">
<h3 className="font-bold text-white text-lg mb-6">Alertas Recentes</h3>
<div className="space-y-4">
{[
{ msg: 'Manutenção Preventiva - ABC-1234', time: 'Há 2h', type: 'warn' },
{ msg: 'Multa Registrada - XYZ-9876', time: 'Há 4h', type: 'crit' },
{ msg: 'Novo Veículo Cadastrado', time: 'Há 5h', type: 'info' },
{ msg: 'Checklist Atrasado - GOL-5544', time: 'Ontem', type: 'warn' },
].map((alert, i) => (
<div key={i} className="flex items-start gap-3 pb-3 border-b border-[#2a2a2a] last:border-0 last:pb-0">
<div className={`w-2 h-2 mt-1.5 rounded-full ${alert.type === 'crit' ? 'bg-red-500' : alert.type === 'warn' ? 'bg-yellow-500' : 'bg-blue-500'}`} />
<div>
<p className="text-sm text-slate-300 font-medium">{alert.msg}</p>
<span className="text-[10px] text-slate-500 uppercase font-bold flex items-center gap-1">
<Clock size={10} /> {alert.time}
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -1,59 +0,0 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatcher } from '../hooks/useDispatcher';
import ExcelTable from '../components/ExcelTable';
const DispatcherView = () => {
const { data, loading, fetchData } = useDispatcher();
useEffect(() => {
fetchData();
}, [fetchData]);
const columns = useMemo(() => {
if (!data || data.length === 0) return [];
// Take the first item to generate columns
const firstItem = data[0];
return Object.keys(firstItem).map(key => ({
field: key,
header: key.replace(/_/g, ' '), // Simple formatter: replace underscores with spaces
width: 150, // Default width
className: 'text-xs'
}));
}, [data]);
const filterDefs = useMemo(() => {
// Create filters for all columns by default, or maybe just some specific ones?
// For "advanced filters", let's just enable all of them as 'select' type for now
return columns.map(col => ({
field: col.field,
label: col.header,
type: 'select'
}));
}, [columns]);
return (
<div className="h-full flex flex-col p-4 gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">Dispatcher</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">Visualização de dados do Dispatcher.</p>
</div>
<div className="flex-1 min-h-0">
<ExcelTable
data={data}
columns={columns}
filterDefs={filterDefs}
loading={loading}
pageSize={50}
/>
</div>
</div>
);
};
export default DispatcherView;

View File

@ -1,92 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useDrivers } from '../hooks/useDrivers';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
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-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
export default function DriversView() {
const { drivers, fetchDrivers } = useDrivers();
const [searchTerm, setSearchTerm] = useState('');
// Note: Drivers View is Read-Only for now as per instructions (just listing)
// If editing is needed, we can implement it later.
useEffect(() => {
fetchDrivers();
}, []);
const filteredData = Array.isArray(drivers) ? drivers.filter(item =>
item.NOME_FAVORECIDO?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.CPF_CNPJ_FAVORECIDO?.includes(searchTerm)
) : [];
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Motoristas</h1>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar motorista..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
</div>
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
columns={[
{ header: 'NOME', field: 'NOME_FAVORECIDO', width: '300px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'CPF/CNPJ', field: 'CPF_CNPJ_FAVORECIDO', width: '180px' },
{ header: 'TELEFONE', field: 'TELEFONE', width: '150px' },
{ header: 'ENDEREÇO', field: 'ENDERECO', width: '250px' },
{ header: 'BASE', field: 'BASE', width: '150px' },
]}
filterDefs={[
{ field: 'NOME_FAVORECIDO', label: 'Nome', type: 'text' },
{ field: 'BASE', label: 'Base', type: 'select' },
]}
// Edit disabled for now
/>
</div>
</div>
);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,224 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useMoki } from '../hooks/useMoki';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-lg font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
export default function MokiView() {
const { mokis, fetchMokis, createMoki, updateMoki, deleteMoki } = useMoki();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const initialFormState = {
aprovacao: '', autor: '', checklist: '', cod_unidade: '', data: '',
idmoki_frota: '', nome_unidade: '', origem: '', status: 'Não Conforme',
status_checklist: '', status_unidade: '', unidade: ''
};
const [formData, setFormData] = useState(initialFormState);
useEffect(() => {
fetchMokis();
}, []);
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({
...initialFormState,
...item,
data: (item.data || item.data_moki || item.data_checklist || '').split('T')[0]
});
} else {
setEditingItem(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
const { idmoki_frota, ...rest } = formData;
const payload = {
...rest,
id: idmoki_frota ? Number(idmoki_frota) : undefined
};
let success;
if (editingItem) {
success = await updateMoki(Number(idmoki_frota || editingItem.id), payload);
} else {
success = await createMoki(payload);
}
if (success) setIsModalOpen(false);
};
const filteredData = mokis.filter(item =>
item.checklist?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.unidade?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.nome_unidade?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="space-y-6 min-w-0 overflow-hidden">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Checklists Moki</h1>
<p className="text-slate-500 text-sm">Inspeções e vistorias realizadas.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="w-full md:w-64 bg-white dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-10 pr-4 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
placeholder="Buscar checklist..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Checklist
</DarkButton>
</div>
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
columns={[
{ header: 'ID', field: 'idmoki_frota', width: '80px' },
{ header: 'DATA', field: 'data', width: '100px', render: (row) => row.data?.split('T')[0] || row.data_checklist?.split('T')[0] },
{ header: 'CHECKLIST', field: 'checklist', width: '220px', className: 'font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS CHECKLIST', field: 'status_checklist', width: '150px' },
{ header: 'STATUS', field: 'status', width: '120px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border ${
row.status === 'Não Conforme' ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' :
row.status === 'Aprovado' || row.status === 'Conforme' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-amber-500/10 text-amber-500 border-amber-500/20'
}`}>
{row.status || 'Pendente'}
</span>
)},
{ header: 'UNIDADE', field: 'unidade', width: '150px' },
{ header: 'COD. UNIDADE', field: 'cod_unidade', width: '120px' },
{ header: 'NOME UNIDADE', field: 'nome_unidade', width: '180px' },
{ header: 'STATUS UNIDADE', field: 'status_unidade', width: '120px' },
{ header: 'AUTOR', field: 'autor', width: '120px' },
{ header: 'ORIGEM', field: 'origem', width: '120px' },
{ header: 'APROVAÇÃO', field: 'aprovacao', width: '120px' },
]}
filterDefs={[
{ field: 'origem', label: 'Origem', type: 'select' },
{ field: 'unidade', label: 'Unidade', type: 'select' },
{ field: 'checklist', label: 'Checklist', type: 'select' },
{ field: 'status', label: 'Status', type: 'select' },
{ field: 'autor', label: 'Autor', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteMoki(item.idmoki_frota)}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-4xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a]">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold">
{editingItem ? `Editando Checklist ID: ${editingItem.idmoki_frota}` : 'Novo Checklist Moki'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-stone-500">
Informações da vistoria via sistema Moki.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-4 space-y-4 max-h-[70vh] overflow-y-auto custom-scrollbar">
<div className="bg-orange-500/5 p-4 rounded-xl border border-orange-500/10 mb-2">
<DarkInput
type="number"
label="ID do Moki (Obrigatório)"
value={formData.idmoki_frota}
onChange={e => setFormData({...formData, idmoki_frota: e.target.value})}
required
placeholder="Ex: 123456"
/>
<p className="text-[10px] text-orange-500/60 mt-1 ml-1 uppercase font-bold tracking-widest">Este campo deve ser preenchido manualmente para novos registros.</p>
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Origem" value={formData.origem} onChange={e => setFormData({...formData, origem: e.target.value})} />
<DarkInput label="Unidade" value={formData.unidade} onChange={e => setFormData({...formData, unidade: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Cod. Unidade" value={formData.cod_unidade} onChange={e => setFormData({...formData, cod_unidade: e.target.value})} />
<DarkInput label="Nome Unidade" value={formData.nome_unidade} onChange={e => setFormData({...formData, nome_unidade: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Status Unidade" value={formData.status_unidade} onChange={e => setFormData({...formData, status_unidade: e.target.value})} />
<DarkInput label="Checklist" value={formData.checklist} onChange={e => setFormData({...formData, checklist: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Status Checklist" value={formData.status_checklist} onChange={e => setFormData({...formData, status_checklist: e.target.value})} />
<DarkInput type="date" label="Data" value={formData.data?.split('T')[0]} onChange={e => setFormData({...formData, data: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Autor" value={formData.autor} onChange={e => setFormData({...formData, autor: e.target.value})} />
<DarkSelect label="Status" options={['Pendente', 'Conforme', 'Não Conforme', 'Aprovado']} value={formData.status} onChange={v => setFormData({...formData, status: v})} />
</div>
<DarkInput label="Aprovação (Obs)" value={formData.aprovacao} onChange={e => setFormData({...formData, aprovacao: e.target.value})} />
<DialogFooter className="sticky bottom-0 bg-white dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit">Salvar Checklist</DarkButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,339 +0,0 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useMonitoring } from '../hooks/useMonitoring';
import { useVehicles } from '../hooks/useVehicles';
import { useDrivers } from '../hooks/useDrivers';
import AutocompleteInput from '../components/AutocompleteInput';
import ExcelTable from '../components/ExcelTable';
import { Wrench, CheckCircle, Truck, Plus, Search } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
import { prafrotStatisticsService } from '../services/prafrotStatisticsService';
export default function MonitoringView() {
const { monitorings, loading, fetchMonitoring, createMonitoring, updateMonitoring, deleteMonitoring } = useMonitoring();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [statusStats, setStatusStats] = useState([]);
const [selectedStatusRecords, setSelectedStatusRecords] = useState(null);
const [isRecordsModalOpen, setIsRecordsModalOpen] = useState(false);
// Initial Form State
const initialFormState = {
data_carga: '', id_externo: '', idmonitoramento_frota: '', motorista: '',
placa: '', unidade: '', obs: '', rota: ''
};
const [formData, setFormData] = useState(initialFormState);
const { vehicles, fetchVehicles, deleteVehicle } = useVehicles();
const { drivers, fetchDrivers } = useDrivers();
useEffect(() => {
fetchMonitoring();
fetchVehicles();
fetchDrivers();
// Fetch Status Stats
prafrotStatisticsService.getPlacasPorStatus().then(data => {
if (Array.isArray(data)) setStatusStats(data);
});
}, []);
const getStatusData = (status) => {
return statusStats.find(item => {
const itemStatus = (item.status || item.status_frota || '');
return itemStatus.trim().toLowerCase() === status?.trim().toLowerCase();
});
};
const handleStatusClick = (status) => {
const data = getStatusData(status);
if (data && data.registros) {
setSelectedStatusRecords({
title: status,
records: JSON.parse(data.registros).filter(r => r !== null)
});
setIsRecordsModalOpen(true);
}
};
const handleVehicleSelect = (vehicle) => {
if (!vehicle) return;
setFormData(prev => ({
...prev,
placa: vehicle.placa || prev.placa,
unidade: vehicle.base || prev.unidade,
rota: vehicle.id_rota || prev.rota,
// Add other relevant fields for monitoring
}));
};
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
// Format date properly for HTML date input (YYYY-MM-DD)
const formattedItem = { ...item };
if (formattedItem.data_carga) {
// Handle various date formats from backend
const date = new Date(formattedItem.data_carga);
if (!isNaN(date.getTime())) {
// Format to YYYY-MM-DD for HTML date input
formattedItem.data_carga = date.toISOString().split('T')[0];
}
}
setFormData({ ...initialFormState, ...formattedItem });
} else {
setEditingItem(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
let success;
if (editingItem) {
success = await updateMonitoring(editingItem.idmonitoramento_frota, formData);
} else {
success = await createMonitoring(formData);
}
if (success) setIsModalOpen(false);
};
const filteredData = useMemo(() => Array.isArray(monitorings) ? monitorings.filter(item =>
item.unidade?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.motorista?.toLowerCase().includes(searchTerm.toLowerCase())
) : [], [monitorings, searchTerm]);
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Monitoramento & Logística</h1>
<p className="text-slate-500 text-sm">Acompanhamento em tempo real e rastreabilidade.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar unidade..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Monitoramento
</DarkButton>
</div>
</div>
{/* Status Highlights */}
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Em Operação', status: 'Em Operação', color: 'bg-orange-500/10 text-orange-600', icon: <CheckCircle size={20} />, hover: 'hover:border-orange-500/30' },
{ label: 'Veículo Alugado', status: 'Veículo Alugado', color: 'bg-blue-500/10 text-blue-600', icon: <Truck size={20} />, hover: 'hover:border-blue-500/30' },
].map((card, i) => (
<div
key={i}
onClick={() => handleStatusClick(card.status)}
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 flex-col sm:flex-row items-center justify-between group cursor-pointer ${card.hover}`}
>
<div className="flex items-center gap-4">
<div className={`p-4 rounded-xl ${card.color} group-hover:scale-110 transition-transform`}>
{card.icon}
</div>
<div className="flex flex-col">
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">{card.label}</span>
<div className="text-3xl font-bold text-slate-800 dark:text-white mt-0.5">
{getStatusData(card.status)?.total || 0}
</div>
</div>
</div>
</div>
))}
</div>
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
loading={loading}
columns={[
{ header: 'ID', field: 'idmonitoramento_frota', width: '80px' },
{ header: 'ID EXTERNO', field: 'id_externo', width: '120px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'UNIDADE', field: 'unidade', width: '150px' },
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
{ header: 'DATA CARGA', field: 'data_carga', width: '150px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'Buscar placa...' },
{ field: 'unidade', label: 'Unidade', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteMonitoring(item.idmonitoramento_frota)}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-4xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden shadow-2xl">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a]">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold">
{editingItem ? 'Editar Monitoramento' : 'Novo Registro de Monitoramento'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-stone-500">
Detalhes sobre o acompanhamento da viagem e carga.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-4 max-h-[75vh] overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-2 gap-4 pb-4">
<AutocompleteInput
label="Placa"
value={formData.placa}
onChange={v => setFormData({...formData, placa: v})}
options={vehicles}
displayKey="placa"
valueKey="placa"
searchKeys={['placa']}
onSelect={handleVehicleSelect}
required
/>
<DarkInput label="Unidade" value={formData.unidade} onChange={e => setFormData({...formData, unidade: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4 pb-4">
<DarkInput label="Rota" value={formData.rota} onChange={e => setFormData({...formData, rota: e.target.value})} />
<AutocompleteInput
label="Motorista"
value={formData.motorista}
onChange={v => setFormData({...formData, motorista: v})}
options={drivers}
displayKey="NOME_FAVORECIDO"
valueKey="NOME_FAVORECIDO"
placeholder="Buscar motorista..."
/>
<DarkInput type="date" label="Data Carga" value={formData.data_carga || ''} onChange={e => setFormData({...formData, data_carga: e.target.value})} />
</div>
<div className="grid grid-cols-1 gap-4 pb-4">
<DarkInput label="ID Externo" value={formData.id_externo} onChange={e => setFormData({...formData, id_externo: e.target.value})} />
</div>
<div className="gap-4 pb-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações de Monitoramento</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[100px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
</div>
<DialogFooter className="bg-slate-50 dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit">Salvar Registro</DarkButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Records Detail Modal */}
<Dialog open={isRecordsModalOpen} onOpenChange={setIsRecordsModalOpen}>
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex items-center gap-4">
<div className="p-4 bg-orange-500/10 rounded-2xl text-orange-600 shadow-inner">
<Truck size={28} />
</div>
<div>
<DialogTitle className="text-2xl font-bold text-slate-800 dark:text-white uppercase tracking-tight">
Veículos: {selectedStatusRecords?.title}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Monitoramento detalhado dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-orange-500 font-bold">"{selectedStatusRecords?.title}"</span>.
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
<div className="h-full w-full rounded-2xl border border-slate-200 dark:border-[#2a2a2a] overflow-hidden shadow-sm">
<ExcelTable
data={selectedStatusRecords?.records || []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
{ header: 'Status', field: 'status', width: '140px' },
{ header: 'Manutenção', field: 'manutencao', width: '120px' },
{ header: 'Atuação', field: 'atuacao', width: '140px' },
{ header: 'Proprietário', field: 'proprietario', width: '140px' },
{ header: 'VecFleet', field: 'vecfleet', width: '140px' },
{ header: 'OBS', field: 'obs', width: '250px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text' },
{ field: 'motorista', label: 'Motorista', type: 'text' },
{ field: 'base', label: 'Unidade', type: 'select' }
]}
onDelete={(item) => deleteVehicle(item.idveiculo_frota)}
/>
</div>
</div>
<DialogFooter className="p-4 border-t border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#1c1c1c]">
<DarkButton variant="secondary" onClick={() => setIsRecordsModalOpen(false)}>
Fechar Listagem
</DarkButton>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,561 +0,0 @@
import React, { useState } from 'react';
import { usePrafrotStatistics } from '../hooks/usePrafrotStatistics';
import { useVehicles } from '../hooks/useVehicles';
import { StatsGrid } from '@/components/shared/StatsGrid';
import {
Truck, Wrench, CheckCircle2, AlertTriangle,
BarChart3, PieChart as PieChartIcon, LineChart as LineChartIcon,
RefreshCw, Download, Calendar, Layers, MapPin, Gauge
} from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import {
PieChart, Pie, Cell, LineChart, Line, Legend, CartesianGrid, Bar,
ResponsiveContainer, Tooltip, XAxis, YAxis, BarChart
} from 'recharts';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
} from "@/components/ui/dialog";
import ExcelTable from '../components/ExcelTable';
// Cores premium para os gráficos
const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#14b8a6'];
const Skeleton = ({ className }) => (
<div className={`animate-pulse bg-slate-200 dark:bg-slate-800 rounded-xl ${className}`} />
);
// Componente de Tooltip Customizado e Premium
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload || !payload.length) return null;
return (
<div className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] p-4 rounded-2xl shadow-2xl backdrop-blur-md bg-opacity-95 dark:bg-opacity-95 ring-1 ring-black/5 z-[9999] pointer-events-none">
{label && (
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-2">
{label}
</p>
)}
<div className="space-y-1.5">
{payload.map((item, index) => {
const rawValue = item?.value;
const isNumber = typeof rawValue === 'number' && !Number.isNaN(rawValue);
const displayValue = isNumber ? rawValue.toLocaleString() : (rawValue ?? '-');
return (
<div key={index} className="flex items-center gap-3">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: item?.color || item?.fill || '#3b82f6' }}
/>
<div className="flex flex-col">
<span className="text-xs font-bold text-slate-700 dark:text-slate-200">
{item?.name ?? 'Valor'}
</span>
<span className="text-sm font-bold text-slate-900 dark:text-white">
{displayValue}
</span>
</div>
</div>
);
})}
</div>
</div>
);
};
export default function StatisticsView() {
const { data, loading: statsLoading, refresh } = usePrafrotStatistics();
const { deleteVehicle } = useVehicles();
const [selectedStatus, setSelectedStatus] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
if (statsLoading) {
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div className="flex justify-between items-center">
<div className="space-y-2">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-4 w-96" />
</div>
<div className="flex gap-2">
<Skeleton className="h-10 w-32" />
<Skeleton className="h-10 w-32" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => <Skeleton key={i} className="h-32 w-full" />)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Skeleton className="h-[400px] w-full" />
<Skeleton className="h-[400px] w-full" />
</div>
</div>
);
}
// Prepara dados para os KPIs - Baseado nos dados reais fornecidos
const totalVeiculos = data.totalPlacas;
// Em manutenção: Somar status "Aberto" de manutencao_frota ou status relacionados
const emManutencao = data.placasPorManutencao
.filter(m => m.manutencao === "Aberto" || m.manutencao === "ABERTO")
.reduce((acc, curr) => acc + curr.total, 0);
// Disponíveis: Filtrar pela data mais recente e status DISPONÍVEL ou ROTA
/* const disponiveis = data.placasPorDisponibilidade
.filter(d => d.status_disponibilidade === 'DISPONÍVEL' || d.status_disponibilidade === 'ROTA' || d.status_disponibilidade === 'DISP')
.reduce((acc, curr) => acc + curr.total, 0); */
const sinistros = data.placasPorSinistro.reduce((acc, curr) => acc + curr.total, 0);
const stats = [
{
label: 'Frota Total',
value: totalVeiculos.toLocaleString(),
icon: <Truck />,
color: 'bg-orange-500/10 text-orange-600'
},
{
label: 'Em Manutenção (Aberta)',
value: emManutencao.toLocaleString(),
icon: <Wrench />,
color: 'bg-orange-500/10 text-orange-600'
},
/* {
label: 'Em Operação/Disp.',
value: disponiveis.toLocaleString(),
icon: <CheckCircle2 />,
color: 'bg-blue-500/10 text-blue-600'
}, */
/* {
label: 'Sinistros/Vendas/Dev.',
value: sinistros.toLocaleString(),
icon: <AlertTriangle />,
color: 'bg-red-500/10 text-red-600'
} */
];
// Nomes dos meses para o gráfico quantitativo
const monthNames = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
const formattedQuantitativo = data.quantitativoManutencao.map(item => ({
...item,
name: monthNames[item.mes - 1]
}));
const chartStatusData = data.placasPorStatus.map(item => ({
...item,
status: (item.status || item.status_frota || 'NÃO INFORMADO').trim()
}));
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-12">
{/* Header com Design Premium */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-1">
<h1 className="text-4xl font-bold text-slate-900 dark:text-white tracking-tight">
Dashboard <span className="text-orange-500">Estatístico</span>
</h1>
<p className="text-slate-500 dark:text-slate-400 font-medium text-lg">
Monitoramento de BI e KPIs em tempo real da operação de frota.
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={refresh}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-600 dark:text-slate-300 font-bold text-sm hover:shadow-lg hover:-translate-y-0.5 transition-all"
>
<RefreshCw size={18} className={statsLoading ? "animate-spin" : ""} />
Sincronizar Dados
</button>
{/* <button className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-orange-600 text-white font-bold text-sm hover:bg-orange-700 hover:shadow-xl hover:shadow-orange-500/20 hover:-translate-y-0.5 transition-all">
<Download size={18} />
Exportar BI
</button> */}
</div>
</div>
{/* KPI Section */}
<div className="relative">
<div className="absolute -inset-1 bg-gradient-to-r from-orange-500/10 to-blue-500/10 blur-2xl opacity-50 -z-10" />
<StatsGrid stats={stats} />
</div>
{/* Status Distribution - Full Width */}
<Card className="border-none shadow-sm hover:shadow-md transition-all overflow-hidden bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between border-b dark:border-[#2a2a2a] mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-xl text-blue-600">
<PieChartIcon size={20} />
</div>
<CardTitle className="text-xl font-bold">Distribuição por Status</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col lg:flex-row items-center gap-8">
{/* Graph Left */}
<div className="h-[300px] w-full lg:w-1/2 min-h-[300px] relative">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartStatusData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={110}
paddingAngle={5}
dataKey="total"
nameKey="status"
stroke="none"
onClick={(_, index) => {
const entry = chartStatusData[index];
if (!entry) return;
setSelectedStatus(entry);
setIsModalOpen(true);
}}
>
{chartStatusData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
{/* Center Text Overlay */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none text-center px-4">
<span className="text-[11px] font-bold uppercase tracking-[0.2em] text-slate-400">
Total
</span>
<span className="text-3xl font-bold text-slate-800 dark:text-white leading-tight">
{totalVeiculos.toLocaleString()}
</span>
<span className="text-[10px] uppercase font-bold text-slate-400 tracking-[0.25em]">
Veículos
</span>
</div>
</div>
{/* Cards Right - Row Visualization */}
<div className="w-full lg:w-1/2 flex flex-col gap-3 h-[300px] overflow-y-auto custom-scrollbar pr-2">
{chartStatusData.map((entry, index) => (
<div
key={index}
onClick={() => {
setSelectedStatus(entry);
setIsModalOpen(true);
}}
className="flex items-center justify-between p-4 rounded-xl bg-slate-50 dark:bg-[#252525] border border-slate-100 dark:border-[#333] hover:border-blue-500/30 transition-all cursor-pointer group hover:scale-[1.01] hover:shadow-md"
>
<div className="flex items-center gap-4">
<div className="w-3 h-3 rounded-full shrink-0 shadow-sm" style={{ backgroundColor: COLORS[index % COLORS.length] }} />
<span className="text-sm font-bold text-slate-600 dark:text-slate-300 uppercase tracking-tight">{entry.status}</span>
</div>
<div className="flex items-center gap-4">
<div className="h-2 w-24 bg-slate-200 dark:bg-[#333] rounded-full overflow-hidden hidden sm:block">
<div className="h-full rounded-full" style={{ width: `${(entry.total / totalVeiculos) * 100}%`, backgroundColor: COLORS[index % COLORS.length] }} />
</div>
<span className="text-lg font-bold text-slate-900 dark:text-white min-w-[40px] text-right">{entry.total}</span>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
{/* Status Records Modal */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex items-center gap-4">
<div className="p-4 bg-orange-500/10 rounded-2xl text-orange-600 shadow-inner">
<Truck size={28} />
</div>
<div>
<DialogTitle className="text-2xl font-bold text-slate-800 dark:text-white uppercase tracking-tight">
Veículos: {selectedStatus?.status}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Listagem completa dos {selectedStatus?.total} veículos com status <span className="text-orange-500 font-bold">"{selectedStatus?.status}"</span>.
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
<div className="h-full w-full rounded-2xl border border-slate-200 dark:border-[#2a2a2a] overflow-hidden shadow-sm">
<ExcelTable
data={selectedStatus?.registros ? JSON.parse(selectedStatus.registros).filter(r => r !== null) : []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
{ header: 'Status', field: 'status', width: '140px' },
{ header: 'Manutenção', field: 'manutencao', width: '120px' },
{ header: 'Atuação', field: 'atuacao', width: '140px' },
{ header: 'Proprietário', field: 'proprietario', width: '140px' },
{ header: 'VecFleet', field: 'vecfleet', width: '140px' },
{ header: 'OBS', field: 'obs', width: '250px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text' },
{ field: 'motorista', label: 'Motorista', type: 'text' },
{ field: 'base', label: 'Unidade', type: 'select' }
]}
onDelete={(item) => deleteVehicle(item.idveiculo_frota)}
/>
</div>
</div>
<DialogFooter className="p-4 border-t border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#1c1c1c]">
<button
onClick={() => setIsModalOpen(false)}
className="px-6 py-2 rounded-xl bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 font-bold text-sm transition-all border border-slate-200 dark:border-[#333]"
>
Fechar Listagem
</button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Main Charts - Line Chart Full Width now */}
<div className="grid grid-cols-1 gap-6">
{/* Fluxo de Manutenção */}
<Card className="border-none shadow-sm hover:shadow-md transition-all overflow-hidden bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between border-b dark:border-[#2a2a2a] mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-500/10 rounded-xl text-orange-600">
<LineChartIcon size={20} />
</div>
<CardTitle className="text-xl font-bold">Fluxo de Manutenção Mensal</CardTitle>
</div>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={formattedQuantitativo} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorEntrada" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.1}/>
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorSaida" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.1}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis
dataKey="name"
stroke="#94a3b8"
fontSize={12}
tickLine={false}
axisLine={false}
dy={10}
/>
<YAxis
stroke="#94a3b8"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
<Legend iconType="circle" wrapperStyle={{ paddingTop: '20px' }} />
<Line
type="monotone"
dataKey="total_entrada"
name="Veículos Entrando"
stroke="#f59e0b"
strokeWidth={4}
dot={{ r: 4, fill: '#f59e0b', strokeWidth: 2, stroke: '#fff' }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
<Line
type="monotone"
dataKey="total_saida"
name="Veículos Liberados"
stroke="#10b981"
strokeWidth={4}
dot={{ r: 4, fill: '#10b981', strokeWidth: 2, stroke: '#fff' }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Row 3 - Regional (Full Width) */}
<div className="grid grid-cols-1 gap-6">
{/* Veículos por Base */}
<Card className="border-none shadow-sm hover:shadow-md transition-all bg-white dark:bg-[#1c1c1c]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Regional</span>
<MapPin size={14} className="text-orange-500" />
</div>
<CardTitle className="text-lg font-bold">Veículos por Base</CardTitle>
</CardHeader>
<CardContent className="h-[350px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.placasPorBase} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="base" stroke="#94a3b8" fontSize={11} axisLine={false} tickLine={false} />
<YAxis stroke="#94a3b8" fontSize={11} axisLine={false} tickLine={false} />
<Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip />} />
<Bar dataKey="total" fill="#10b981" radius={[6, 6, 0, 0]} barSize={40} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Row 4 - Chronology and Category */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Ano de Fabricação */}
<Card className="border-none shadow-sm hover:shadow-md transition-all bg-white dark:bg-[#1c1c1c]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Cronologia</span>
<Calendar size={14} className="text-blue-500" />
</div>
<CardTitle className="text-lg font-bold">Idade da Frota (Ano)</CardTitle>
</CardHeader>
<CardContent className="h-[280px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.placasPorAno}>
<XAxis dataKey="ano_fabricacao" stroke="#94a3b8" fontSize={9} axisLine={false} tickLine={false} />
<YAxis hide />
<Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip />} />
<Bar dataKey="total" name="Total de Veículos" fill="#3b82f6" radius={[6, 6, 0, 0]} barSize={32} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Categorias */}
<Card className="border-none shadow-sm hover:shadow-md transition-all bg-white dark:bg-[#1c1c1c]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Classificação</span>
<Layers size={14} className="text-purple-500" />
</div>
<CardTitle className="text-lg font-bold">Frota por Categoria</CardTitle>
</CardHeader>
<CardContent className="h-[280px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.placasPorCategoria} layout="vertical" margin={{ left: 0, right: 40, top: 10, bottom: 10 }}>
<defs>
<linearGradient id="barGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#a78bfa" />
</linearGradient>
</defs>
<XAxis type="number" hide />
<YAxis
dataKey="categoria"
type="category"
fontSize={10}
stroke="#94a3b8"
axisLine={false}
tickLine={false}
width={90}
tick={{ fontWeight: '600' }}
/>
<Tooltip cursor={{ fill: 'rgba(139, 92, 246, 0.05)' }} content={<CustomTooltip />} />
<Bar
dataKey="total"
name="Total de Veículos"
fill="url(#barGradient)"
radius={[0, 10, 10, 0]}
barSize={18}
label={{
position: 'right',
fill: '#94a3b8',
fontSize: 10,
fontWeight: 'bold',
formatter: (val) => val.toLocaleString()
}}
/>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Row 4 - Details and Units */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* <Card className="border-none shadow-sm bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between overflow-hidden">
<div>
<CardTitle className="text-xl font-bold">Disponibilidade Detalhada</CardTitle>
<p className="text-sm text-slate-500">Breakdown por status de operação.</p>
</div>
<div className="p-3 bg-orange-500/10 rounded-2xl text-orange-600">
<Gauge size={24} strokeWidth={2.5} />
</div>
</CardHeader>
<CardContent className="pt-2">
<div className="space-y-5">
{data.placasPorDisponibilidade.slice(0, 10).map((item, i) => (
<div key={i} className="group cursor-default">
<div className="flex items-center justify-between mb-2">
<div className="flex flex-col min-w-0">
<span className="text-sm font-bold text-slate-800 dark:text-slate-100 group-hover:text-orange-500 transition-colors uppercase tracking-tight truncate">{item.status_disponibilidade || 'NÃO INFORMADO'}</span>
<span className="text-[10px] font-bold text-slate-400 tracking-widest uppercase truncate">{item.disponibilidade}</span>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="text-2xl font-bold text-slate-900 dark:text-white">{item.total}</span>
<span className="text-xs font-bold px-2 py-0.5 rounded bg-slate-100 dark:bg-[#2a2a2a] text-slate-500">
{((item.total / (totalVeiculos || 1)) * 100).toFixed(1)}%
</span>
</div>
</div>
<div className="w-full h-2 bg-slate-100 dark:bg-[#2a2a2a] rounded-full overflow-hidden">
<div
className="h-full bg-orange-500 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${(item.total / (totalVeiculos || 1)) * 100}%` }}
/>
</div>
</div>
))}
</div>
</CardContent>
</Card> */}
<Card className="border-none shadow-sm bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="text-xl font-bold">Monitoramento por Unidade</CardTitle>
<p className="text-sm text-slate-500">Volume de monitoramento ativo por central.</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-2xl text-blue-600">
<BarChart3 size={24} strokeWidth={2.5} />
</div>
</CardHeader>
<CardContent className="pt-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-h-[500px] overflow-y-auto custom-scrollbar pr-2">
{data.placasPorUnidade.map((item, i) => (
<div key={i} className="p-4 rounded-2xl bg-slate-50 dark:bg-[#252525] border border-slate-100 dark:border-[#2a2a2a] flex justify-between items-center hover:border-blue-500/50 hover:shadow-md transition-all">
<div className="flex flex-col min-w-0">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Unidade</span>
<span className="text-sm font-bold text-slate-700 dark:text-slate-200 truncate">{item.unidade}</span>
</div>
<span className="text-2xl font-bold text-blue-500 shrink-0">{item.total}</span>
</div>
))}
{data.placasPorUnidade.length === 0 && (
<div className="col-span-2 py-12 flex flex-col items-center justify-center text-slate-400">
<Layers size={48} strokeWidth={1} className="mb-2 opacity-20" />
<p className="text-sm font-medium">Nenhum dado de unidade encontrado</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

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

View File

@ -1,111 +0,0 @@
import React, { useState, useMemo } from 'react';
import { Search } from 'lucide-react';
import ExcelTable from '../components/ExcelTable';
const TableDebug = () => {
const [searchTerm, setSearchTerm] = useState('');
// Mock Data matching the screenshot structure
const vehicles = useMemo(() => {
// Top specific rows to match screenshot
const topRows = [
{ id: 1, modelo: 'TEO0G02', placa: 'TEO0G02', status: 'Em uso', motorista: 'Larissa Ribeiro de Souza', combustivel: 'Gasolina', tipo: 'Passeio', marca: 'Fiat', ano: '2020', cidade: 'São Paulo', empresa: 'Empresa A', financiamento: 'Sim', financeira: 'Banco X' },
{ id: 2, modelo: 'HFB3A70', placa: 'HFB3A70', status: 'Não informado', motorista: 'Não atribuído', combustivel: 'Etanol', tipo: 'Utilitário', marca: 'VW', ano: '2021', cidade: 'Rio', empresa: 'Empresa B', financiamento: 'Não', financeira: '-' },
{ id: 3, modelo: 'LUN1968', placa: 'LUN1968', status: 'Manutenção', motorista: 'Carlos Silva', combustivel: 'Diesel', tipo: 'Caminhão', marca: 'Volvo', ano: '2019', cidade: 'Curitiba', empresa: 'Empresa A', financiamento: 'Sim', financeira: 'Banco Y' },
// ... mix of data
];
// Fill rest with randomized variety
const fillRows = Array.from({ length: 40 }, (_, i) => ({
id: 10 + i,
modelo: `FLEX${i}`,
placa: `ABC${10 + i}99`,
status: ['Em uso', 'Manutenção', 'Vendido'][i % 3], // Varied status
motorista: i % 2 === 0 ? 'Motorista Teste' : 'Não atribuído',
combustivel: ['Flex', 'Gasolina', 'Diesel'][i % 3],
tipo: ['Passeio', 'Utilitário', 'Caminhão'][i % 3],
marca: ['Fiat', 'VW', 'Ford', 'Chevrolet'][i % 4],
ano: String(2015 + (i % 10)),
cidade: ['São Paulo', 'Rio', 'Belo Horizonte'][i % 3],
empresa: ['Empresa A', 'Empresa B', 'Empresa C'][i % 3],
financiamento: i % 2 === 0 ? 'Sim' : 'Não',
financeira: i % 2 === 0 ? `Banco ${['X', 'Y', 'Z'][i % 3]}` : '-'
}));
return [...topRows, ...fillRows];
}, []);
const columns = [
{ header: 'MODELO', field: 'modelo', width: '120px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'text-[#eab308] font-bold' }, // Yellow text for Placa
{
header: 'STATUS',
field: 'status',
width: '140px',
render: (row) => (
row.status === 'Em uso'
? <span className="inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider bg-[#1d4ed8] text-white">Em uso</span>
: <span className="inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider bg-[#333] text-[#aaa]">Não informado</span>
)
},
{ header: 'MOTORISTA ATUAL', field: 'motorista', width: '200px' },
{ header: 'COMBUSTÍVEL', field: 'combustivel', width: '120px' },
{ header: 'TIPO', field: 'tipo', width: '100px' },
{ header: 'MARCA', field: 'marca', width: '120px' },
{ header: 'ANO', field: 'ano', width: '80px' },
{ header: 'CIDADE', field: 'cidade', width: '120px' },
];
return (
<div className="min-h-screen bg-[#111] text-white p-8 font-sans">
<div className="mb-8">
<h1 className="text-3xl font-bold text-[#eab308] mb-2 flex items-center gap-3">
<span className="p-2 bg-[#eab308]/10 rounded-lg">🔧</span>
Debug - Tabela Prafrot
</h1>
<p className="text-slate-400">Teste standalone da tabela Excel-style (Pixel Perfect)</p>
</div>
<div className="flex items-center gap-4 mb-6">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={18} />
<input
type="text"
placeholder="Buscar placa..."
className="w-full bg-[#1a1a1a] border border-[#333] rounded px-10 py-2 text-sm text-slate-200 focus:outline-none focus:border-[#eab308] placeholder:text-slate-600 font-medium"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<button className="px-4 py-2 bg-[#eab308] text-black font-bold rounded hover:bg-[#ca9a00] transition-colors flex items-center gap-2 text-sm">
+ Novo
</button>
</div>
{/* New ExcelTable Component */}
<div className="h-[600px]">
<ExcelTable
data={vehicles}
columns={columns}
filterDefs={[
{ field: 'empresa', label: 'Empresa', type: 'select' },
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'por placa' },
{ field: 'status', label: 'Status', type: 'select' },
{ field: 'combustivel', label: 'Combustível', type: 'select' },
{ field: 'tipo', label: 'Tipo', type: 'select' },
{ field: 'modelo', label: 'Modelo', type: 'text', placeholder: 'por modelo' },
{ field: 'financiamento', label: 'Financiamento', type: 'select' },
{ field: 'financeira', label: 'Financiador', type: 'text', placeholder: 'financiador' },
{ field: 'motorista', label: 'Proprietário', type: 'text', placeholder: 'proprietário' }
]}
/>
</div>
</div>
);
};
export default TableDebug;

View File

@ -1,548 +0,0 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useVehicles } from '../hooks/useVehicles';
import { useDrivers } from '../hooks/useDrivers';
import { useFleetLists } from '../hooks/useFleetLists';
import AutocompleteInput from '../components/AutocompleteInput';
import ExcelTable from '../components/ExcelTable';
import {
Plus, Filter, Search, Edit2, Trash2,
MapPin, Truck, Calendar, DollarSign, Wrench, CheckCircle
} from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from 'sonner';
import { useFeedback } from '../components/FeedbackNotification';
// --- Components Styled for Dark Theme ---
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<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-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
// --- Inline Status Editor ---
const StatusCell = ({ currentStatus, idVehicle, options, onUpdate }) => {
const [isEditing, setIsEditing] = useState(false);
const [tempValue, setTempValue] = useState(currentStatus);
useEffect(() => {
setTempValue(currentStatus);
}, [currentStatus]);
const handleBlur = () => {
setIsEditing(false);
if (tempValue !== currentStatus) {
onUpdate(idVehicle, tempValue);
}
};
const handleChange = (e) => {
setTempValue(e.target.value);
// Optional: Auto-save immediately on change instead of blur?
// User asked: "Clicar fora e enviaria", so blur is better.
};
if (isEditing) {
return (
<select
autoFocus
value={tempValue}
onChange={handleChange}
onBlur={handleBlur}
className="w-full bg-slate-50 dark:bg-[#141414] border border-orange-500 rounded px-1 py-0.5 text-[10px] uppercase font-bold text-slate-700 dark:text-slate-200 focus:outline-none"
onClick={(e) => e.stopPropagation()} // Prevent row selection if applied
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
}
return (
<div
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border cursor-pointer hover:opacity-80 transition-opacity ${
currentStatus === 'ATIVO' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-slate-500/10 text-slate-500 border-slate-500/20'
}`}
>
{currentStatus || 'N/A'}
</div>
);
};
import { prafrotStatisticsService } from '../services/prafrotStatisticsService';
export default function VehiclesView() {
const { vehicles, loading, fetchVehicles, createVehicle, updateVehicle, updateVehicleStatus, deleteVehicle } = useVehicles();
const { fetchListsConfig, statusFrotaOptions } = useFleetLists();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingVehicle, setEditingVehicle] = useState(null);
const [statusStats, setStatusStats] = useState([]);
const [selectedStatusRecords, setSelectedStatusRecords] = useState(null);
const [isRecordsModalOpen, setIsRecordsModalOpen] = useState(false);
// Initial Form State
const initialFormState = {
ano_fabricacao: '', ano_modelo: '', atuacao: '', base: '', categoria: 'Passeio',
chassi: '', cnpj: '', combustivel: 'Gasolina', contrato: '', coordenador: '',
cor: '', data_limite: '', dispatcher: '', fabricante: '', fiscal_operacao: '',
geotab: 'NÃO', gestor: '', golfleet: 'NÃO', idveiculo_frota: '', modelo: '',
placa: '', pooltrack: 'NÃO', primeira_locacao: '', proprietario: '',
renavam: '', sascar: 'NÃO', t4s: 'NÃO', tipo_de_placa: '', tipo_frota: '',
uf: '', valor_aluguel: '', valor_fipe: '',
situacao: 'ATIVO', km_atual: '', observacoes: '', empresa: '', financiamento: '',
locadora: '', motorista_atual: ''
};
const [formData, setFormData] = useState(initialFormState);
const { drivers, fetchDrivers } = useDrivers();
useEffect(() => {
fetchVehicles();
fetchDrivers();
fetchListsConfig();
// Fetch Status Stats
prafrotStatisticsService.getPlacasPorStatus().then(data => {
if (Array.isArray(data)) setStatusStats(data);
});
}, []);
const getStatusData = (status) => {
return statusStats.find(item => {
const itemStatus = (item.status || item.status_frota || '');
return itemStatus.trim().toLowerCase() === status?.trim().toLowerCase();
});
};
const handleStatusClick = (status) => {
const data = getStatusData(status);
if (data && data.registros) {
setSelectedStatusRecords({
title: status,
records: JSON.parse(data.registros).filter(r => r !== null)
});
setIsRecordsModalOpen(true);
}
};
const handleOpenModal = (vehicle = null) => {
if (vehicle) {
setEditingVehicle(vehicle);
setFormData({
...initialFormState,
...vehicle, // Fill with vehicle data
tipo_placa: vehicle.tipo_de_placa || vehicle.tipo_placa || '', // Handle varied naming
data_aquisicao: vehicle.data_aquisicao?.split('T')[0] || '',
data_venda: vehicle.data_venda?.split('T')[0] || '',
data_limite: vehicle.data_limite?.split('T')[0] || '',
primeira_locacao: vehicle.primeira_locacao?.split('T')[0] || '',
situacao: vehicle.situacao || vehicle.status_frota || 'ATIVO',
});
} else {
setEditingVehicle(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const { success: notifySuccess, error: notifyError, notifyFields } = useFeedback();
const handleSubmit = async (e) => {
e.preventDefault();
// Validação de campos obrigatórios
const requiredFields = [];
if (!formData.placa) requiredFields.push('Placa');
if (!formData.modelo) requiredFields.push('Modelo');
if (!formData.fabricante) requiredFields.push('Fabricante');
if (requiredFields.length > 0) {
notifyFields(requiredFields);
return;
}
const payload = { ...formData, tipo_de_placa: formData.tipo_placa };
delete payload.motorista_atual;
delete payload.empresa;
delete payload.locadora;
delete payload.km_atual;
delete payload.data_venda;
delete payload.valor_venda;
delete payload.situacao;
let success;
if (editingVehicle) {
success = await updateVehicle(editingVehicle.idveiculo_frota, payload);
} else {
success = await createVehicle(payload);
}
if (success) setIsModalOpen(false);
};
const filteredVehicles = useMemo(() => Array.isArray(vehicles) ? vehicles.filter(v =>
v.placa?.toLowerCase().includes(searchTerm.toLowerCase()) ||
v.modelo?.toLowerCase().includes(searchTerm.toLowerCase())
) : [], [vehicles, searchTerm]);
return (
<div className="space-y-6">
{/* Header Actions */}
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Frota & Ativos</h1>
<p className="text-slate-500 text-sm">Gerencie os veículos cadastrados na plataforma.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Novo Veículo
</DarkButton>
</div>
</div>
{/* Status Highlights */}
<div className="grid grid-cols-2 gap-4">
<div
onClick={() => handleStatusClick('Veículo Alugado')}
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-blue-500/30"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-500/10 rounded-xl text-blue-600 group-hover:bg-blue-500 group-hover:text-white transition-colors">
<Truck size={24} />
</div>
<div className="flex flex-col">
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Veículos Alugados</span>
<div className="text-3xl font-bold text-slate-800 dark:text-white mt-0.5">
{getStatusData('Veículo Alugado')?.total || 0}
</div>
</div>
</div>
</div>
<div
onClick={() => handleStatusClick('Em Operação')}
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] rounded-xl p-6 shadow-sm hover:shadow-md transition-all flex items-center justify-between group cursor-pointer hover:border-orange-500/30"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-orange-500/10 rounded-xl text-orange-600 group-hover:bg-orange-500 group-hover:text-white transition-colors">
<CheckCircle size={24} />
</div>
<div className="flex flex-col">
<span className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Em Operação</span>
<div className="text-3xl font-bold text-slate-800 dark:text-white mt-0.5">
{getStatusData('Em Operação')?.total || 0}
</div>
</div>
</div>
</div>
</div>
{/* Modular Excel Table */}
<div className="h-[600px] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredVehicles}
loading={loading}
columns={[
{ header: 'ID', field: 'idveiculo_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'MODELO', field: 'modelo', width: '140px' },
{ header: ' FABRICANTE', field: 'fabricante', width: '120px' },
{ header: 'CORES', field: 'cor', width: '100px' },
{ header: 'ANO FAB.', field: 'ano_fabricacao', width: '80px' },
{ header: 'ANO MOD.', field: 'ano_modelo', width: '80px' },
{ header: 'RENAVAM', field: 'renavam', width: '130px' },
{ header: 'CHASSI', field: 'chassi', width: '180px' },
{ header: 'CATEGORIA', field: 'categoria', width: '120px' },
{ header: 'COMBUSTÍVEL', field: 'combustivel', width: '100px' },
{ header: 'TIPO PLACA', field: 'tipo_de_placa', width: '100px' },
{ header: 'TIPO FROTA', field: 'tipo_frota', width: '120px' },
{ header: 'ATUAÇÃO', field: 'atuacao', width: '150px' },
{ header: 'BASE', field: 'base', width: '120px' },
{ header: 'UF', field: 'uf', width: '60px' },
{ header: 'PROPRIETÁRIO', field: 'proprietario', width: '140px' },
{ header: 'CNPJ', field: 'cnpj', width: '150px' },
{ header: 'CONTRATO', field: 'contrato', width: '120px' },
{ header: 'GESTOR', field: 'gestor', width: '140px' },
{ header: 'COORDENADOR', field: 'coordenador', width: '140px' },
{ header: 'DISPATCHER', field: 'dispatcher', width: '140px' },
{ header: 'FISCAL OPER.', field: 'fiscal_operacao', width: '140px' },
{ header: 'VALOR FIPE', field: 'valor_fipe', width: '120px' },
{ header: 'VALOR ALUGUEL', field: 'valor_aluguel', width: '120px' },
{ header: '1ª LOCAÇÃO', field: 'primeira_locacao', width: '110px' },
{ header: 'DATA LIMITE', field: 'data_limite', width: '110px' },
{ header: 'GEOTAB', field: 'geotab', width: '80px' },
{ header: 'SASCAR', field: 'sascar', width: '80px' },
{ header: 'GOLFLEET', field: 'golfleet', width: '80px' },
{ header: 'POOLTRACK', field: 'pooltrack', width: '80px' },
{ header: 'T4S', field: 't4s', width: '80px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text', placeholder: 'Buscar placa...' },
{ field: 'modelo', label: 'Modelo', type: 'select' },
{ field: 'fabricante', label: 'Fabricante', type: 'select' },
{ field: 'base', label: 'Base', type: 'select' },
{ field: 'proprietario', label: 'Proprietário', type: 'select' },
{ field: 'tipo_frota', label: 'Tipo de Frota', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteVehicle(item.idveiculo_frota)}
/>
</div>
{/* Modal - Dark Theme */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-4xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden shadow-2xl">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] bg-slate-50 dark:bg-[#1c1c1c]">
<DialogTitle className="text-xl font-bold text-slate-800 dark:text-white uppercase tracking-tight">
{editingVehicle ? `Editando: ${formData.placa}` : 'Cadastro de Novo Veículo'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-stone-500">
Preencha os dados completos do ativo na frota.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-2 max-h-[75vh] overflow-y-auto custom-scrollbar">
{/* Seletor de Status Destacado */}
{/* Seletor de Status removido conforme solicitacao */}
<Tabs defaultValue="basicos" className="w-full">
<TabsList className="bg-slate-100 dark:bg-[#141414] border border-slate-200 dark:border-[#2a2a2a] w-full justify-start p-1 h-auto mb-4">
{['basicos', 'operacional', 'financeiro', 'tecnico'].map(tab => (
<TabsTrigger
key={tab}
value={tab}
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
>
{tab === 'tecnico' ? 'RASTREADOR' : tab}
</TabsTrigger>
))}
</TabsList>
<div className="pb-4 min-h-[350px]">
<TabsContent value="basicos" className="space-y-4 m-0">
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Placa" value={formData.placa} onChange={e => setFormData({...formData, placa: e.target.value})} required />
<DarkInput label="Chassi" value={formData.chassi} onChange={e => setFormData({...formData, chassi: e.target.value})} />
<DarkInput label="Renavam" value={formData.renavam} onChange={e => setFormData({...formData, renavam: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Modelo" value={formData.modelo} onChange={e => setFormData({...formData, modelo: e.target.value})} />
<DarkInput label="Fabricante" value={formData.fabricante} onChange={e => setFormData({...formData, fabricante: e.target.value})} />
<DarkInput label="Cor" value={formData.cor} onChange={e => setFormData({...formData, cor: e.target.value})} />
</div>
<div className="grid grid-cols-4 gap-4">
<DarkInput label="Ano Fab." value={formData.ano_fabricacao} onChange={e => setFormData({...formData, ano_fabricacao: e.target.value})} />
<DarkInput label="Ano Mod." value={formData.ano_modelo} onChange={e => setFormData({...formData, ano_modelo: e.target.value})} />
<DarkSelect label="Categoria" options={['Utilitário', 'Passeio', 'Pesado']} value={formData.categoria} onChange={v => setFormData({...formData, categoria: v})} />
<DarkSelect label="Combustível" options={['Flex', 'Diesel', 'Gasolina', 'Elétrico']} value={formData.combustivel} onChange={v => setFormData({...formData, combustivel: v})} />
</div>
<DarkInput label="Tipo Placa (Contexto)" value={formData.tipo_placa} onChange={e => setFormData({...formData, tipo_placa: e.target.value})} />
</TabsContent>
<TabsContent value="operacional" className="space-y-4 m-0">
<div className="grid grid-cols-3 gap-4">
<DarkInput label="Base" value={formData.base} onChange={e => setFormData({...formData, base: e.target.value})} />
<DarkInput label="UF" value={formData.uf} onChange={e => setFormData({...formData, uf: e.target.value})} maxLength={2} />
{/* Proprietário comentado nesta aba conforme nova solicitação para aba financeiro */}
{/* <DarkSelect label="Proprietário" options={['Localiza', 'Movida', 'Próprio', 'Unidas']} value={formData.proprietario} onChange={v => setFormData({...formData, proprietario: v})} /> */}
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Atuação / Operação" value={formData.atuacao} onChange={e => setFormData({...formData, atuacao: e.target.value})} />
<DarkInput label="Tipo Frota" value={formData.tipo_frota} onChange={e => setFormData({...formData, tipo_frota: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Gestor" value={formData.gestor} onChange={e => setFormData({...formData, gestor: e.target.value})} />
<DarkInput label="Coordenador" value={formData.coordenador} onChange={e => setFormData({...formData, coordenador: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Dispatcher" value={formData.dispatcher} onChange={e => setFormData({...formData, dispatcher: e.target.value})} />
<DarkInput label="Fiscal de Operação" value={formData.fiscal_operacao} onChange={e => setFormData({...formData, fiscal_operacao: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
{/* Campos motorista e status comentados para escrita e envio ao back conforme solicitado */}
{/* <AutocompleteInput
label="Motorista Atual"
value={formData.motorista_atual}
onChange={v => setFormData({...formData, motorista_atual: v})}
options={drivers}
displayKey="NOME_FAVORECIDO"
valueKey="NOME_FAVORECIDO"
placeholder="Buscar motorista..."
/>
<DarkSelect label="Status Veículo" options={['ATIVO', 'INATIVO', 'MANUTENÇÃO', 'VENDIDO', 'SINISTRADO']} value={formData.status_veiculo} onChange={v => setFormData({...formData, status_veiculo: v})} /> */}
</div>
</TabsContent>
<TabsContent value="financeiro" className="space-y-4 m-0">
<div className="grid grid-cols-2 gap-4">
<DarkInput label="Valor FIPE" value={formData.valor_fipe} onChange={e => setFormData({...formData, valor_fipe: e.target.value})} />
<DarkInput label="Valor Aluguel" value={formData.valor_aluguel} onChange={e => setFormData({...formData, valor_aluguel: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput label="CNPJ Proprietário" value={formData.cnpj} onChange={e => setFormData({...formData, cnpj: e.target.value})} />
<DarkInput label="Contrato" value={formData.contrato} onChange={e => setFormData({...formData, contrato: e.target.value})} />
</div>
<div className="grid grid-cols-3 gap-4">
{/* Empresa e Locadora comentados para escrita e envio ao back */}
{/* <DarkInput label="Empresa" value={formData.empresa} onChange={e => setFormData({...formData, empresa: e.target.value})} /> */}
<DarkInput label="Proprietário" value={formData.proprietario} onChange={e => setFormData({...formData, proprietario: e.target.value})} />
<DarkInput label="Financiamento" value={formData.financiamento} onChange={e => setFormData({...formData, financiamento: e.target.value})} />
{/* <DarkInput label="Locadora" value={formData.locadora} onChange={e => setFormData({...formData, locadora: e.target.value})} /> */}
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput type="date" label="Data Aquisição" value={formData.data_aquisicao} onChange={e => setFormData({...formData, data_aquisicao: e.target.value})} />
<DarkInput type="number" label="Valor Aquisição" value={formData.valor_aquisicao} onChange={e => setFormData({...formData, valor_aquisicao: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
{/* Campos comentados para escrita e envio ao back conforme solicitado */}
{/* <DarkInput type="date" label="Data Venda" value={formData.data_venda} onChange={e => setFormData({...formData, data_venda: e.target.value})} />
<DarkInput type="number" label="Valor Venda" value={formData.valor_venda} onChange={e => setFormData({...formData, valor_venda: e.target.value})} /> */}
</div>
<div className="grid grid-cols-2 gap-4">
<DarkInput type="date" label="Primeira Locação" value={formData.primeira_locacao} onChange={e => setFormData({...formData, primeira_locacao: e.target.value})} />
<DarkInput type="date" label="Data Limite" value={formData.data_limite} onChange={e => setFormData({...formData, data_limite: e.target.value})} />
</div>
</TabsContent>
<TabsContent value="tecnico" className="space-y-4 m-0">
<div className="grid grid-cols-1 gap-4">
{/* KM Atual comentado para escrita e envio ao back */}
{/* <DarkInput label="KM Atual" value={formData.km_atual} onChange={e => setFormData({...formData, km_atual: e.target.value})} /> */}
</div>
<div className="grid grid-cols-4 gap-4 pt-2">
{['geotab', 'sascar', 'golfleet', 'pooltrack', 't4s'].map(tracker => (
<DarkSelect key={tracker} label={tracker.toUpperCase()} options={['SIM', 'NÃO']} value={formData[tracker]} onChange={v => setFormData({...formData, [tracker]: v})} />
))}
</div>
<div className="gap-4 pt-2">
<label className="text-[10px] uppercase font-bold text-slate-500 tracking-wider ml-1">Observações Gerais</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 min-h-[80px]"
value={formData.observacoes}
onChange={e => setFormData({...formData, observacoes: e.target.value})}
/>
</div>
</TabsContent>
</div>
</Tabs>
</form>
<DialogFooter className="bg-slate-50 dark:bg-[#141414] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit" onClick={handleSubmit}>Salvar Veículo</DarkButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Records Detail Modal */}
<Dialog open={isRecordsModalOpen} onOpenChange={setIsRecordsModalOpen}>
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex items-center gap-4">
<div className="p-4 bg-blue-500/10 rounded-2xl text-blue-600 shadow-inner">
<Truck size={28} />
</div>
<div>
<DialogTitle className="text-2xl font-bold text-slate-800 dark:text-white uppercase tracking-tight">
Veículos: {selectedStatusRecords?.title}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Listagem completa dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-blue-500 font-bold">"{selectedStatusRecords?.title}"</span>.
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
<div className="h-full w-full rounded-2xl border border-slate-200 dark:border-[#2a2a2a] overflow-hidden shadow-sm">
<ExcelTable
data={selectedStatusRecords?.records || []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
{ header: 'Status', field: 'status', width: '140px' },
{ header: 'Manutenção', field: 'manutencao', width: '120px' },
{ header: 'Atuação', field: 'atuacao', width: '140px' },
{ header: 'Proprietário', field: 'proprietario', width: '140px' },
{ header: 'VecFleet', field: 'vecfleet', width: '140px' },
{ header: 'OBS', field: 'obs', width: '250px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text' },
{ field: 'motorista', label: 'Motorista', type: 'text' },
{ field: 'base', label: 'Unidade', type: 'select' }
]}
/>
</div>
</div>
<DialogFooter className="p-4 border-t border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#1c1c1c]">
<DarkButton variant="secondary" onClick={() => setIsRecordsModalOpen(false)}>
Fechar Listagem
</DarkButton>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,221 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useWorkshops } from '../hooks/useWorkshops';
import ExcelTable from '../components/ExcelTable';
import { Plus, Search } from 'lucide-react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription
} from "@/components/ui/dialog";
// Reusing styled components locally
const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
);
const DarkSelect = ({ label, options, value, onChange }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
return (
<button className={`${baseClass} ${variants[variant]} ${className}`} {...props}>
{children}
</button>
);
};
export default function WorkshopsView() {
const { workshops, loading, fetchWorkshops, createWorkshop, updateWorkshop, deleteWorkshop } = useWorkshops();
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const initialFormState = {
idoficinas_frota: '',
cod_estabelecimento: '',
cnpj: '',
razao_social: '',
nome_reduzido: '',
tipo_estabelecimento: '',
endereco: '',
bairro: '',
cidade: '',
uf: '',
fone_comercial: '',
descricao_grupo: ''
};
const [formData, setFormData] = useState(initialFormState);
useEffect(() => {
fetchWorkshops();
}, []);
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...initialFormState, ...item });
} else {
setEditingItem(null);
setFormData(initialFormState);
}
setIsModalOpen(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
let success;
if (editingItem) {
success = await updateWorkshop(editingItem.idoficinas_frota, formData);
} else {
success = await createWorkshop(formData);
}
if (success) setIsModalOpen(false);
};
const filteredData = Array.isArray(workshops) ? workshops.filter(item =>
item.nome_reduzido?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.razao_social?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.cidade?.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.cnpj?.toLowerCase().includes(searchTerm.toLowerCase())
) : [];
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight uppercase">Gestão de Oficinas</h1>
<p className="text-slate-500 text-sm">Rede credenciada e prestadores de serviços.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64 transition-all"
placeholder="Buscar por nome, CNPJ ou cidade..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<DarkButton onClick={() => handleOpenModal()}>
<Plus size={18} /> Nova Oficina
</DarkButton>
</div>
</div>
<div className="h-[calc(100vh-220px)] w-full max-w-full overflow-hidden min-w-0">
<ExcelTable
data={filteredData}
loading={loading}
rowKey="idoficinas_frota"
columns={[
{ header: 'ID', field: 'idoficinas_frota', width: '80px' },
{ header: 'Cod. Estab.', field: 'cod_estabelecimento', width: '100px' },
{ header: 'Nome Reduzido', field: 'nome_reduzido', width: '220px', className: 'font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Razão Social', field: 'razao_social', width: '250px' },
{ header: 'CNPJ', field: 'cnpj', width: '150px' },
{ header: 'Tipo', field: 'tipo_estabelecimento', width: '180px' },
{ header: 'Cidade', field: 'cidade', width: '150px' },
{ header: 'UF', field: 'uf', width: '60px' },
{ header: 'Bairro', field: 'bairro', width: '150px' },
{ header: 'Endereço', field: 'endereco', width: '300px' },
{ header: 'Telefone', field: 'fone_comercial', width: '150px' },
{ header: 'Grupo', field: 'descricao_grupo', width: '200px' },
]}
filterDefs={[
{ field: 'cidade', label: 'Cidade', type: 'select' },
{ field: 'tipo_estabelecimento', label: 'Tipo', type: 'select' },
{ field: 'uf', label: 'UF', type: 'select' },
{ field: 'descricao_grupo', label: 'Grupo', type: 'select' },
]}
onEdit={handleOpenModal}
onDelete={(item) => deleteWorkshop(item.idoficinas_frota)}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-3xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden shadow-2xl">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#222]/30">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold flex items-center gap-2">
<div className="w-2 h-6 bg-orange-500 rounded-xl"></div>
{editingItem ? 'Editar Oficina' : 'Cadastrar Nova Oficina'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
Preencha os dados cadastrais da oficina conforme os registros do sistema.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="px-6 py-6 space-y-6 max-h-[70vh] overflow-y-auto custom-scrollbar">
{/* Seção: Identificação */}
<div className="space-y-4">
<h3 className="text-[11px] font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/20 pb-1">Identificação</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<DarkInput label="Cod. Estabelecimento" value={formData.cod_estabelecimento} onChange={e => setFormData({...formData, cod_estabelecimento: e.target.value})} />
</div>
<div className="md:col-span-2">
<DarkInput label="CNPJ" value={formData.cnpj} onChange={e => setFormData({...formData, cnpj: e.target.value})} />
</div>
</div>
<DarkInput label="Razão Social" value={formData.razao_social} onChange={e => setFormData({...formData, razao_social: e.target.value})} required />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DarkInput label="Nome Reduzido" value={formData.nome_reduzido} onChange={e => setFormData({...formData, nome_reduzido: e.target.value})} required />
<DarkInput label="Tipo Estabelecimento" value={formData.tipo_estabelecimento} onChange={e => setFormData({...formData, tipo_estabelecimento: e.target.value})} />
</div>
</div>
{/* Seção: Localização e Contato */}
<div className="space-y-4">
<h3 className="text-[11px] font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/20 pb-1">Localização e Contato</h3>
<DarkInput label="Endereço" value={formData.endereco} onChange={e => setFormData({...formData, endereco: e.target.value})} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<DarkInput label="Bairro" value={formData.bairro} onChange={e => setFormData({...formData, bairro: e.target.value})} />
<DarkInput label="Cidade" value={formData.cidade} onChange={e => setFormData({...formData, cidade: e.target.value})} />
<DarkInput label="UF" value={formData.uf} onChange={e => setFormData({...formData, uf: e.target.value})} maxLength={2} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DarkInput label="Telefone Comercial" value={formData.fone_comercial} onChange={e => setFormData({...formData, fone_comercial: e.target.value})} />
<DarkInput label="Descrição do Grupo" value={formData.descricao_grupo} onChange={e => setFormData({...formData, descricao_grupo: e.target.value})} />
</div>
</div>
<DialogFooter className="bg-slate-50 dark:bg-[#1c1c1c] border-t border-slate-200 dark:border-[#2a2a2a] p-4 gap-2 mt-6">
<DarkButton type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancelar</DarkButton>
<DarkButton type="submit" className="min-w-[150px]">Salvar Cadastro</DarkButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -25,7 +25,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -38,7 +38,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-lg font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
@ -113,14 +113,14 @@ export default function AvailabilityView() {
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Disponibilidade e Agenda</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Disponibilidade e Agenda</h1>
<p className="text-slate-500 text-sm">Visualização de disponibilidade da frota.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -138,11 +138,11 @@ export default function AvailabilityView() {
columns={[
{ header: 'ID', field: 'iddisponibilidade_frota', width: '80px' },
{ header: 'VEÍCULO ID', field: 'idveiculo_frota', width: '100px' },
{ header: 'PLACA', field: 'placa', width: '120px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '120px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'DATA', field: 'disponibilidade', width: '180px', render: (row) => row.disponibilidade?.split('T')[0] },
{ header: 'STATUS', field: 'status_disponibilidade', width: '150px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border ${
row.status_disponibilidade === 'Disponível' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
row.status_disponibilidade === 'Disponível' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
'bg-red-500/10 text-red-500 border-red-500/20'
}`}>
{row.status_disponibilidade}
@ -168,16 +168,16 @@ export default function AvailabilityView() {
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-4 max-h-[75vh] overflow-y-auto custom-scrollbar">
{formData.iddisponibilidade_frota && (
<div className="bg-emerald-500/5 p-3 rounded-xl border border-emerald-500/10 mb-2">
<p className="text-[10px] uppercase font-bold text-emerald-500/60 tracking-widest">ID do Registro</p>
<p className="text-lg font-bold text-emerald-500">{formData.iddisponibilidade_frota}</p>
<div className="bg-orange-500/5 p-3 rounded-xl border border-orange-500/10 mb-2">
<p className="text-[10px] uppercase font-bold text-orange-500/60 tracking-widest">ID do Registro</p>
<p className="text-lg font-bold text-orange-500">{formData.iddisponibilidade_frota}</p>
</div>
)}
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Placa (Pesquisar)</label>
<input
list="veiculos-list"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
value={formData.placa}
onChange={handlePlateChange}
placeholder="Digite ou selecione a placa..."
@ -206,3 +206,6 @@ export default function AvailabilityView() {
</div>
);
}

View File

@ -15,7 +15,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -27,7 +27,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -40,7 +40,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
@ -132,14 +132,14 @@ export default function ClaimsView() {
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Sinistros e Devoluções</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Sinistros e Devoluções</h1>
<p className="text-slate-500 text-sm">Gestão de incidentes e movimentações de frota.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar registro..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -157,7 +157,7 @@ export default function ClaimsView() {
loading={loading}
columns={[
{ header: 'ID', field: 'idsinistro_devolucao_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS', field: 'status', width: '120px', render: (row) => (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border ${
row.status === 'Sinistro' ? 'bg-rose-500/10 text-rose-500 border-rose-500/20' :
@ -238,7 +238,7 @@ export default function ClaimsView() {
<TabsTrigger
key={tab}
value={tab}
className="data-[state=active]:bg-emerald-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
className="data-[state=active]:bg-orange-600 data-[state=active]:text-white text-slate-500 dark:text-stone-400 text-[10px] uppercase font-bold px-4 py-2"
>
{tab}
</TabsTrigger>
@ -295,7 +295,7 @@ export default function ClaimsView() {
<div className="gap-1.5 flex flex-col">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 transition-all placeholder:text-slate-400 min-h-[80px]"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 min-h-[80px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
@ -375,3 +375,6 @@ export default function ClaimsView() {
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -20,7 +20,7 @@ const DarkInput = ({ label, ...props }) => (
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
@ -52,13 +52,13 @@ export default function DriversView() {
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Motoristas</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Motoristas</h1>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar motorista..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -71,7 +71,7 @@ export default function DriversView() {
<ExcelTable
data={filteredData}
columns={[
{ header: 'NOME', field: 'NOME_FAVORECIDO', width: '300px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'NOME', field: 'NOME_FAVORECIDO', width: '300px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'CPF/CNPJ', field: 'CPF_CNPJ_FAVORECIDO', width: '180px' },
{ header: 'TELEFONE', field: 'TELEFONE', width: '150px' },
{ header: 'ENDEREÇO', field: 'ENDERECO', width: '250px' },
@ -87,3 +87,6 @@ export default function DriversView() {
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -26,7 +26,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -39,7 +39,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
@ -115,7 +115,17 @@ export default function MonitoringView() {
const handleOpenModal = (item = null) => {
if (item) {
setEditingItem(item);
setFormData({ ...initialFormState, ...item });
// Format date properly for HTML date input (YYYY-MM-DD)
const formattedItem = { ...item };
if (formattedItem.data_carga) {
// Handle various date formats from backend
const date = new Date(formattedItem.data_carga);
if (!isNaN(date.getTime())) {
// Format to YYYY-MM-DD for HTML date input
formattedItem.data_carga = date.toISOString().split('T')[0];
}
}
setFormData({ ...initialFormState, ...formattedItem });
} else {
setEditingItem(null);
setFormData(initialFormState);
@ -143,14 +153,14 @@ export default function MonitoringView() {
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Monitoramento & Logística</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Monitoramento & Logística</h1>
<p className="text-slate-500 text-sm">Acompanhamento em tempo real e rastreabilidade.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar unidade..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -165,7 +175,7 @@ export default function MonitoringView() {
{/* Status Highlights */}
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Em Operação', status: 'Em Operação', color: 'bg-emerald-500/10 text-emerald-600', icon: <CheckCircle size={20} />, hover: 'hover:border-emerald-500/30' },
{ label: 'Em Operação', status: 'Em Operação', color: 'bg-orange-500/10 text-orange-600', icon: <CheckCircle size={20} />, hover: 'hover:border-orange-500/30' },
{ label: 'Veículo Alugado', status: 'Veículo Alugado', color: 'bg-blue-500/10 text-blue-600', icon: <Truck size={20} />, hover: 'hover:border-blue-500/30' },
].map((card, i) => (
<div
@ -195,7 +205,7 @@ export default function MonitoringView() {
columns={[
{ header: 'ID', field: 'idmonitoramento_frota', width: '80px' },
{ header: 'ID EXTERNO', field: 'id_externo', width: '120px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'UNIDADE', field: 'unidade', width: '150px' },
{ header: 'MOTORISTA', field: 'motorista', width: '150px' },
{ header: 'DATA CARGA', field: 'data_carga', width: '150px' },
@ -246,7 +256,7 @@ export default function MonitoringView() {
valueKey="NOME_FAVORECIDO"
placeholder="Buscar motorista..."
/>
<DarkInput type="date" label="Data Carga" value={formData.data_carga?.split('T')[0]} onChange={e => setFormData({...formData, data_carga: e.target.value})} />
<DarkInput type="date" label="Data Carga" value={formData.data_carga || ''} onChange={e => setFormData({...formData, data_carga: e.target.value})} />
</div>
<div className="grid grid-cols-1 gap-4 pb-4">
<DarkInput label="ID Externo" value={formData.id_externo} onChange={e => setFormData({...formData, id_externo: e.target.value})} />
@ -255,7 +265,7 @@ export default function MonitoringView() {
<div className="gap-4 pb-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações de Monitoramento</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[100px]"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[100px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
@ -274,7 +284,7 @@ export default function MonitoringView() {
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex items-center gap-4">
<div className="p-4 bg-emerald-500/10 rounded-2xl text-emerald-600 shadow-inner">
<div className="p-4 bg-orange-500/10 rounded-2xl text-orange-600 shadow-inner">
<Truck size={28} />
</div>
<div>
@ -282,7 +292,7 @@ export default function MonitoringView() {
Veículos: {selectedStatusRecords?.title}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Monitoramento detalhado dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-emerald-500 font-bold">"{selectedStatusRecords?.title}"</span>.
Monitoramento detalhado dos {selectedStatusRecords?.records?.length || 0} veículos com status <span className="text-orange-500 font-bold">"{selectedStatusRecords?.title}"</span>.
</DialogDescription>
</div>
</div>
@ -293,7 +303,7 @@ export default function MonitoringView() {
<ExcelTable
data={selectedStatusRecords?.records || []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
@ -324,3 +334,6 @@ export default function MonitoringView() {
</div>
);
}

View File

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

View File

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

View File

@ -15,7 +15,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -27,7 +27,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -40,7 +40,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-lg font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
@ -79,7 +79,7 @@ const StatusCell = ({ currentStatus, id, options, onUpdate }) => {
value={tempValue}
onChange={handleChange}
onBlur={handleBlur}
className="w-full bg-slate-50 dark:bg-[#141414] border border-emerald-500 rounded px-1 py-0.5 text-[10px] uppercase font-bold text-slate-700 dark:text-slate-200 focus:outline-none"
className="w-full bg-slate-50 dark:bg-[#141414] border border-orange-500 rounded px-1 py-0.5 text-[10px] uppercase font-bold text-slate-700 dark:text-slate-200 focus:outline-none"
onClick={(e) => e.stopPropagation()}
>
<option value="">Selecione...</option>
@ -94,7 +94,7 @@ const StatusCell = ({ currentStatus, id, options, onUpdate }) => {
<div
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
className={`inline-flex items-center px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border cursor-pointer hover:opacity-80 transition-opacity ${
currentStatus === 'Disponível' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' :
currentStatus === 'Disponível' ? 'bg-orange-500/10 text-orange-500 border-orange-500/20' :
currentStatus === 'Em Manutenção' ? 'bg-amber-500/10 text-amber-500 border-amber-500/20' :
'bg-slate-500/10 text-slate-500 border-slate-500/20'
}`}
@ -203,7 +203,7 @@ export default function StatusView() {
{/* ... Header ... */}
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight">Status da Frota</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight">Status da Frota</h1>
<p className="text-slate-500 text-sm">Histórico de movimentação e estados dos veículos.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
@ -211,7 +211,7 @@ export default function StatusView() {
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-lg text-sm focus:outline-none focus:border-orange-500 w-full md:w-64"
placeholder="Buscar placa..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -232,7 +232,7 @@ export default function StatusView() {
rowKey="idstatus_frota"
columns={[
{ header: 'ID', field: 'idstatus_frota', width: '80px' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'PLACA', field: 'placa', width: '100px', className: 'font-mono font-bold text-orange-600 dark:text-orange-500' },
{ header: 'STATUS', field: 'status_frota', width: '150px', render: (row) => (
<StatusCell
currentStatus={row.status_frota}
@ -327,7 +327,7 @@ export default function StatusView() {
<div className="gap-4 pb-4">
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">Observações / Motivo</label>
<textarea
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[80px]"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700 min-h-[80px]"
value={formData.obs}
onChange={e => setFormData({...formData, obs: e.target.value})}
/>
@ -344,3 +344,6 @@ export default function StatusView() {
);
}

View File

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

View File

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

View File

@ -11,7 +11,7 @@ const DarkInput = ({ label, ...props }) => (
<div className="space-y-1.5">
{label && <label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">{label}</label>}
<input
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
{...props}
/>
</div>
@ -23,7 +23,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
<select
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all cursor-pointer"
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-xl px-3 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-orange-500 focus:ring-1 focus:ring-orange-500 transition-all cursor-pointer"
>
<option value="">Selecione...</option>
{options.map(opt => (
@ -36,7 +36,7 @@ const DarkSelect = ({ label, options, value, onChange }) => (
const DarkButton = ({ children, variant = 'primary', className = '', ...props }) => {
const baseClass = "px-4 py-2 rounded-xl font-bold text-sm transition-all shadow-lg active:scale-95 flex items-center justify-center gap-2";
const variants = {
primary: "bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/10",
primary: "bg-orange-600 hover:bg-orange-500 text-white shadow-orange-500/10",
secondary: "bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-[#333]",
ghost: "bg-transparent hover:bg-slate-100 dark:hover:bg-[#2a2a2a] text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-white"
};
@ -107,14 +107,14 @@ export default function WorkshopsView() {
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-end md:items-center gap-4">
<div>
<h1 className="text-2xl font-medium text-white tracking-tight uppercase">Gestão de Oficinas</h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white tracking-tight uppercase">Gestão de Oficinas</h1>
<p className="text-slate-500 text-sm">Rede credenciada e prestadores de serviços.</p>
</div>
<div className="flex items-center gap-3 w-full md:w-auto">
<div className="relative flex-1 md:flex-none">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-emerald-500 w-full md:w-64 transition-all"
className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 pl-10 pr-4 py-2 rounded-xl text-sm focus:outline-none focus:border-orange-500 w-full md:w-64 transition-all"
placeholder="Buscar por nome, CNPJ ou cidade..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
@ -134,7 +134,7 @@ export default function WorkshopsView() {
columns={[
{ header: 'ID', field: 'idoficinas_frota', width: '80px' },
{ header: 'Cod. Estab.', field: 'cod_estabelecimento', width: '100px' },
{ header: 'Nome Reduzido', field: 'nome_reduzido', width: '220px', className: 'font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Nome Reduzido', field: 'nome_reduzido', width: '220px', className: 'font-bold text-orange-600 dark:text-orange-500' },
{ header: 'Razão Social', field: 'razao_social', width: '250px' },
{ header: 'CNPJ', field: 'cnpj', width: '150px' },
{ header: 'Tipo', field: 'tipo_estabelecimento', width: '180px' },
@ -160,7 +160,7 @@ export default function WorkshopsView() {
<DialogContent className="max-w-3xl bg-white dark:bg-[#1c1c1c] border-slate-200 dark:border-[#2a2a2a] text-slate-700 dark:text-slate-200 p-0 overflow-hidden shadow-2xl">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#222]/30">
<DialogTitle className="text-slate-800 dark:text-white uppercase font-bold flex items-center gap-2">
<div className="w-2 h-6 bg-emerald-500 rounded-xl"></div>
<div className="w-2 h-6 bg-orange-500 rounded-xl"></div>
{editingItem ? 'Editar Oficina' : 'Cadastrar Nova Oficina'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
@ -171,7 +171,7 @@ export default function WorkshopsView() {
<form onSubmit={handleSubmit} className="px-6 py-6 space-y-6 max-h-[70vh] overflow-y-auto custom-scrollbar">
{/* Seção: Identificação */}
<div className="space-y-4">
<h3 className="text-[11px] font-bold text-emerald-600 dark:text-emerald-500 uppercase tracking-[0.2em] border-b border-emerald-500/20 pb-1">Identificação</h3>
<h3 className="text-[11px] font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/20 pb-1">Identificação</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<DarkInput label="Cod. Estabelecimento" value={formData.cod_estabelecimento} onChange={e => setFormData({...formData, cod_estabelecimento: e.target.value})} />
@ -191,7 +191,7 @@ export default function WorkshopsView() {
{/* Seção: Localização e Contato */}
<div className="space-y-4">
<h3 className="text-[11px] font-bold text-emerald-600 dark:text-emerald-500 uppercase tracking-[0.2em] border-b border-emerald-500/20 pb-1">Localização e Contato</h3>
<h3 className="text-[11px] font-bold text-orange-600 dark:text-orange-500 uppercase tracking-[0.2em] border-b border-orange-500/20 pb-1">Localização e Contato</h3>
<DarkInput label="Endereço" value={formData.endereco} onChange={e => setFormData({...formData, endereco: e.target.value})} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@ -216,3 +216,6 @@ export default function WorkshopsView() {
</div>
);
}