diff --git a/src/app/[slug]/agendar/page.tsx b/src/app/[slug]/agendar/page.tsx index c747fc9..2d48a89 100644 --- a/src/app/[slug]/agendar/page.tsx +++ b/src/app/[slug]/agendar/page.tsx @@ -29,23 +29,40 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: allowOnlineBooking: true, bookingAdvanceMin: 60, bookingAdvanceMaxDays: 30 }); - // Algoritmo dinâmico de fatiamento de grade - const generateSlots = (startTimeStr: string, endTimeStr: string, intervalMin: number) => { + const [profileSchedules, setProfileSchedules] = useState({ + dailySchedules: null, + profSchedules: {}, + interval: 30, + startTime: '09:00', + endTime: '19:00' + }); + + // Algoritmo dinâmico de fatiamento de grade multi-turno + const generateSlotsForShifts = (shifts: { start: string; end: string }[], intervalMin: number) => { const generated: string[] = []; - const [sh, sm] = startTimeStr.split(':').map(Number); - const [eh, em] = endTimeStr.split(':').map(Number); - - let currentMinutes = sh * 60 + sm; - const endMinutes = eh * 60 + em; - - while (currentMinutes < endMinutes) { - const h = Math.floor(currentMinutes / 60); - const m = currentMinutes % 60; - const timeStr = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; - generated.push(timeStr); - currentMinutes += intervalMin; + for (const shift of shifts) { + if (!shift.start || !shift.end) continue; + const [sh, sm] = shift.start.split(':').map(Number); + const [eh, em] = shift.end.split(':').map(Number); + + let currentMinutes = sh * 60 + sm; + const endMinutes = eh * 60 + em; + + while (currentMinutes < endMinutes) { + const h = Math.floor(currentMinutes / 60); + const m = currentMinutes % 60; + const timeStr = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; + if (!generated.includes(timeStr)) { + generated.push(timeStr); + } + currentMinutes += intervalMin; + } } - return generated; + return generated.sort(); + }; + + const generateSlots = (startTimeStr: string, endTimeStr: string, intervalMin: number) => { + return generateSlotsForShifts([{ start: startTimeStr, end: endTimeStr }], intervalMin); }; useEffect(() => { @@ -74,11 +91,16 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: bookingAdvanceMaxDays: profile.bookingAdvanceMaxDays || 30, }); - // Gerar slots com horários do tenant - const startTime = profile.startTime || '09:00'; - const endTime = profile.endTime || '19:00'; - const interval = profile.interval || 30; - setSlots(generateSlots(startTime, endTime, interval)); + setProfileSchedules({ + dailySchedules: profile.dailySchedules || null, + profSchedules: profile.profSchedules || {}, + interval: profile.interval || 30, + startTime: profile.startTime || '09:00', + endTime: profile.endTime || '19:00' + }); + + // Slots iniciais + setSlots(generateSlots(profile.startTime || '09:00', profile.endTime || '19:00', profile.interval || 30)); // Aplicar cor do tema if (profile.themeColor) { @@ -86,7 +108,6 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: document.documentElement.style.setProperty('--accent-glow', `${profile.themeColor}26`); } } else { - // Fallback setSlots(generateSlots('09:00', '19:00', 30)); } @@ -108,6 +129,36 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: fetchData(); }, [slug]); + // Recalcular horários quando mudar data ou profissional + useEffect(() => { + if (!selectedDate) return; + const dateObj = new Date(selectedDate + 'T12:00:00'); + const dayOfWeek = dateObj.getDay(); + + const interval = profileSchedules.interval || 30; + + let dayConfig = null; + if (selectedProfessional && profileSchedules.profSchedules?.[selectedProfessional]) { + dayConfig = profileSchedules.profSchedules[selectedProfessional][dayOfWeek]; + } + if (!dayConfig && profileSchedules.dailySchedules) { + dayConfig = profileSchedules.dailySchedules[dayOfWeek]; + } + + if (dayConfig) { + if (!dayConfig.active) { + setSlots([]); + return; + } + if (Array.isArray(dayConfig.shifts) && dayConfig.shifts.length > 0) { + setSlots(generateSlotsForShifts(dayConfig.shifts, interval)); + return; + } + } + + setSlots(generateSlots(profileSchedules.startTime || '09:00', profileSchedules.endTime || '19:00', interval)); + }, [selectedDate, selectedProfessional, profileSchedules]); + const today = new Date(); const maxDays = establishment.bookingAdvanceMaxDays || 30; const dates = Array.from({ length: maxDays }, (_, i) => { diff --git a/src/app/api/tenant/profile/route.ts b/src/app/api/tenant/profile/route.ts index 76b7bc6..c1f165f 100644 --- a/src/app/api/tenant/profile/route.ts +++ b/src/app/api/tenant/profile/route.ts @@ -47,6 +47,8 @@ export async function GET(req: NextRequest) { allowOnlineBooking: settings.allow_online_booking !== false, bookingAdvanceMin: settings.booking_advance_min || 60, bookingAdvanceMaxDays: settings.booking_advance_max_days || 30, + dailySchedules: settings.daily_schedules || null, + profSchedules: settings.prof_schedules || {}, }); } catch (error: any) { console.error('GET Profile error:', error); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 2ceb0cc..331ef8a 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Store, Clock, Globe, Bell, Palette, Shield, Save, Copy, ExternalLink, Check, Link, Loader } from 'lucide-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 }, @@ -67,16 +67,41 @@ export default function Settings() { // Custom Schedules per day & per professional const [professionals, setProfessionals] = useState([]); const [selectedProfId, setSelectedProfId] = useState('all'); - const [dailySchedules, setDailySchedules] = useState<{ active: boolean; start: string; end: string }[]>([ - { active: false, start: '09:00', end: '18:00' }, // Dom - { active: true, start: '09:00', end: '19:00' }, // Seg - { active: true, start: '09:00', end: '19:00' }, // Ter - { active: true, start: '09:00', end: '19:00' }, // Qua - { active: true, start: '09:00', end: '19:00' }, // Qui - { active: true, start: '09:00', end: '19:00' }, // Sex - { active: true, start: '09:00', end: '17:00' }, // Sáb + 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>({}); + const [profSchedules, setProfSchedules] = useState>({}); + + 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(''); @@ -127,10 +152,14 @@ export default function Settings() { setWorkingDays([0, 1, 2, 3, 4, 5, 6].map(d => wd.includes(d))); if (data.dailySchedules) { - setDailySchedules(data.dailySchedules); + setDailySchedules(normalizeSchedule(data.dailySchedules)); } if (data.profSchedules) { - setProfSchedules(data.profSchedules); + const normalizedMap: Record = {}; + Object.keys(data.profSchedules).forEach(k => { + normalizedMap[k] = normalizeSchedule(data.profSchedules[k]); + }); + setProfSchedules(normalizedMap); } // Carrega profissionais para horario individual @@ -384,9 +413,7 @@ export default function Settings() { ? dailySchedules : (profSchedules[selectedProfId] || dailySchedules); - const updateSched = (index: number, field: 'active' | 'start' | 'end', val: any) => { - const updated = [...activeSched]; - updated[index] = { ...updated[index], [field]: val }; + const saveActiveSched = (updated: any[]) => { if (selectedProfId === 'all') { setDailySchedules(updated); } else { @@ -394,27 +421,138 @@ export default function Settings() { } }; + 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, start: '09:00', end: '19:00' }; + 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 (
-
{day}
-
updateSched(i, 'active', !item.active)} /> - - {item.active ? ( -
- updateSched(i, 'start', e.target.value)} /> - até - updateSched(i, 'end', e.target.value)} /> +
+
+
toggleDayActive(i)} /> + {day} + + {item.active ? '• Aberto' : '• Fechado'} + +
+ + {item.active && ( +
+ + + {i === 1 && ( + + )} +
+ )} +
+ + {item.active && ( +
+ {shifts.map((shift, sIdx) => ( +
+ + {shifts.length > 1 ? `Turno ${sIdx + 1}:` : 'Horário:'} + + updateShiftTime(i, sIdx, 'start', e.target.value)} + /> + até + updateShiftTime(i, sIdx, 'end', e.target.value)} + /> + + {shifts.length > 1 && ( + + )} +
+ ))}
- ) : ( - Fechado neste dia )}
);