feat: conectar Clientes e Servicos ao banco de dados PostgreSQL
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m42s Details

This commit is contained in:
Sidney 2026-06-07 19:32:49 -03:00
parent c4871262f9
commit 867275388a
5 changed files with 385 additions and 109 deletions

View File

@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const slug = searchParams.get('slug');
if (!slug) {
return NextResponse.json({ error: 'Missing tenant slug' }, { status: 400 });
}
try {
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
if (tenantRes.rows.length === 0) return NextResponse.json([]);
const tenantId = tenantRes.rows[0].id;
const res = await query(`
SELECT id, name, email, phone, cpf,
TO_CHAR(birth_date, 'YYYY-MM-DD') as birthDate,
notes, total_visits as visits, total_spent as spent,
TO_CHAR(last_visit_at, 'YYYY-MM-DD') as "lastVisit"
FROM clients
WHERE tenant_id = $1 AND is_active = true
ORDER BY name ASC
`, [tenantId]);
const clients = res.rows.map(row => ({
...row,
spent: Number(row.spent || 0),
visits: Number(row.visits || 0),
email: row.email || '',
phone: row.phone || '',
cpf: row.cpf || '',
birthDate: row.birthdate || '',
notes: row.notes || '',
lastVisit: row.lastVisit || ''
}));
return NextResponse.json(clients);
} catch (error: any) {
console.error('GET Clients error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const { slug, name, email, phone, cpf, birthDate, notes } = await req.json();
if (!slug || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
const tenantId = tenantRes.rows[0].id;
const res = await query(
`INSERT INTO clients (tenant_id, name, email, phone, cpf, birth_date, notes)
VALUES ($1, $2, $3, $4, $5, NULLIF($6, '')::DATE, $7) RETURNING *`,
[tenantId, name, email, phone, cpf, birthDate, notes]
);
return NextResponse.json(res.rows[0]);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function PUT(req: NextRequest) {
try {
const { id, name, email, phone, cpf, birthDate, notes } = await req.json();
if (!id || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
const res = await query(
`UPDATE clients
SET name = $1, email = $2, phone = $3, cpf = $4, birth_date = NULLIF($5, '')::DATE, notes = $6, updated_at = NOW()
WHERE id = $7 RETURNING *`,
[name, email, phone, cpf, birthDate, notes, id]
);
return NextResponse.json(res.rows[0]);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
await query('DELETE FROM clients WHERE id = $1', [id]);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const slug = searchParams.get('slug');
if (!slug) {
return NextResponse.json({ error: 'Missing tenant slug' }, { status: 400 });
}
try {
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
if (tenantRes.rows.length === 0) return NextResponse.json([]);
const tenantId = tenantRes.rows[0].id;
const res = await query(`
SELECT s.id, s.name, s.description, s.duration_minutes as duration, s.price, s.price_type as type,
COALESCE(c.name, 'Geral') as category
FROM services s
LEFT JOIN service_categories c ON s.category_id = c.id
WHERE s.tenant_id = $1 AND s.is_active = true
ORDER BY s.sort_order ASC, s.name ASC
`, [tenantId]);
const services = res.rows.map(row => ({
...row,
duration: Number(row.duration || 30),
price: Number(row.price || 0)
}));
return NextResponse.json(services);
} catch (error: any) {
console.error('GET Services error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const { slug, name, category, price, duration, type, description } = await req.json();
if (!slug || !name) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
const tenantId = tenantRes.rows[0].id;
// Get or create category
let categoryId = null;
if (category) {
let catRes = await query('SELECT id FROM service_categories WHERE tenant_id = $1 AND name = $2', [tenantId, category]);
if (catRes.rows.length === 0) {
catRes = await query('INSERT INTO service_categories (tenant_id, name) VALUES ($1, $2) RETURNING id', [tenantId, category]);
}
categoryId = catRes.rows[0].id;
}
const res = await query(
`INSERT INTO services (tenant_id, category_id, name, description, duration_minutes, price, price_type)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
[tenantId, categoryId, name, description, duration, price, type]
);
return NextResponse.json(res.rows[0]);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function PUT(req: NextRequest) {
try {
const { id, slug, name, category, price, duration, type, description } = await req.json();
if (!id || !name || !slug) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
const tenantRes = await query('SELECT id FROM tenants WHERE slug = $1', [slug]);
if (tenantRes.rows.length === 0) return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
const tenantId = tenantRes.rows[0].id;
let categoryId = null;
if (category) {
let catRes = await query('SELECT id FROM service_categories WHERE tenant_id = $1 AND name = $2', [tenantId, category]);
if (catRes.rows.length === 0) {
catRes = await query('INSERT INTO service_categories (tenant_id, name) VALUES ($1, $2) RETURNING id', [tenantId, category]);
}
categoryId = catRes.rows[0].id;
}
const res = await query(
`UPDATE services
SET category_id = $1, name = $2, description = $3, duration_minutes = $4, price = $5, price_type = $6, updated_at = NOW()
WHERE id = $7 RETURNING *`,
[categoryId, name, description, duration, price, type, id]
);
return NextResponse.json(res.rows[0]);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
await query('DELETE FROM services WHERE id = $1', [id]);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@ -22,17 +22,26 @@ export default function Agenda() {
const [currentDate, setCurrentDate] = useState(today); const [currentDate, setCurrentDate] = useState(today);
const [appointments, setAppointments] = useState<any[]>([]); const [appointments, setAppointments] = useState<any[]>([]);
const [professionals, setProfessionals] = useState<any[]>([]); const [professionals, setProfessionals] = useState<any[]>([]);
const [services, setServices] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const fetchAgenda = async () => { const fetchAgenda = async () => {
try { try {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium'; const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
const res = await fetch(`/api/tenant/agenda?slug=${slug}`); const [agendaRes, servicesRes] = await Promise.all([
if (res.ok) { fetch(`/api/tenant/agenda?slug=${slug}`),
const data = await res.json(); fetch(`/api/tenant/services?slug=${slug}`)
]);
if (agendaRes.ok) {
const data = await agendaRes.json();
setProfessionals(data.professionals || []); setProfessionals(data.professionals || []);
setAppointments(data.appointments || []); setAppointments(data.appointments || []);
} }
if (servicesRes.ok) {
const data = await servicesRes.json();
setServices(data || []);
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
@ -197,11 +206,12 @@ export default function Agenda() {
<div className="form-group"> <div className="form-group">
<label className="form-label"><Scissors size={14} style={{ display: 'inline', marginRight: 4 }} />Serviço</label> <label className="form-label"><Scissors size={14} style={{ display: 'inline', marginRight: 4 }} />Serviço</label>
<select className="form-select" value={newService} onChange={e => setNewService(e.target.value)}> <select className="form-select" value={newService} onChange={e => setNewService(e.target.value)}>
<option>Corte Masculino - R$ 55,00 (40min)</option> <option value="">Selecione um serviço...</option>
<option>Barba Completa - R$ 40,00 (30min)</option> {services.map(s => (
<option>Combo Corte + Barba - R$ 85,00 (60min)</option> <option key={s.id} value={`${s.name} - R$ ${s.price} (${s.duration}min)`}>
<option>Design de Sobrancelha - R$ 25,00 (15min)</option> {s.name} - R$ {s.price} ({s.duration}min)
<option>Corte Infantil - R$ 40,00 (30min)</option> </option>
))}
</select> </select>
</div> </div>
<div className="form-group"> <div className="form-group">

View File

@ -5,31 +5,34 @@ interface Client {
id: string; name: string; email: string; phone: string; cpf: string; birthDate: string; notes: string; visits: number; spent: number; lastVisit: string; id: string; name: string; email: string; phone: string; cpf: string; birthDate: string; notes: string; visits: number; spent: number; lastVisit: string;
} }
const initialClients: Client[] = [
{ id: '1', name: 'João Mendes', email: 'joao@email.com', phone: '(11) 97777-1111', cpf: '123.456.789-00', birthDate: '1990-05-15', notes: 'Cliente VIP', visits: 12, spent: 780, lastVisit: '2026-05-30' },
{ id: '2', name: 'Pedro Oliveira', email: 'pedro@email.com', phone: '(11) 97777-2222', cpf: '', birthDate: '', notes: '', visits: 8, spent: 520, lastVisit: '2026-05-28' },
{ id: '3', name: 'Lucas Ferreira', email: 'lucas@email.com', phone: '(11) 97777-3333', cpf: '', birthDate: '', notes: '', visits: 5, spent: 275, lastVisit: '2026-05-25' },
{ id: '4', name: 'Mateus Costa', email: 'mateus@email.com', phone: '(11) 97777-4444', cpf: '', birthDate: '', notes: '', visits: 3, spent: 165, lastVisit: '2026-05-20' },
{ id: '5', name: 'Gabriel Souza', email: 'gabriel@email.com', phone: '(11) 97777-5555', cpf: '', birthDate: '', notes: 'Prefere corte degradê', visits: 15, spent: 1120, lastVisit: '2026-05-31' },
{ id: '6', name: 'André Lima', email: 'andre@email.com', phone: '(11) 97777-6666', cpf: '', birthDate: '', notes: '', visits: 22, spent: 1540, lastVisit: '2026-05-31' },
{ id: '7', name: 'Ricardo Alves', email: 'ricardo@email.com', phone: '(11) 97777-7777', cpf: '', birthDate: '', notes: '', visits: 6, spent: 330, lastVisit: '2026-05-27' },
];
const emptyClient: Client = { id: '', name: '', email: '', phone: '', cpf: '', birthDate: '', notes: '', visits: 0, spent: 0, lastVisit: '' }; const emptyClient: Client = { id: '', name: '', email: '', phone: '', cpf: '', birthDate: '', notes: '', visits: 0, spent: 0, lastVisit: '' };
export default function Clients() { export default function Clients() {
const [clients, setClients] = useState<Client[]>(initialClients); const [clients, setClients] = useState<Client[]>([]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [editingClient, setEditingClient] = useState<Client | null>(null); const [editingClient, setEditingClient] = useState<Client | null>(null);
const [formData, setFormData] = useState<Client>(emptyClient); const [formData, setFormData] = useState<Client>(emptyClient);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null); const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const fetchClients = async () => {
try {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
const res = await fetch(`/api/tenant/clients?slug=${slug}`);
if (res.ok) {
const data = await res.json();
setClients(data);
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
const savedName = localStorage.getItem('agendapro_tenant_name'); fetchClients();
if (savedName) {
setClients([]);
}
}, []); }, []);
const cleanSearch = search.replace(/\D/g, ''); const cleanSearch = search.replace(/\D/g, '');
@ -41,7 +44,7 @@ export default function Clients() {
const openAdd = () => { const openAdd = () => {
setEditingClient(null); setEditingClient(null);
setFormData({ ...emptyClient, id: Date.now().toString() }); setFormData({ ...emptyClient });
setShowModal(true); setShowModal(true);
}; };
@ -51,26 +54,43 @@ export default function Clients() {
setShowModal(true); setShowModal(true);
}; };
const handleSave = () => { const handleSave = async () => {
if (!formData.name.trim()) return; if (!formData.name.trim()) return;
if (editingClient) { const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
setClients(prev => prev.map(c => c.id === editingClient.id ? { ...formData } : c)); const method = editingClient ? 'PUT' : 'POST';
} else {
setClients(prev => [...prev, { ...formData, lastVisit: new Date().toISOString().split('T')[0] }]); try {
const res = await fetch('/api/tenant/clients', {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, slug })
});
if (res.ok) {
fetchClients();
setShowModal(false);
setEditingClient(null);
}
} catch (e) {
console.error(e);
} }
setShowModal(false);
setEditingClient(null);
}; };
const handleDelete = (id: string) => { const handleDelete = async (id: string) => {
setClients(prev => prev.filter(c => c.id !== id)); try {
setDeleteConfirm(null); await fetch(`/api/tenant/clients?id=${id}`, { method: 'DELETE' });
setClients(prev => prev.filter(c => c.id !== id));
setDeleteConfirm(null);
} catch (e) {
console.error(e);
}
}; };
const updateForm = (field: keyof Client, value: string) => { const updateForm = (field: keyof Client, value: string) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
}; };
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando clientes...</div>;
return ( return (
<div className="slide-up"> <div className="slide-up">
<div className="flex-between mb-6"> <div className="flex-between mb-6">
@ -112,10 +132,10 @@ export default function Clients() {
<td> <td>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
<span style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '4px' }}> <span style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '4px' }}>
<Phone size={12} color="var(--text-muted)" /> {c.phone} <Phone size={12} color="var(--text-muted)" /> {c.phone || '-'}
</span> </span>
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '4px' }}> <span style={{ fontSize: '12px', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '4px' }}>
<Mail size={12} /> {c.email} <Mail size={12} /> {c.email || '-'}
</span> </span>
</div> </div>
</td> </td>

View File

@ -1,79 +1,100 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Plus, X, Clock, DollarSign, Edit, Trash2, AlertTriangle } from 'lucide-react'; import { Plus, Search, Clock, DollarSign, X, Edit, Trash2, AlertTriangle, Layers } from 'lucide-react';
const categories = [
{ id: '1', name: 'Cabelo', icon: '✂️' },
{ id: '2', name: 'Barba', icon: '🧔' },
{ id: '3', name: 'Combos', icon: '⭐' },
{ id: '4', name: 'Manicure', icon: '💅' },
{ id: '5', name: 'Massagem', icon: '💆' },
];
interface Service { interface Service {
id: string; catId: string; name: string; desc: string; duration: number; price: number; active: boolean; id: string; name: string; category: string; price: number; duration: number; type: 'fixed' | 'starting' | 'variable'; description: string;
} }
const initialServices: Service[] = [ const emptyService: Service = { id: '', name: '', category: 'Cabelo', price: 0, duration: 30, type: 'fixed', description: '' };
{ id: '1', catId: '1', name: 'Corte Masculino', desc: 'Corte moderno com lavagem e finalização', duration: 40, price: 55, active: true },
{ id: '2', catId: '1', name: 'Corte Infantil', desc: 'Corte para crianças até 12 anos', duration: 30, price: 40, active: true },
{ id: '3', catId: '2', name: 'Barba Completa', desc: 'Aparar, modelar e hidratação', duration: 30, price: 40, active: true },
{ id: '4', catId: '2', name: 'Design de Sobrancelha', desc: 'Alinhamento com navalha', duration: 15, price: 25, active: true },
{ id: '5', catId: '3', name: 'Combo Corte + Barba', desc: 'Corte masculino + barba completa', duration: 60, price: 85, active: true },
{ id: '6', catId: '4', name: 'Manicure Completa', desc: 'Lixar, cuticular e esmaltação', duration: 45, price: 50, active: true },
{ id: '7', catId: '4', name: 'Pedicure', desc: 'Tratamento completo para os pés', duration: 50, price: 55, active: true },
{ id: '8', catId: '5', name: 'Massagem Relaxante', desc: 'Massagem corpo inteiro 60min', duration: 60, price: 120, active: true },
{ id: '9', catId: '5', name: 'Quick Massage', desc: 'Massagem expressa 20min', duration: 20, price: 45, active: true },
];
const emptyService: Service = { id: '', catId: '1', name: '', desc: '', duration: 30, price: 50, active: true };
export default function Services() { export default function Services() {
const [services, setServices] = useState<Service[]>(initialServices); const [services, setServices] = useState<Service[]>([]);
const [search, setSearch] = useState('');
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [selectedCat, setSelectedCat] = useState<string | null>(null); const [selectedCat, setSelectedCat] = useState<string | null>(null);
const [editingService, setEditingService] = useState<Service | null>(null); const [editingService, setEditingService] = useState<Service | null>(null);
const [formData, setFormData] = useState<Service>(emptyService); const [formData, setFormData] = useState<Service>(emptyService);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null); const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const fetchServices = async () => {
try {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
const res = await fetch(`/api/tenant/services?slug=${slug}`);
if (res.ok) {
const data = await res.json();
setServices(data);
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
const savedName = localStorage.getItem('agendapro_tenant_name'); fetchServices();
if (savedName) {
setServices([
{ id: '1', catId: '1', name: 'Corte de Cabelo (Exemplo)', desc: 'Corte padrão do estabelecimento', duration: 30, price: 40, active: true }
]);
}
}, []); }, []);
const filtered = selectedCat ? services.filter(s => s.catId === selectedCat) : services; // Derive unique categories from fetched services
const categories = Array.from(new Set(services.map(s => s.category || 'Geral')));
const filtered = services.filter(s => {
const matchesSearch = s.name.toLowerCase().includes(search.toLowerCase()) ||
(s.category && s.category.toLowerCase().includes(search.toLowerCase()));
const matchesCat = selectedCat ? s.category === selectedCat : true;
return matchesSearch && matchesCat;
});
const openAdd = () => { const openAdd = () => {
setEditingService(null); setEditingService(null);
setFormData({ ...emptyService, id: Date.now().toString() }); setFormData({ ...emptyService });
setShowModal(true); setShowModal(true);
}; };
const openEdit = (svc: Service) => { const openEdit = (service: Service) => {
setEditingService(svc); setEditingService(service);
setFormData({ ...svc }); setFormData({ ...service });
setShowModal(true); setShowModal(true);
}; };
const handleSave = () => { const handleSave = async () => {
if (!formData.name.trim()) return; if (!formData.name.trim()) return;
if (editingService) { const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
setServices(prev => prev.map(s => s.id === editingService.id ? { ...formData } : s)); const method = editingService ? 'PUT' : 'POST';
} else {
setServices(prev => [...prev, { ...formData }]); try {
const res = await fetch('/api/tenant/services', {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, slug })
});
if (res.ok) {
fetchServices();
setShowModal(false);
setEditingService(null);
}
} catch (e) {
console.error(e);
} }
setShowModal(false);
setEditingService(null);
}; };
const handleDelete = (id: string) => { const handleDelete = async (id: string) => {
setServices(prev => prev.filter(s => s.id !== id)); try {
setDeleteConfirm(null); await fetch(`/api/tenant/services?id=${id}`, { method: 'DELETE' });
setServices(prev => prev.filter(s => s.id !== id));
setDeleteConfirm(null);
} catch (e) {
console.error(e);
}
}; };
const updateForm = (field: keyof Service, value: string | number) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando serviços...</div>;
return ( return (
<div className="slide-up"> <div className="slide-up">
<div className="flex-between mb-6"> <div className="flex-between mb-6">
@ -81,10 +102,10 @@ export default function Services() {
<button className={`btn btn-sm ${!selectedCat ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setSelectedCat(null)}> <button className={`btn btn-sm ${!selectedCat ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setSelectedCat(null)}>
Todos Todos
</button> </button>
{categories.filter(c => services.some(s => s.catId === c.id)).map(c => ( {categories.map(c => (
<button key={c.id} className={`btn btn-sm ${selectedCat === c.id ? 'btn-primary' : 'btn-secondary'}`} <button key={c} className={`btn btn-sm ${selectedCat === c ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setSelectedCat(c.id)}> onClick={() => setSelectedCat(c)}>
{c.icon} {c.name} {c}
</button> </button>
))} ))}
</div> </div>
@ -93,6 +114,12 @@ export default function Services() {
</button> </button>
</div> </div>
<div style={{ marginBottom: '24px', position: 'relative' }}>
<Search size={18} style={{ position: 'absolute', left: 14, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)', pointerEvents: 'none' }} />
<input className="form-input" placeholder="Buscar serviço por nome ou categoria..."
style={{ width: '100%', maxWidth: '380px', paddingLeft: '42px' }} value={search} onChange={e => setSearch(e.target.value)} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
{filtered.map(svc => ( {filtered.map(svc => (
<div key={svc.id} className="card" style={{ padding: '20px', position: 'relative' }}> <div key={svc.id} className="card" style={{ padding: '20px', position: 'relative' }}>
@ -110,7 +137,9 @@ export default function Services() {
</div> </div>
)} )}
<div className="flex-between" style={{ marginBottom: '12px' }}> <div className="flex-between" style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '24px' }}>{categories.find(c => c.id === svc.catId)?.icon}</span> <span className="badge badge-purple" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
<Layers size={12} /> {svc.category || 'Geral'}
</span>
<div style={{ display: 'flex', gap: '4px' }}> <div style={{ display: 'flex', gap: '4px' }}>
<button className="btn-ghost btn-icon" title="Editar" onClick={() => openEdit(svc)}><Edit size={14} /></button> <button className="btn-ghost btn-icon" title="Editar" onClick={() => openEdit(svc)}><Edit size={14} /></button>
<button className="btn-ghost btn-icon" title="Excluir" style={{ color: 'var(--danger)' }} <button className="btn-ghost btn-icon" title="Excluir" style={{ color: 'var(--danger)' }}
@ -118,13 +147,15 @@ export default function Services() {
</div> </div>
</div> </div>
<h3 style={{ fontSize: '16px', fontWeight: 700, marginBottom: '6px' }}>{svc.name}</h3> <h3 style={{ fontSize: '16px', fontWeight: 700, marginBottom: '6px' }}>{svc.name}</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '16px' }}>{svc.desc}</p> <p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '16px' }}>{svc.description || 'Sem descrição'}</p>
<div className="flex-between"> <div className="flex-between">
<div className="flex-gap gap-sm"> <div className="flex-gap gap-sm">
<span className="badge badge-purple"><Clock size={12} /> {svc.duration}min</span> <span className="badge badge-outline"><Clock size={12} /> {svc.duration}min</span>
</div> </div>
<span style={{ fontSize: '20px', fontWeight: 800, color: 'var(--accent)' }}> <span style={{ fontSize: '20px', fontWeight: 800, color: 'var(--accent)' }}>
R$ {svc.price.toFixed(2)} {svc.type === 'starting' && <span style={{fontSize: '12px', fontWeight: 600, marginRight: '4px', color: 'var(--text-muted)'}}>A partir de</span>}
{svc.type === 'variable' && <span style={{fontSize: '12px', fontWeight: 600, marginRight: '4px', color: 'var(--text-muted)'}}>Variável</span>}
{svc.type !== 'variable' && `R$ ${svc.price.toFixed(2)}`}
</span> </span>
</div> </div>
</div> </div>
@ -147,33 +178,38 @@ export default function Services() {
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div className="form-group"> <div className="form-group">
<label className="form-label">Categoria</label> <label className="form-label">Nome do Serviço *</label>
<select className="form-select" value={formData.catId} onChange={e => setFormData(p => ({ ...p, catId: e.target.value }))}> <input className="form-input" placeholder="Ex: Corte Degrade" value={formData.name} onChange={e => updateForm('name', e.target.value)} />
{categories.map(c => <option key={c.id} value={c.id}>{c.icon} {c.name}</option>)}
</select>
</div>
<div className="form-group">
<label className="form-label">Nome do serviço *</label>
<input className="form-input" placeholder="Ex: Corte Masculino" value={formData.name}
onChange={e => setFormData(p => ({ ...p, name: e.target.value }))} />
</div>
<div className="form-group">
<label className="form-label">Descrição</label>
<textarea className="form-textarea" placeholder="Descreva o serviço..." value={formData.desc}
onChange={e => setFormData(p => ({ ...p, desc: e.target.value }))} />
</div> </div>
<div className="grid-2"> <div className="grid-2">
<div className="form-group"> <div className="form-group">
<label className="form-label"><Clock size={14} style={{ display: 'inline', marginRight: 4 }} />Duração (min)</label> <label className="form-label">Categoria</label>
<input className="form-input" type="number" value={formData.duration} <input className="form-input" placeholder="Ex: Cabelo, Barba..." value={formData.category} onChange={e => updateForm('category', e.target.value)} />
onChange={e => setFormData(p => ({ ...p, duration: Number(e.target.value) }))} />
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label"><DollarSign size={14} style={{ display: 'inline', marginRight: 4 }} />Preço (R$)</label> <label className="form-label">Duração Estimada (min)</label>
<input className="form-input" type="number" step="0.01" value={formData.price} <input className="form-input" type="number" min="5" step="5" value={formData.duration} onChange={e => updateForm('duration', parseInt(e.target.value as string) || 0)} />
onChange={e => setFormData(p => ({ ...p, price: Number(e.target.value) }))} />
</div> </div>
</div> </div>
<div className="grid-2">
<div className="form-group">
<label className="form-label">Tipo de Preço</label>
<select className="form-select" value={formData.type} onChange={e => updateForm('type', e.target.value as any)}>
<option value="fixed">Preço Fixo</option>
<option value="starting">A partir de</option>
<option value="variable">Variável (Sob Consulta)</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Valor (R$)</label>
<input className="form-input" type="number" step="0.01" min="0" disabled={formData.type === 'variable'}
value={formData.price} onChange={e => updateForm('price', parseFloat(e.target.value as string) || 0)} />
</div>
</div>
<div className="form-group">
<label className="form-label">Descrição (Opcional)</label>
<textarea className="form-textarea" placeholder="Detalhes sobre o serviço..." value={formData.description} onChange={e => updateForm('description', e.target.value)} />
</div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button> <button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>