feat: conectar Clientes e Servicos ao banco de dados PostgreSQL
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m42s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m42s
Details
This commit is contained in:
parent
c4871262f9
commit
867275388a
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -22,17 +22,26 @@ export default function Agenda() {
|
|||
const [currentDate, setCurrentDate] = useState(today);
|
||||
const [appointments, setAppointments] = useState<any[]>([]);
|
||||
const [professionals, setProfessionals] = useState<any[]>([]);
|
||||
const [services, setServices] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchAgenda = async () => {
|
||||
try {
|
||||
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
const res = await fetch(`/api/tenant/agenda?slug=${slug}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const [agendaRes, servicesRes] = await Promise.all([
|
||||
fetch(`/api/tenant/agenda?slug=${slug}`),
|
||||
fetch(`/api/tenant/services?slug=${slug}`)
|
||||
]);
|
||||
|
||||
if (agendaRes.ok) {
|
||||
const data = await agendaRes.json();
|
||||
setProfessionals(data.professionals || []);
|
||||
setAppointments(data.appointments || []);
|
||||
}
|
||||
if (servicesRes.ok) {
|
||||
const data = await servicesRes.json();
|
||||
setServices(data || []);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
|
@ -197,11 +206,12 @@ export default function Agenda() {
|
|||
<div className="form-group">
|
||||
<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)}>
|
||||
<option>Corte Masculino - R$ 55,00 (40min)</option>
|
||||
<option>Barba Completa - R$ 40,00 (30min)</option>
|
||||
<option>Combo Corte + Barba - R$ 85,00 (60min)</option>
|
||||
<option>Design de Sobrancelha - R$ 25,00 (15min)</option>
|
||||
<option>Corte Infantil - R$ 40,00 (30min)</option>
|
||||
<option value="">Selecione um serviço...</option>
|
||||
{services.map(s => (
|
||||
<option key={s.id} value={`${s.name} - R$ ${s.price} (${s.duration}min)`}>
|
||||
{s.name} - R$ {s.price} ({s.duration}min)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
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: '' };
|
||||
|
||||
export default function Clients() {
|
||||
const [clients, setClients] = useState<Client[]>(initialClients);
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingClient, setEditingClient] = useState<Client | null>(null);
|
||||
const [formData, setFormData] = useState<Client>(emptyClient);
|
||||
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(() => {
|
||||
const savedName = localStorage.getItem('agendapro_tenant_name');
|
||||
if (savedName) {
|
||||
setClients([]);
|
||||
}
|
||||
fetchClients();
|
||||
}, []);
|
||||
|
||||
const cleanSearch = search.replace(/\D/g, '');
|
||||
|
|
@ -41,7 +44,7 @@ export default function Clients() {
|
|||
|
||||
const openAdd = () => {
|
||||
setEditingClient(null);
|
||||
setFormData({ ...emptyClient, id: Date.now().toString() });
|
||||
setFormData({ ...emptyClient });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
|
|
@ -51,26 +54,43 @@ export default function Clients() {
|
|||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim()) return;
|
||||
if (editingClient) {
|
||||
setClients(prev => prev.map(c => c.id === editingClient.id ? { ...formData } : c));
|
||||
} else {
|
||||
setClients(prev => [...prev, { ...formData, lastVisit: new Date().toISOString().split('T')[0] }]);
|
||||
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
const method = editingClient ? 'PUT' : 'POST';
|
||||
|
||||
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) => {
|
||||
setClients(prev => prev.filter(c => c.id !== id));
|
||||
setDeleteConfirm(null);
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
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) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando clientes...</div>;
|
||||
|
||||
return (
|
||||
<div className="slide-up">
|
||||
<div className="flex-between mb-6">
|
||||
|
|
@ -112,10 +132,10 @@ export default function Clients() {
|
|||
<td>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||
<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 style={{ fontSize: '12px', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<Mail size={12} /> {c.email}
|
||||
<Mail size={12} /> {c.email || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,79 +1,100 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Plus, X, Clock, DollarSign, Edit, Trash2, AlertTriangle } 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: '💆' },
|
||||
];
|
||||
import { Plus, Search, Clock, DollarSign, X, Edit, Trash2, AlertTriangle, Layers } from 'lucide-react';
|
||||
|
||||
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[] = [
|
||||
{ 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 };
|
||||
const emptyService: Service = { id: '', name: '', category: 'Cabelo', price: 0, duration: 30, type: 'fixed', description: '' };
|
||||
|
||||
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 [selectedCat, setSelectedCat] = useState<string | null>(null);
|
||||
const [editingService, setEditingService] = useState<Service | null>(null);
|
||||
const [formData, setFormData] = useState<Service>(emptyService);
|
||||
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(() => {
|
||||
const savedName = localStorage.getItem('agendapro_tenant_name');
|
||||
if (savedName) {
|
||||
setServices([
|
||||
{ id: '1', catId: '1', name: 'Corte de Cabelo (Exemplo)', desc: 'Corte padrão do estabelecimento', duration: 30, price: 40, active: true }
|
||||
]);
|
||||
}
|
||||
fetchServices();
|
||||
}, []);
|
||||
|
||||
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 = () => {
|
||||
setEditingService(null);
|
||||
setFormData({ ...emptyService, id: Date.now().toString() });
|
||||
setFormData({ ...emptyService });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const openEdit = (svc: Service) => {
|
||||
setEditingService(svc);
|
||||
setFormData({ ...svc });
|
||||
const openEdit = (service: Service) => {
|
||||
setEditingService(service);
|
||||
setFormData({ ...service });
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim()) return;
|
||||
if (editingService) {
|
||||
setServices(prev => prev.map(s => s.id === editingService.id ? { ...formData } : s));
|
||||
} else {
|
||||
setServices(prev => [...prev, { ...formData }]);
|
||||
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
||||
const method = editingService ? 'PUT' : 'POST';
|
||||
|
||||
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) => {
|
||||
setServices(prev => prev.filter(s => s.id !== id));
|
||||
setDeleteConfirm(null);
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
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 (
|
||||
<div className="slide-up">
|
||||
<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)}>
|
||||
Todos
|
||||
</button>
|
||||
{categories.filter(c => services.some(s => s.catId === c.id)).map(c => (
|
||||
<button key={c.id} className={`btn btn-sm ${selectedCat === c.id ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setSelectedCat(c.id)}>
|
||||
{c.icon} {c.name}
|
||||
{categories.map(c => (
|
||||
<button key={c} className={`btn btn-sm ${selectedCat === c ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setSelectedCat(c)}>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -93,6 +114,12 @@ export default function Services() {
|
|||
</button>
|
||||
</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' }}>
|
||||
{filtered.map(svc => (
|
||||
<div key={svc.id} className="card" style={{ padding: '20px', position: 'relative' }}>
|
||||
|
|
@ -110,7 +137,9 @@ export default function Services() {
|
|||
</div>
|
||||
)}
|
||||
<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' }}>
|
||||
<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)' }}
|
||||
|
|
@ -118,13 +147,15 @@ export default function Services() {
|
|||
</div>
|
||||
</div>
|
||||
<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-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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -147,33 +178,38 @@ export default function Services() {
|
|||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Categoria</label>
|
||||
<select className="form-select" value={formData.catId} onChange={e => setFormData(p => ({ ...p, catId: 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 }))} />
|
||||
<label className="form-label">Nome do Serviço *</label>
|
||||
<input className="form-input" placeholder="Ex: Corte Degrade" value={formData.name} onChange={e => updateForm('name', e.target.value)} />
|
||||
</div>
|
||||
<div className="grid-2">
|
||||
<div className="form-group">
|
||||
<label className="form-label"><Clock size={14} style={{ display: 'inline', marginRight: 4 }} />Duração (min)</label>
|
||||
<input className="form-input" type="number" value={formData.duration}
|
||||
onChange={e => setFormData(p => ({ ...p, duration: Number(e.target.value) }))} />
|
||||
<label className="form-label">Categoria</label>
|
||||
<input className="form-input" placeholder="Ex: Cabelo, Barba..." value={formData.category} onChange={e => updateForm('category', e.target.value)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label"><DollarSign size={14} style={{ display: 'inline', marginRight: 4 }} />Preço (R$)</label>
|
||||
<input className="form-input" type="number" step="0.01" value={formData.price}
|
||||
onChange={e => setFormData(p => ({ ...p, price: Number(e.target.value) }))} />
|
||||
<label className="form-label">Duração Estimada (min)</label>
|
||||
<input className="form-input" type="number" min="5" step="5" value={formData.duration} onChange={e => updateForm('duration', parseInt(e.target.value as string) || 0)} />
|
||||
</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 className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue