import React, { useState, useEffect, useRef } from 'react'; import { Camera, Send, FileText, User, LogOut, CheckCircle, XCircle, Download, Search, Loader2, Eye, ScanLine, DollarSign, Calendar, Plus, Trash2, Save, Edit3 } from 'lucide-react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, addDoc, query, orderBy, onSnapshot, updateDoc, doc, deleteDoc } from 'firebase/firestore'; // --- Configuração do Firebase --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // --- Carregar Tesseract.js --- const loadTesseract = () => { return new Promise((resolve, reject) => { if (window.Tesseract) { resolve(window.Tesseract); return; } const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js'; script.onload = () => resolve(window.Tesseract); script.onerror = reject; document.head.appendChild(script); }); }; // --- Componente Principal --- export default function MaterialManagerApp() { const [user, setUser] = useState(null); const [userName, setUserName] = useState(''); const [userRole, setUserRole] = useState(null); const [loadingAuth, setLoadingAuth] = useState(true); useEffect(() => { const initAuth = async () => { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { // Token customizado } else { await signInAnonymously(auth); } }; initAuth(); loadTesseract().catch(err => console.log("Erro OCR:", err)); const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setUser(currentUser); const savedName = localStorage.getItem('tech_app_name'); const savedRole = localStorage.getItem('tech_app_role'); if (savedName) setUserName(savedName); if (savedRole) setUserRole(savedRole); setLoadingAuth(false); }); return () => unsubscribe(); }, []); const handleLogin = (name, role) => { setUserName(name); setUserRole(role); localStorage.setItem('tech_app_name', name); localStorage.setItem('tech_app_role', role); }; const handleLogout = () => { setUserName(''); setUserRole(null); localStorage.removeItem('tech_app_name'); localStorage.removeItem('tech_app_role'); }; if (loadingAuth) return
; if (!userName || !userRole) { return ; } return (

Gestão de Materiais

Gestão

{userRole === 'admin' ? 'Controle de Estoque/Custos' : 'App do Técnico'}

{userName}

{userRole === 'tech' ? 'Técnico' : 'Administrador'}

{userRole === 'tech' ? ( ) : ( )}
); } // --- Tela de Login --- function LoginPage({ onLogin }) { const [name, setName] = useState(''); const [role, setRole] = useState('tech'); return (

Acesso ao Sistema

{ e.preventDefault(); if(name.trim()) onLogin(name, role); }} className="space-y-5">
setName(e.target.value)} />
); } // --- Visão do Técnico (Upload Simples) --- function TechnicianView({ userName, user }) { const [osNumber, setOsNumber] = useState(''); const [unit, setUnit] = useState(''); const [amount, setAmount] = useState(''); const [date, setDate] = useState(new Date().toISOString().split('T')[0]); const [image, setImage] = useState(null); const [preview, setPreview] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [feedback, setFeedback] = useState(null); const fileInputRef = useRef(null); const handleImageChange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (event) => { const img = new Image(); img.src = event.target.result; img.onload = () => { const canvas = document.createElement('canvas'); const MAX_WIDTH = 1000; const scaleSize = MAX_WIDTH / img.width; canvas.width = MAX_WIDTH; canvas.height = img.height * scaleSize; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.7); setImage(compressedDataUrl); setPreview(compressedDataUrl); }; }; }; const handleSubmit = async (e) => { e.preventDefault(); if (!user) return; if (!image) { setFeedback({ type: 'error', msg: 'Foto da nota é obrigatória.' }); return; } setIsSubmitting(true); try { // Cria a nota com status 'pendente' e array de itens vazio (para o ADM preencher) await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'receipts'), { osNumber, unit, totalAmount: parseFloat(amount.replace(',', '.')) || 0, // Valor total estimado pelo técnico date, technicianName: userName, technicianId: user.uid, imageUrl: image, timestamp: new Date().toISOString(), status: 'pendente', items: [] // Array vazio para ser populado pela ADM }); setFeedback({ type: 'success', msg: 'Nota enviada com sucesso!' }); setOsNumber(''); setUnit(''); setAmount(''); setImage(null); setPreview(null); if (fileInputRef.current) fileInputRef.current.value = ''; setTimeout(() => setFeedback(null), 3000); } catch (error) { console.error(error); setFeedback({ type: 'error', msg: 'Erro ao enviar.' }); } finally { setIsSubmitting(false); } }; return (

Lançar Nota de Material

{feedback && (
{feedback.type === 'success' ? : } {feedback.msg}
)}
fileInputRef.current.click()} > {preview ? (
Preview

Toque para trocar

) : (

Tirar foto da nota

)}
setAmount(e.target.value)} />
setDate(e.target.value)} />
setOsNumber(e.target.value)} />
setUnit(e.target.value)} />
); } // --- Modal de Extração de Itens (ADM) --- function ItemExtractionModal({ receipt, onClose, onSave }) { const [items, setItems] = useState(receipt.items || []); const [isProcessing, setIsProcessing] = useState(false); const [newItem, setNewItem] = useState({ name: '', qty: 1, price: '' }); const [totalCheck, setTotalCheck] = useState(0); // Calcula o total ao vivo useEffect(() => { const total = items.reduce((acc, item) => acc + (item.qty * item.price), 0); setTotalCheck(total); }, [items]); const runOCR = async () => { setIsProcessing(true); try { const Tesseract = await loadTesseract(); const result = await Tesseract.recognize(receipt.imageUrl, 'por', { logger: m => {} // logs desativados para limpar console }); const lines = result.data.text.split('\n'); const suggestedItems = []; // Lógica simples para tentar achar linhas com preço // Procura linhas que terminam com padrao monetario ex: 10,00 ou 10.00 const priceRegex = /(\d+)[.,](\d{2})\s*$/; lines.forEach(line => { const cleanLine = line.trim(); const match = cleanLine.match(priceRegex); if (match && cleanLine.length > 5) { // Se achou preço, assume que o resto é o nome const priceStr = match[0].replace(',', '.'); const price = parseFloat(priceStr); const name = cleanLine.replace(match[0], '').trim(); // Tenta achar quantidade no inicio (ex: 2x Coca Cola) let qty = 1; const qtyMatch = name.match(/^(\d+)[xX\s]/); let cleanName = name; if (qtyMatch) { qty = parseInt(qtyMatch[1]); cleanName = name.substring(qtyMatch[0].length).trim(); } // Filtra ruídos muito curtos if (cleanName.length > 2) { suggestedItems.push({ id: Date.now() + Math.random(), name: cleanName, qty: qty, price: price }); } } }); if (suggestedItems.length > 0) { // Adiciona aos itens existentes setItems([...items, ...suggestedItems]); } else { alert("O OCR leu o texto, mas não identificou o padrão de itens automaticamente. O texto bruto pode estar bagunçado. Por favor, insira manualmente consultando a imagem."); } } catch (err) { console.error(err); alert("Erro ao processar imagem."); } finally { setIsProcessing(false); } }; const addItem = () => { if (!newItem.name || !newItem.price) return; setItems([...items, { ...newItem, id: Date.now(), price: parseFloat(newItem.price) }]); setNewItem({ name: '', qty: 1, price: '' }); }; const removeItem = (id) => { setItems(items.filter(i => i.id !== id)); }; return (
{/* Lado Esquerdo: Imagem */}
Referência Visual
Cupom
Dica: A IA sugere,
mas você valida.
{/* Lado Direito: Tabela de Edição */}

Detalhamento de Materiais

{/* Tabela de Itens */}
{items.length === 0 ? ( ) : items.map(item => ( ))}
Item / Material Qtd R$ Unit. Total
Nenhum item listado.
Use o botão "Extrair Itens" ou adicione manualmente.
setItems(items.map(i => i.id === item.id ? {...i, name: e.target.value} : i))} className="w-full bg-transparent border-none outline-none focus:ring-0 font-medium" /> setItems(items.map(i => i.id === item.id ? {...i, qty: parseFloat(e.target.value)} : i))} className="w-full text-center bg-transparent outline-none" /> setItems(items.map(i => i.id === item.id ? {...i, price: parseFloat(e.target.value)} : i))} className="w-full text-right bg-transparent outline-none" /> {(item.qty * item.price).toFixed(2)}
{/* Adicionar Manual */}
setNewItem({...newItem, name: e.target.value})} className="w-full p-2 border rounded" placeholder="Ex: Cabo de Rede" />
setNewItem({...newItem, qty: e.target.value})} className="w-full p-2 border rounded text-center" />
setNewItem({...newItem, price: e.target.value})} className="w-full p-2 border rounded text-right" />
{/* Rodapé com Totais e Salvar */}

