// ServicesPage.jsx
'use strict';
const { useState, useEffect } = React;
const SVC_CATS = ['Informática','Celular','Eletrônico','Geral','Outro'];
const CAT_COLORS = { Informática:'#3B82F6', Celular:'#10B981', Eletrônico:'#F59E0B', Geral:'#6366F1', Outro:'#94A3B8' };
const CAT_ICONS = { Informática:'Laptop', Celular:'Smartphone', Eletrônico:'Tv', Geral:'Wrench', Outro:'Settings' };
function ServiceModal({ open, onClose, onSave, editSvc }) {
const empty = { name:'', category:'Informática', description:'', basePrice:0, estimatedTime:60 };
const [form, setForm] = useState(empty);
const [errors, setErrors] = useState({});
useEffect(()=>{ if(open){ setForm(editSvc?{...editSvc}:{...empty}); setErrors({}); } },[open,editSvc]);
const set=(k,v)=>setForm(f=>({...f,[k]:v}));
const validate=()=>{ const e={}; if(!form.name.trim())e.name='Nome obrigatório'; if(!form.basePrice)e.basePrice='Informe o valor'; setErrors(e); return Object.keys(e).length===0; };
const handleSave=()=>{ if(!validate())return; onSave(form); onClose(); };
return (
);
}
// ── Service Detail Modal ────────────────────────────────────────────────────
function ServiceDetailModal({ open, onClose, service, workOrders, onEdit, onDelete }) {
if (!service) return null;
const cat = service.category;
const catColor = CAT_COLORS[cat] || '#64748b';
const catIcon = CAT_ICONS[cat] || 'Wrench';
// Cruza com workOrders para calcular estatisticas de uso (match exato pelo nome do item)
const stats = (() => {
const matches = [];
let totalRevenue = 0;
let totalQty = 0;
(workOrders || []).forEach(os => {
(os.items || []).forEach(item => {
if (item.desc === service.name) {
matches.push({ os, item });
totalRevenue += item.total || 0;
totalQty += item.qty || 0;
}
});
});
matches.sort((a, b) => (b.os.entryDate || '').localeCompare(a.os.entryDate || ''));
return { matches, totalRevenue, totalQty, count: matches.length };
})();
const hours = Math.floor((service.estimatedTime || 0) / 60);
const mins = (service.estimatedTime || 0) % 60;
const timeLabel = hours > 0 ? (mins ? `${hours}h ${mins}min` : `${hours}h`) : `${mins}min`;
return (
{/* Hero */}
{cat}
#{service.id}
{service.name}
{/* Metric tiles */}
Valor Base
{fmtCurrency(service.basePrice)}
Tempo Estimado
{timeLabel}
{/* Body */}
{/* Description */}
{service.description && (
Descrição
{service.description}
)}
{/* Stats */}
Histórico de Uso
Faturamento
{fmtCurrency(stats.totalRevenue)}
Unidades
{stats.totalQty}
{/* Recent OSs */}
{stats.matches.length > 0 ? (
Últimas Ordens de Serviço
{stats.matches.length > 5 &&
mostrando 5 de {stats.matches.length}}
{stats.matches.slice(0, 5).map(({ os, item }, i) => (
0 ? 'border-t border-slate-50' : ''} hover:bg-slate-50 transition-colors`}>
#{os.num}
{os.clientName}
{fmtDate(os.entryDate)} • Qtd. {item.qty}
{fmtCurrency(item.total)}
))}
) : (
Este serviço ainda não foi usado em nenhuma OS.
)}
{/* Actions */}
Fechar
{ onDelete(service); onClose(); }}
className="text-red-500 hover:bg-red-50">Excluir
{ onEdit(service); onClose(); }}>Editar Serviço
);
}
function ServicesPage({ services, workOrders, onAdd, onUpdate, onDelete, addToast }) {
const [search, setSearch] = useState('');
const [catFilter, setCatFilter] = useState('');
const [showModal, setShowModal] = useState(false);
const [editSvc, setEditSvc] = useState(null);
const [detailSvc, setDetailSvc] = useState(null);
const [confirm, setConfirm] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(()=>{ const t=setTimeout(()=>setLoading(false),400); return()=>clearTimeout(t); },[]);
const filtered = services.filter(s=>{
const q=search.toLowerCase();
const mQ=!q||s.name?.toLowerCase().includes(q)||s.description?.toLowerCase().includes(q);
const mC=!catFilter||s.category===catFilter;
return mQ&&mC;
});
const handleSave=form=>{
if(editSvc){ onUpdate(editSvc.id,form); addToast({type:'success',msg:'Serviço atualizado!'}); }
else{ onAdd({...form,id:'SVC'+String(services.length+1).padStart(3,'0')}); addToast({type:'success',msg:'Serviço cadastrado!'}); }
};
const cats = [...new Set(services.map(s=>s.category))];
return (
{setEditSvc(null);setShowModal(true);}}>Novo Serviço}/>
{/* Category filter */}
{cats.map(c=>(
))}
{/* Cards grid */}
{loading ? (
{Array.from({length:6}).map((_,i)=>)}
) : filtered.length===0 ? (
setShowModal(true)}/>
) : (
{filtered.map(s=>{
const catColor = CAT_COLORS[s.category]||'#94A3B8';
const catIcon = CAT_ICONS[s.category]||'Wrench';
return (
setDetailSvc(s)}
className="p-5 group relative overflow-hidden transition-all hover:-translate-y-0.5 hover:shadow-card-md">
{/* Decorative side accent */}
{s.description && {s.description}
}
{s.estimatedTime}min
{fmtCurrency(s.basePrice)}
{/* Hover hint */}
);
})}
)}
{setShowModal(false);setEditSvc(null);}} onSave={handleSave} editSvc={editSvc}/>
setDetailSvc(null)} service={detailSvc} workOrders={workOrders}
onEdit={s=>{setEditSvc(s);setShowModal(true);}}
onDelete={s=>setConfirm(s.id)}/>
setConfirm(null)}
onConfirm={()=>{onDelete(confirm);addToast({type:'info',msg:'Serviço excluído.'});}}
message="Excluir este serviço do catálogo?"/>
);
}
Object.assign(window, { ServicesPage });