736 lines
37 KiB
TypeScript
736 lines
37 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Store, Clock, Globe, Bell, Palette, Shield, Save, Copy, ExternalLink, Check, Link, Loader, Plus, Trash2 } from 'lucide-react';
|
|
|
|
const tabs = [
|
|
{ id: 'general', label: 'Geral', icon: Store },
|
|
{ id: 'hours', label: 'Horários', icon: Clock },
|
|
{ id: 'booking', label: 'Reservas Online', icon: Globe },
|
|
{ id: 'notifications', label: 'Notificações', icon: Bell },
|
|
{ id: 'appearance', label: 'Aparência', icon: Palette },
|
|
{ id: 'security', label: 'Segurança', icon: Shield },
|
|
];
|
|
|
|
const businessTypes = [
|
|
{ value: 'barbearia', label: '💈 Barbearia' },
|
|
{ value: 'manicure', label: '💅 Manicure / Nail Designer' },
|
|
{ value: 'massagem', label: '💆 Massagem / Spa' },
|
|
{ value: 'estetica', label: '✨ Estética' },
|
|
{ value: 'salao', label: '💇 Salão de Beleza' },
|
|
{ value: 'outro', label: '🏢 Outro' },
|
|
];
|
|
|
|
export default function Settings() {
|
|
const [activeTab, setActiveTab] = useState('general');
|
|
const [saved, setSaved] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// General
|
|
const [tenantName, setTenantName] = useState('');
|
|
const [slug, setSlug] = useState('');
|
|
const [businessType, setBusinessType] = useState('barbearia');
|
|
const [phone, setPhone] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [address, setAddress] = useState('');
|
|
const [city, setCity] = useState('');
|
|
const [state, setState] = useState('SP');
|
|
const [zipCode, setZipCode] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
|
|
// Hours
|
|
const [startTime, setStartTime] = useState('09:00');
|
|
const [endTime, setEndTime] = useState('19:00');
|
|
const [intervalTime, setIntervalTime] = useState(30);
|
|
const [workingDays, setWorkingDays] = useState([false, true, true, true, true, true, true]);
|
|
|
|
// Booking
|
|
const [allowOnline, setAllowOnline] = useState(true);
|
|
const [requireConfirmation, setRequireConfirmation] = useState(false);
|
|
const [autoReminders, setAutoReminders] = useState(true);
|
|
const [allowCancel, setAllowCancel] = useState(true);
|
|
const [bookingAdvanceMin, setBookingAdvanceMin] = useState(60);
|
|
const [bookingAdvanceMaxDays, setBookingAdvanceMaxDays] = useState(30);
|
|
const [cancellationLimitMin, setCancellationLimitMin] = useState(120);
|
|
|
|
// Notifications
|
|
const [notifyNew, setNotifyNew] = useState(true);
|
|
const [notifyCancel, setNotifyCancel] = useState(true);
|
|
const [notifyClient, setNotifyClient] = useState(true);
|
|
const [dailySummary, setDailySummary] = useState(false);
|
|
const [weeklyReport, setWeeklyReport] = useState(true);
|
|
const [reminderMinutes, setReminderMinutes] = useState(60);
|
|
|
|
// Appearance
|
|
const [themeColor, setThemeColor] = useState('#6C63FF');
|
|
|
|
// Custom Schedules per day & per professional
|
|
const [professionals, setProfessionals] = useState<any[]>([]);
|
|
const [selectedProfId, setSelectedProfId] = useState<string>('all');
|
|
const [dailySchedules, setDailySchedules] = useState<{ active: boolean; shifts: { start: string; end: string }[] }>([
|
|
{ active: false, shifts: [{ start: '09:00', end: '18:00' }] }, // Dom
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] }, // Seg
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] }, // Ter
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] }, // Qua
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] }, // Qui
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] }, // Sex
|
|
{ active: true, shifts: [{ start: '08:00', end: '17:00' }] }, // Sáb
|
|
]);
|
|
const [profSchedules, setProfSchedules] = useState<Record<string, { active: boolean; shifts: { start: string; end: string }[] }[]>>({});
|
|
|
|
const normalizeSchedule = (rawList: any) => {
|
|
if (!Array.isArray(rawList) || rawList.length === 0) {
|
|
return [
|
|
{ active: false, shifts: [{ start: '09:00', end: '18:00' }] },
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] },
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] },
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] },
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] },
|
|
{ active: true, shifts: [{ start: '08:00', end: '12:00' }, { start: '13:00', end: '18:00' }] },
|
|
{ active: true, shifts: [{ start: '08:00', end: '17:00' }] },
|
|
];
|
|
}
|
|
return [0, 1, 2, 3, 4, 5, 6].map((dayIdx) => {
|
|
const item = rawList[dayIdx];
|
|
if (!item) return { active: dayIdx > 0, shifts: [{ start: '09:00', end: '18:00' }] };
|
|
if (Array.isArray(item.shifts) && item.shifts.length > 0) {
|
|
return { active: !!item.active, shifts: item.shifts };
|
|
}
|
|
return {
|
|
active: !!item.active,
|
|
shifts: [{ start: item.start || '09:00', end: item.end || '19:00' }]
|
|
};
|
|
});
|
|
};
|
|
|
|
// Security
|
|
const [currentPassword, setCurrentPassword] = useState('');
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
|
const [securityError, setSecurityError] = useState('');
|
|
const [securitySuccess, setSecuritySuccess] = useState('');
|
|
|
|
// Carrega dados do banco de dados
|
|
useEffect(() => {
|
|
const fetchSettings = async () => {
|
|
const activeSlug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
|
|
setSlug(activeSlug);
|
|
|
|
try {
|
|
const res = await fetch(`/api/tenant/settings?slug=${activeSlug}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setTenantName(data.name || '');
|
|
setBusinessType(data.businessType || 'barbearia');
|
|
setPhone(data.phone || '');
|
|
setEmail(data.email || '');
|
|
setAddress(data.address || '');
|
|
setCity(data.city || '');
|
|
setState(data.state || 'SP');
|
|
setZipCode(data.zipCode || '');
|
|
setDescription(data.description || '');
|
|
setThemeColor(data.themeColor || '#6C63FF');
|
|
setStartTime(data.startTime || '09:00');
|
|
setEndTime(data.endTime || '19:00');
|
|
setIntervalTime(data.interval || 30);
|
|
setAllowOnline(data.allowOnlineBooking !== false);
|
|
setRequireConfirmation(data.requireConfirmation || false);
|
|
setAutoReminders(data.sendReminders !== false);
|
|
setAllowCancel(data.allowCancellation !== false);
|
|
setBookingAdvanceMin(data.bookingAdvanceMin || 60);
|
|
setBookingAdvanceMaxDays(data.bookingAdvanceMaxDays || 30);
|
|
setCancellationLimitMin(data.cancellationLimitMin || 120);
|
|
setNotifyNew(data.notifyNewAppointment !== false);
|
|
setNotifyCancel(data.notifyCancellation !== false);
|
|
setNotifyClient(data.notifyClientReminder !== false);
|
|
setDailySummary(data.dailySummary || false);
|
|
setWeeklyReport(data.weeklyReport !== false);
|
|
setReminderMinutes(data.reminderMinutesBefore || 60);
|
|
|
|
// Converte workingDays [1,2,3,4,5,6] para array booleano [dom,seg,...,sab]
|
|
const wd = data.workingDays || [1, 2, 3, 4, 5, 6];
|
|
setWorkingDays([0, 1, 2, 3, 4, 5, 6].map(d => wd.includes(d)));
|
|
|
|
if (data.dailySchedules) {
|
|
setDailySchedules(normalizeSchedule(data.dailySchedules));
|
|
}
|
|
if (data.profSchedules) {
|
|
const normalizedMap: Record<string, any> = {};
|
|
Object.keys(data.profSchedules).forEach(k => {
|
|
normalizedMap[k] = normalizeSchedule(data.profSchedules[k]);
|
|
});
|
|
setProfSchedules(normalizedMap);
|
|
}
|
|
|
|
// Carrega profissionais para horario individual
|
|
const teamRes = await fetch(`/api/tenant/team?slug=${activeSlug}`);
|
|
if (teamRes.ok) {
|
|
const teamData = await teamRes.json();
|
|
setProfessionals(teamData || []);
|
|
}
|
|
|
|
// Aplica cor do tema
|
|
if (data.themeColor) {
|
|
document.documentElement.style.setProperty('--accent', data.themeColor);
|
|
document.documentElement.style.setProperty('--accent-glow', `${data.themeColor}26`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Erro ao carregar configurações:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchSettings();
|
|
}, []);
|
|
|
|
const publicUrl = typeof window !== 'undefined' ? `${window.location.origin}/${slug}/agendar` : `/${slug}/agendar`;
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
|
|
// Converte workingDays boolean[] para int[]
|
|
const wdInts = workingDays.map((active, i) => active ? i : -1).filter(i => i >= 0);
|
|
|
|
try {
|
|
const res = await fetch('/api/tenant/settings', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
slug,
|
|
name: tenantName,
|
|
businessType,
|
|
phone, email, address, city, state, zipCode, description,
|
|
themeColor,
|
|
workingDays: wdInts,
|
|
startTime, endTime,
|
|
interval: intervalTime,
|
|
allowOnlineBooking: allowOnline,
|
|
requireConfirmation,
|
|
sendReminders: autoReminders,
|
|
reminderMinutesBefore: reminderMinutes,
|
|
allowCancellation: allowCancel,
|
|
bookingAdvanceMin,
|
|
bookingAdvanceMaxDays,
|
|
cancellationLimitMin,
|
|
notifyNewAppointment: notifyNew,
|
|
notifyCancellation: notifyCancel,
|
|
notifyClientReminder: notifyClient,
|
|
dailySummary,
|
|
weeklyReport,
|
|
dailySchedules,
|
|
profSchedules,
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
// Atualiza localStorage para manter compatibilidade
|
|
localStorage.setItem('agendapro_tenant_name', tenantName);
|
|
if (typeof window !== 'undefined') window.dispatchEvent(new Event('storage'));
|
|
|
|
setSaved(true);
|
|
setTimeout(() => setSaved(false), 2000);
|
|
}
|
|
} catch (err) {
|
|
console.error('Erro ao salvar:', err);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleChangePassword = async () => {
|
|
setSecurityError('');
|
|
setSecuritySuccess('');
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
setSecurityError('Preencha todos os campos.');
|
|
return;
|
|
}
|
|
if (newPassword !== confirmNewPassword) {
|
|
setSecurityError('A nova senha e a confirmação não conferem.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('agendapro_token');
|
|
const res = await fetch('/api/auth/change-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, currentPassword, newPassword })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (res.ok) {
|
|
setSecuritySuccess('Senha alterada com sucesso!');
|
|
setCurrentPassword('');
|
|
setNewPassword('');
|
|
setConfirmNewPassword('');
|
|
} else {
|
|
setSecurityError(data.error || 'Erro ao alterar senha.');
|
|
}
|
|
} catch (err) {
|
|
setSecurityError('Falha de conexão com o servidor.');
|
|
}
|
|
};
|
|
|
|
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando configurações...</div>;
|
|
|
|
return (
|
|
<div className="slide-up">
|
|
<div className="tabs">
|
|
{tabs.map(t => {
|
|
const Icon = t.icon;
|
|
return (
|
|
<button key={t.id} className={`tab ${activeTab === t.id ? 'active' : ''}`}
|
|
onClick={() => setActiveTab(t.id)}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
|
|
<Icon size={14} /> {t.label}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{activeTab === 'general' && (
|
|
<div className="card">
|
|
<h3 className="card-title mb-4">Informações do Estabelecimento</h3>
|
|
<div className="form-group">
|
|
<label className="form-label">Nome do Estabelecimento</label>
|
|
<input className="form-input" value={tenantName} onChange={e => setTenantName(e.target.value)} />
|
|
</div>
|
|
<div className="grid-2">
|
|
<div className="form-group">
|
|
<label className="form-label">Tipo de Negócio</label>
|
|
<select className="form-select" value={businessType} onChange={e => setBusinessType(e.target.value)}>
|
|
{businessTypes.map(bt => <option key={bt.value} value={bt.value}>{bt.label}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Slug (URL)</label>
|
|
<input className="form-input" value={slug} readOnly style={{ opacity: 0.6 }}
|
|
title="O slug é definido na criação do tenant e não pode ser alterado" />
|
|
</div>
|
|
</div>
|
|
<div className="grid-2">
|
|
<div className="form-group">
|
|
<label className="form-label">Telefone</label>
|
|
<input className="form-input" value={phone} onChange={e => setPhone(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">E-mail</label>
|
|
<input className="form-input" value={email} onChange={e => setEmail(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Endereço</label>
|
|
<input className="form-input" placeholder="Rua, número, bairro" value={address} onChange={e => setAddress(e.target.value)} />
|
|
</div>
|
|
<div className="grid-3">
|
|
<div className="form-group">
|
|
<label className="form-label">Cidade</label>
|
|
<input className="form-input" placeholder="São Paulo" value={city} onChange={e => setCity(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Estado</label>
|
|
<select className="form-select" value={state} onChange={e => setState(e.target.value)}>
|
|
{['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO'].map(uf => (
|
|
<option key={uf} value={uf}>{uf}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">CEP</label>
|
|
<input className="form-input" placeholder="00000-000" value={zipCode} onChange={e => setZipCode(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Descrição</label>
|
|
<textarea className="form-textarea" value={description} onChange={e => setDescription(e.target.value)} />
|
|
</div>
|
|
<div style={{ marginTop: '24px' }}>
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
|
{saving ? <><Loader size={16} className="spin" /> Salvando...</> : <><Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar Alterações'}</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'hours' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
<div className="card">
|
|
<h3 className="card-title mb-4">Grade de Agendamento</h3>
|
|
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '20px' }}>
|
|
Defina o horário do expediente e o intervalo dinâmico de tempo entre cada horário livre para o agendamento online do cliente.
|
|
</p>
|
|
<div className="grid-3" style={{ gap: '16px' }}>
|
|
<div className="form-group" style={{ margin: 0 }}>
|
|
<label className="form-label">Início do Expediente</label>
|
|
<input className="form-input" type="time" value={startTime} onChange={e => setStartTime(e.target.value)} />
|
|
</div>
|
|
<div className="form-group" style={{ margin: 0 }}>
|
|
<label className="form-label">Fim do Expediente</label>
|
|
<input className="form-input" type="time" value={endTime} onChange={e => setEndTime(e.target.value)} />
|
|
</div>
|
|
<div className="form-group" style={{ margin: 0 }}>
|
|
<label className="form-label">Intervalo de Agendamento</label>
|
|
<select className="form-select" value={intervalTime} onChange={e => setIntervalTime(Number(e.target.value))}>
|
|
<option value={15}>15 minutos</option>
|
|
<option value={20}>20 minutos</option>
|
|
<option value={30}>30 minutos</option>
|
|
<option value={40}>40 minutos</option>
|
|
<option value={45}>45 minutos</option>
|
|
<option value={60}>60 minutos</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div style={{ marginTop: '24px' }}>
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
|
{saving ? <><Loader size={16} className="spin" /> Salvando...</> : <><Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar Alterações'}</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
|
|
<div>
|
|
<h3 className="card-title">Dias e Horários Personalizados</h3>
|
|
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Configure os horários de início e fim do expediente para cada dia da semana e profissional.</p>
|
|
</div>
|
|
<div style={{ minWidth: '220px' }}>
|
|
<label className="form-label" style={{ fontSize: '12px', marginBottom: '4px' }}>Horário do Profissional:</label>
|
|
<select className="form-select" value={selectedProfId} onChange={e => setSelectedProfId(e.target.value)}>
|
|
<option value="all">🏢 Geral (Estabelecimento)</option>
|
|
{professionals.map(p => (
|
|
<option key={p.id} value={p.id}>👤 {p.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{(() => {
|
|
const activeSched = selectedProfId === 'all'
|
|
? dailySchedules
|
|
: (profSchedules[selectedProfId] || dailySchedules);
|
|
|
|
const saveActiveSched = (updated: any[]) => {
|
|
if (selectedProfId === 'all') {
|
|
setDailySchedules(updated);
|
|
} else {
|
|
setProfSchedules({ ...profSchedules, [selectedProfId]: updated });
|
|
}
|
|
};
|
|
|
|
const toggleDayActive = (i: number) => {
|
|
const updated = [...activeSched];
|
|
const current = updated[i] || { active: false, shifts: [{ start: '09:00', end: '18:00' }] };
|
|
updated[i] = { ...current, active: !current.active };
|
|
saveActiveSched(updated);
|
|
};
|
|
|
|
const addShift = (i: number) => {
|
|
const updated = [...activeSched];
|
|
const current = updated[i] || { active: true, shifts: [{ start: '09:00', end: '18:00' }] };
|
|
const shifts = [...(current.shifts || [])];
|
|
shifts.push({ start: '19:00', end: '22:00' });
|
|
updated[i] = { ...current, active: true, shifts };
|
|
saveActiveSched(updated);
|
|
};
|
|
|
|
const addLunchBreak = (i: number) => {
|
|
const updated = [...activeSched];
|
|
const current = updated[i] || { active: true, shifts: [{ start: '08:00', end: '18:00' }] };
|
|
const firstStart = current.shifts?.[0]?.start || '08:00';
|
|
const lastEnd = current.shifts?.[current.shifts.length - 1]?.end || '18:00';
|
|
updated[i] = {
|
|
active: true,
|
|
shifts: [
|
|
{ start: firstStart, end: '12:00' },
|
|
{ start: '13:00', end: lastEnd }
|
|
]
|
|
};
|
|
saveActiveSched(updated);
|
|
};
|
|
|
|
const updateShiftTime = (dayIdx: number, shiftIdx: number, field: 'start' | 'end', val: string) => {
|
|
const updated = [...activeSched];
|
|
const current = updated[dayIdx];
|
|
if (!current) return;
|
|
const shifts = [...current.shifts];
|
|
shifts[shiftIdx] = { ...shifts[shiftIdx], [field]: val };
|
|
updated[dayIdx] = { ...current, shifts };
|
|
saveActiveSched(updated);
|
|
};
|
|
|
|
const removeShift = (dayIdx: number, shiftIdx: number) => {
|
|
const updated = [...activeSched];
|
|
const current = updated[dayIdx];
|
|
if (!current) return;
|
|
const shifts = current.shifts.filter((_, idx) => idx !== shiftIdx);
|
|
updated[dayIdx] = { ...current, shifts: shifts.length > 0 ? shifts : [{ start: '09:00', end: '18:00' }] };
|
|
saveActiveSched(updated);
|
|
};
|
|
|
|
const copySegToWeekdays = () => {
|
|
const updated = [...activeSched];
|
|
const seg = updated[1];
|
|
if (!seg) return;
|
|
for (let idx = 1; idx <= 5; idx++) {
|
|
updated[idx] = JSON.parse(JSON.stringify(seg));
|
|
}
|
|
saveActiveSched(updated);
|
|
};
|
|
|
|
return ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'].map((day, i) => {
|
|
const item = activeSched[i] || { active: i > 0, shifts: [{ start: '09:00', end: '19:00' }] };
|
|
const shifts = item.shifts && item.shifts.length > 0 ? item.shifts : [{ start: '09:00', end: '19:00' }];
|
|
|
|
return (
|
|
<div key={day} style={{
|
|
padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-primary)',
|
|
border: '1px solid var(--border-color)', marginBottom: '12px'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '12px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
<div className={`toggle ${item.active ? 'active' : ''}`}
|
|
onClick={() => toggleDayActive(i)} />
|
|
<span style={{ fontWeight: 700, fontSize: '15px' }}>{day}</span>
|
|
<span style={{ fontSize: '12px', fontWeight: 600, color: item.active ? 'var(--success)' : 'var(--text-muted)' }}>
|
|
{item.active ? '• Aberto' : '• Fechado'}
|
|
</span>
|
|
</div>
|
|
|
|
{item.active && (
|
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
<button type="button" className="btn btn-sm btn-ghost" title="Dividir horário em 2 turnos com intervalo de almoço (12:00 - 13:00)"
|
|
onClick={() => addLunchBreak(i)} style={{ fontSize: '12px', gap: '4px', background: 'var(--bg-card)' }}>
|
|
🍱 Add Almoço (12h-13h)
|
|
</button>
|
|
<button type="button" className="btn btn-sm btn-ghost" title="Adicionar um novo turno ou horário de atendimento"
|
|
onClick={() => addShift(i)} style={{ fontSize: '12px', gap: '4px', background: 'var(--bg-card)' }}>
|
|
<Plus size={14} /> Novo Turno
|
|
</button>
|
|
{i === 1 && (
|
|
<button type="button" className="btn btn-sm btn-ghost" title="Copiar este horário para Segunda a Sexta"
|
|
onClick={copySegToWeekdays} style={{ fontSize: '12px', gap: '4px', background: 'var(--bg-card)', color: 'var(--accent)' }}>
|
|
<Copy size={13} /> Copiar Seg-Sex
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{item.active && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '12px' }}>
|
|
{shifts.map((shift, sIdx) => (
|
|
<div key={sIdx} style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)', minWidth: '60px' }}>
|
|
{shifts.length > 1 ? `Turno ${sIdx + 1}:` : 'Horário:'}
|
|
</span>
|
|
<input
|
|
type="time" className="form-input" style={{ width: '120px', padding: '6px 10px' }}
|
|
value={shift.start}
|
|
onChange={e => updateShiftTime(i, sIdx, 'start', e.target.value)}
|
|
/>
|
|
<span style={{ color: 'var(--text-muted)', fontSize: '13px' }}>até</span>
|
|
<input
|
|
type="time" className="form-input" style={{ width: '120px', padding: '6px 10px' }}
|
|
value={shift.end}
|
|
onChange={e => updateShiftTime(i, sIdx, 'end', e.target.value)}
|
|
/>
|
|
|
|
{shifts.length > 1 && (
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-ghost"
|
|
title="Excluir este turno"
|
|
onClick={() => removeShift(i, sIdx)}
|
|
style={{ color: 'var(--danger)', padding: '6px' }}
|
|
>
|
|
<Trash2 size={15} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
})()}
|
|
|
|
<div style={{ marginTop: '20px' }}>
|
|
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
|
{saving ? <><Loader size={16} className="spin" /> Salvando...</> : <><Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar Alterações'}</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'booking' && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
{/* PUBLIC LINK CARD */}
|
|
<div style={{
|
|
background: 'linear-gradient(135deg, rgba(108,99,255,0.15), rgba(168,85,247,0.1))',
|
|
border: '1px solid rgba(108,99,255,0.3)', borderRadius: 'var(--radius-xl)', padding: '28px'
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
|
<div style={{ width: 40, height: 40, borderRadius: 'var(--radius-md)', background: 'var(--gradient-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Link size={20} color="white" />
|
|
</div>
|
|
<div>
|
|
<h3 style={{ fontSize: '16px', fontWeight: 700 }}>Link Público de Agendamento</h3>
|
|
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Compartilhe este link com seus clientes para agendamento online</p>
|
|
</div>
|
|
</div>
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: '8px', background: 'var(--bg-primary)',
|
|
borderRadius: 'var(--radius-sm)', padding: '4px 4px 4px 16px', border: '1px solid var(--border-color)'
|
|
}}>
|
|
<Globe size={16} color="var(--text-muted)" />
|
|
<span style={{ flex: 1, fontSize: '14px', fontWeight: 500, color: 'var(--accent)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{publicUrl}
|
|
</span>
|
|
<button className="btn btn-sm btn-secondary" style={{ flexShrink: 0 }}
|
|
onClick={() => { navigator.clipboard.writeText(publicUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); }}>
|
|
{copied ? <><Check size={14} /> Copiado!</> : <><Copy size={14} /> Copiar</>}
|
|
</button>
|
|
<a href={`/${slug}/agendar`} target="_blank" rel="noopener noreferrer" className="btn btn-sm btn-primary" style={{ flexShrink: 0, textDecoration: 'none' }}>
|
|
<ExternalLink size={14} /> Abrir
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{/* BOOKING OPTIONS */}
|
|
<div className="card">
|
|
<h3 className="card-title mb-4">Configurações de Reserva Online</h3>
|
|
{[
|
|
{ label: 'Permitir agendamento online', desc: 'Clientes podem agendar pela página pública', value: allowOnline, setter: setAllowOnline },
|
|
{ label: 'Exigir confirmação manual', desc: 'Agendamentos precisam de aprovação', value: requireConfirmation, setter: setRequireConfirmation },
|
|
{ label: 'Enviar lembretes automáticos', desc: 'Enviar lembrete por e-mail/SMS antes do horário', value: autoReminders, setter: setAutoReminders },
|
|
{ label: 'Permitir cancelamento online', desc: 'Clientes podem cancelar pela plataforma', value: allowCancel, setter: setAllowCancel },
|
|
].map((item, i) => (
|
|
<div key={i} style={{
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
padding: '16px 0', borderBottom: i < 3 ? '1px solid var(--border-color)' : 'none'
|
|
}}>
|
|
<div>
|
|
<div style={{ fontWeight: 600, fontSize: '14px' }}>{item.label}</div>
|
|
<div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{item.desc}</div>
|
|
</div>
|
|
<div className={`toggle ${item.value ? 'active' : ''}`}
|
|
onClick={() => item.setter(!item.value)} />
|
|
</div>
|
|
))}
|
|
<div className="grid-2 mt-6">
|
|
<div className="form-group">
|
|
<label className="form-label">Antecedência mínima (min)</label>
|
|
<input className="form-input" type="number" value={bookingAdvanceMin} onChange={e => setBookingAdvanceMin(Number(e.target.value))} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Antecedência máxima (dias)</label>
|
|
<input className="form-input" type="number" value={bookingAdvanceMaxDays} onChange={e => setBookingAdvanceMaxDays(Number(e.target.value))} />
|
|
</div>
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Limite para cancelamento (minutos antes)</label>
|
|
<input className="form-input" type="number" value={cancellationLimitMin} onChange={e => setCancellationLimitMin(Number(e.target.value))} />
|
|
</div>
|
|
<button className="btn btn-primary mt-4" onClick={handleSave} disabled={saving}>
|
|
<Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'notifications' && (
|
|
<div className="card">
|
|
<h3 className="card-title mb-4">Preferências de Notificação</h3>
|
|
{[
|
|
{ label: 'Novo agendamento', desc: 'Quando um novo agendamento é criado', value: notifyNew, setter: setNotifyNew },
|
|
{ label: 'Cancelamento', desc: 'Quando um agendamento é cancelado', value: notifyCancel, setter: setNotifyCancel },
|
|
{ label: 'Lembrete para o cliente', desc: 'Enviar lembrete automático', value: notifyClient, setter: setNotifyClient },
|
|
{ label: 'Resumo diário', desc: 'Receber resumo dos agendamentos do dia', value: dailySummary, setter: setDailySummary },
|
|
{ label: 'Relatório semanal', desc: 'Relatório de desempenho semanal', value: weeklyReport, setter: setWeeklyReport },
|
|
].map((item, i) => (
|
|
<div key={i} style={{
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
padding: '14px 0', borderBottom: i < 4 ? '1px solid var(--border-color)' : 'none'
|
|
}}>
|
|
<div>
|
|
<div style={{ fontWeight: 600, fontSize: '14px' }}>{item.label}</div>
|
|
<div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{item.desc}</div>
|
|
</div>
|
|
<div className={`toggle ${item.value ? 'active' : ''}`}
|
|
onClick={() => item.setter(!item.value)} />
|
|
</div>
|
|
))}
|
|
<div className="form-group mt-6">
|
|
<label className="form-label">Tempo do lembrete (minutos antes)</label>
|
|
<input className="form-input" type="number" value={reminderMinutes} onChange={e => setReminderMinutes(Number(e.target.value))} style={{ width: '200px' }} />
|
|
</div>
|
|
<button className="btn btn-primary mt-4" onClick={handleSave} disabled={saving}>
|
|
<Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'appearance' && (
|
|
<div className="card">
|
|
<h3 className="card-title mb-4">Personalização Visual</h3>
|
|
<div className="form-group">
|
|
<label className="form-label">Cor Principal</label>
|
|
<div className="flex-gap">
|
|
{['#6C63FF', '#a855f7', '#ec4899', '#f59e0b', '#22c55e', '#3b82f6', '#ef4444'].map(c => (
|
|
<div key={c} style={{
|
|
width: 40, height: 40, borderRadius: 'var(--radius-sm)', background: c,
|
|
cursor: 'pointer', border: themeColor === c ? '3px solid white' : '2px solid transparent',
|
|
transition: 'all 0.2s', boxShadow: themeColor === c ? `0 0 0 2px ${c}` : 'none'
|
|
}}
|
|
onMouseEnter={e => (e.currentTarget as HTMLElement).style.transform = 'scale(1.1)'}
|
|
onMouseLeave={e => (e.currentTarget as HTMLElement).style.transform = 'scale(1)'}
|
|
onClick={() => {
|
|
setThemeColor(c);
|
|
document.documentElement.style.setProperty('--accent', c);
|
|
document.documentElement.style.setProperty('--accent-glow', `${c}26`);
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<p style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '8px' }}>Clique em uma cor para aplicar. A cor será salva no banco de dados.</p>
|
|
</div>
|
|
<button className="btn btn-primary mt-4" onClick={handleSave} disabled={saving}>
|
|
<Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'security' && (
|
|
<div className="card">
|
|
<h3 className="card-title mb-4">Alterar Senha de Acesso</h3>
|
|
{securityError && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '16px', fontSize: '14px' }}>{securityError}</div>}
|
|
{securitySuccess && <div style={{ background: 'var(--success)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '16px', fontSize: '14px' }}>{securitySuccess}</div>}
|
|
<div className="form-group">
|
|
<label className="form-label">Senha Atual</label>
|
|
<input className="form-input" type="password" placeholder="••••••••" value={currentPassword} onChange={e => setCurrentPassword(e.target.value)} />
|
|
</div>
|
|
<div className="grid-2">
|
|
<div className="form-group">
|
|
<label className="form-label">Nova Senha</label>
|
|
<input className="form-input" type="password" placeholder="••••••••" value={newPassword} onChange={e => setNewPassword(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">Confirmar Nova Senha</label>
|
|
<input className="form-input" type="password" placeholder="••••••••" value={confirmNewPassword} onChange={e => setConfirmNewPassword(e.target.value)} />
|
|
</div>
|
|
</div>
|
|
<button className="btn btn-primary mt-4" onClick={handleChangePassword}>
|
|
<Shield size={16} /> Alterar Senha
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|