Total Calculado

R$ {totalCheck.toLocaleString('pt-BR', {minimumFractionDigits: 2})}

{receipt.totalAmount > 0 && Math.abs(totalCheck - receipt.totalAmount) > 1 && (

⚠ Difere do total da nota (R$ {receipt.totalAmount})

)}
); } // --- Visão do Administrador --- function AdminDashboard({ user }) { const [receipts, setReceipts] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState(''); const [editingReceipt, setEditingReceipt] = useState(null); // Nota sendo editada no modal useEffect(() => { if (!user) return; const q = query(collection(db, 'artifacts', appId, 'public', 'data', 'receipts'), orderBy('timestamp', 'desc')); const unsubscribe = onSnapshot(q, (snapshot) => { setReceipts(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))); setLoading(false); }); return () => unsubscribe(); }, [user]); const saveItems = async (receiptId, items) => { try { // Recalcula o total baseado nos itens const newTotal = items.reduce((acc, i) => acc + (i.qty * i.price), 0); await updateDoc(doc(db, 'artifacts', appId, 'public', 'data', 'receipts', receiptId), { items: items, totalAmount: newTotal, status: 'processado' // Marca automaticamente como processado }); setEditingReceipt(null); } catch (e) { console.error("Erro ao salvar itens", e); alert("Erro ao salvar."); } }; const handleDelete = async (id) => { if(confirm("Excluir permanentemente?")) { await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'receipts', id)); } }; // --- Exportação Excel (Granular: Linha por Material) --- const exportToCSV = () => { const BOM = "\uFEFF"; const headers = ['Data Nota', 'Técnico', 'OS', 'Unidade', 'Material/Item', 'Quantidade', 'Valor Unit (R$)', 'Valor Total (R$)']; let csvRows = []; receipts.forEach(r => { const noteDate = r.date ? new Date(r.date).toLocaleDateString('pt-BR') : '-'; if (r.items && r.items.length > 0) { // Se tem itens detalhados, cria uma linha para cada item r.items.forEach(item => { csvRows.push([ noteDate, `"${r.technicianName}"`, `"${r.osNumber}"`, `"${r.unit}"`, `"${item.name}"`, // O material em si item.qty.toString().replace('.', ','), item.price.toString().replace('.', ','), (item.qty * item.price).toString().replace('.', ',') ].join(';')); }); } else { // Se não tem itens detalhados (nota fechada), cria uma linha genérica csvRows.push([ noteDate, `"${r.technicianName}"`, `"${r.osNumber}"`, `"${r.unit}"`, `"NOTA FECHADA (SEM DETALHE)"`, "1", r.totalAmount ? r.totalAmount.toString().replace('.', ',') : '0,00', r.totalAmount ? r.totalAmount.toString().replace('.', ',') : '0,00', ].join(';')); } }); const csvContent = BOM + [headers.join(';'), ...csvRows].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.setAttribute('download', `materiais_detalhados_${new Date().toISOString().slice(0,10)}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const filteredReceipts = receipts.filter(r => r.technicianName.toLowerCase().includes(filter.toLowerCase()) || r.osNumber.includes(filter) || r.unit.toLowerCase().includes(filter.toLowerCase()) ); return (
{/* Barra de Ferramentas */}
setFilter(e.target.value)} />
{/* Lista de Notas */} {loading ? (
) : (
{filteredReceipts.map((receipt) => (
setEditingReceipt(receipt)}> Nota
{receipt.technicianName} OS: {receipt.osNumber}
{receipt.unit} • {new Date(receipt.date).toLocaleDateString('pt-BR')}
{receipt.status} {receipt.items && receipt.items.length > 0 && ( {receipt.items.length} Itens detalhados )}
R$ {receipt.totalAmount?.toLocaleString('pt-BR', {minimumFractionDigits: 2})}
))}
)} {/* Modal de Edição */} {editingReceipt && ( setEditingReceipt(null)} onSave={saveItems} /> )}
); }