arreglos varios antes de la primera version en produccion
This commit is contained in:
@@ -35,6 +35,9 @@ var eaAllowedRoutes = []struct {
|
|||||||
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/services/?$`)},
|
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/services/?$`)},
|
||||||
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/availabilities/?$`)},
|
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/availabilities/?$`)},
|
||||||
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/providers/\d+/?$`)},
|
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/providers/\d+/?$`)},
|
||||||
|
// Solo este ajuste concreto (horario del negocio); el resto de /settings
|
||||||
|
// sigue bloqueado porque contiene datos sensibles.
|
||||||
|
{http.MethodGet, regexp.MustCompile(`^/ea-api/v1/settings/company_working_plan/?$`)},
|
||||||
}
|
}
|
||||||
|
|
||||||
func eaRouteAllowed(method, path string) bool {
|
func eaRouteAllowed(method, path string) bool {
|
||||||
@@ -174,6 +177,7 @@ type eaAppointment struct {
|
|||||||
|
|
||||||
type bookRequest struct {
|
type bookRequest struct {
|
||||||
Nombre string `json:"nombre"`
|
Nombre string `json:"nombre"`
|
||||||
|
Apellidos string `json:"apellidos"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Telefono string `json:"telefono"`
|
Telefono string `json:"telefono"`
|
||||||
Mensaje string `json:"mensaje"`
|
Mensaje string `json:"mensaje"`
|
||||||
@@ -206,11 +210,12 @@ func handleBook(ea *eaClient) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Nombre = strings.TrimSpace(req.Nombre)
|
req.Nombre = strings.TrimSpace(req.Nombre)
|
||||||
|
req.Apellidos = strings.TrimSpace(req.Apellidos)
|
||||||
req.Email = strings.TrimSpace(req.Email)
|
req.Email = strings.TrimSpace(req.Email)
|
||||||
req.Telefono = strings.TrimSpace(req.Telefono)
|
req.Telefono = strings.TrimSpace(req.Telefono)
|
||||||
req.Mensaje = strings.TrimSpace(req.Mensaje)
|
req.Mensaje = strings.TrimSpace(req.Mensaje)
|
||||||
|
|
||||||
if req.Nombre == "" || req.Email == "" || req.Telefono == "" ||
|
if req.Nombre == "" || req.Apellidos == "" || req.Email == "" || req.Telefono == "" ||
|
||||||
len(req.ServiceIDs) == 0 || req.ProviderID <= 0 ||
|
len(req.ServiceIDs) == 0 || req.ProviderID <= 0 ||
|
||||||
!reDate.MatchString(req.Date) || !reTime.MatchString(req.Time) {
|
!reDate.MatchString(req.Date) || !reTime.MatchString(req.Time) {
|
||||||
writeJSONError(w, http.StatusBadRequest, "datos_incompletos")
|
writeJSONError(w, http.StatusBadRequest, "datos_incompletos")
|
||||||
@@ -268,7 +273,7 @@ func handleBook(ea *eaClient) http.HandlerFunc {
|
|||||||
names[i] = s.Name
|
names[i] = s.Name
|
||||||
}
|
}
|
||||||
notesLines := []string{
|
notesLines := []string{
|
||||||
"Nombre: " + req.Nombre,
|
"Nombre: " + req.Nombre + " " + req.Apellidos,
|
||||||
"Email: " + req.Email,
|
"Email: " + req.Email,
|
||||||
"Teléfono: " + req.Telefono,
|
"Teléfono: " + req.Telefono,
|
||||||
"Servicios: " + strings.Join(names, " + "),
|
"Servicios: " + strings.Join(names, " + "),
|
||||||
@@ -320,11 +325,10 @@ func (c *eaClient) findOrCreateCustomer(req bookRequest) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
first, last := splitName(req.Nombre)
|
|
||||||
var created eaCustomer
|
var created eaCustomer
|
||||||
body := eaCustomer{
|
body := eaCustomer{
|
||||||
FirstName: first,
|
FirstName: req.Nombre,
|
||||||
LastName: last,
|
LastName: req.Apellidos,
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
Phone: req.Telefono,
|
Phone: req.Telefono,
|
||||||
Notes: "Creado desde web MAY Studio",
|
Notes: "Creado desde web MAY Studio",
|
||||||
@@ -338,18 +342,6 @@ func (c *eaClient) findOrCreateCustomer(req bookRequest) (int, error) {
|
|||||||
return int(created.ID), nil
|
return int(created.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitName(full string) (first, last string) {
|
|
||||||
parts := strings.Fields(full)
|
|
||||||
switch len(parts) {
|
|
||||||
case 0:
|
|
||||||
return full, ""
|
|
||||||
case 1:
|
|
||||||
return parts[0], ""
|
|
||||||
default:
|
|
||||||
return parts[0], strings.Join(parts[1:], " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
port := flag.Int("port", 8080, "puerto del servidor")
|
port := flag.Int("port", 8080, "puerto del servidor")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|||||||
+32
-18
@@ -23,6 +23,7 @@ func TestEaRouteAllowed(t *testing.T) {
|
|||||||
{"GET", "/ea-api/v1/services", true},
|
{"GET", "/ea-api/v1/services", true},
|
||||||
{"GET", "/ea-api/v1/availabilities", true},
|
{"GET", "/ea-api/v1/availabilities", true},
|
||||||
{"GET", "/ea-api/v1/providers/1", true},
|
{"GET", "/ea-api/v1/providers/1", true},
|
||||||
|
{"GET", "/ea-api/v1/settings/company_working_plan", true},
|
||||||
|
|
||||||
// Bloqueadas: clientes y citas ya NO pasan por el proxy
|
// Bloqueadas: clientes y citas ya NO pasan por el proxy
|
||||||
{"GET", "/ea-api/v1/customers", false},
|
{"GET", "/ea-api/v1/customers", false},
|
||||||
@@ -34,6 +35,8 @@ func TestEaRouteAllowed(t *testing.T) {
|
|||||||
{"GET", "/ea-api/v1/providers", false}, // listar todos
|
{"GET", "/ea-api/v1/providers", false}, // listar todos
|
||||||
{"GET", "/ea-api/v1/providers/abc", false}, // id no numérico
|
{"GET", "/ea-api/v1/providers/abc", false}, // id no numérico
|
||||||
{"GET", "/ea-api/v1/settings", false},
|
{"GET", "/ea-api/v1/settings", false},
|
||||||
|
{"GET", "/ea-api/v1/settings/email_footer", false}, // otros settings no
|
||||||
|
{"POST", "/ea-api/v1/settings/company_working_plan", false},
|
||||||
{"POST", "/ea-api/v1/services", false},
|
{"POST", "/ea-api/v1/services", false},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@@ -43,22 +46,6 @@ func TestEaRouteAllowed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSplitName(t *testing.T) {
|
|
||||||
cases := []struct{ in, first, last string }{
|
|
||||||
{"", "", ""},
|
|
||||||
{"Ana", "Ana", ""},
|
|
||||||
{"Ana López", "Ana", "López"},
|
|
||||||
{"Ana María López Gil", "Ana", "María López Gil"},
|
|
||||||
{" Ana López ", "Ana", "López"},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
f, l := splitName(c.in)
|
|
||||||
if f != c.first || l != c.last {
|
|
||||||
t.Errorf("splitName(%q) = (%q,%q), want (%q,%q)", c.in, f, l, c.first, c.last)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFlexIntUnmarshal(t *testing.T) {
|
func TestFlexIntUnmarshal(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
A flexInt `json:"a"`
|
A flexInt `json:"a"`
|
||||||
@@ -79,6 +66,7 @@ func TestFlexIntUnmarshal(t *testing.T) {
|
|||||||
// duración total y servicio principal en el servidor.
|
// duración total y servicio principal en el servidor.
|
||||||
func TestHandleBook(t *testing.T) {
|
func TestHandleBook(t *testing.T) {
|
||||||
var gotAppt eaAppointment
|
var gotAppt eaAppointment
|
||||||
|
var gotCust eaCustomer
|
||||||
var hits []string
|
var hits []string
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -96,6 +84,7 @@ func TestHandleBook(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
hits = append(hits, "POST customers")
|
hits = append(hits, "POST customers")
|
||||||
|
json.NewDecoder(r.Body).Decode(&gotCust)
|
||||||
json.NewEncoder(w).Encode(eaCustomer{ID: 99})
|
json.NewEncoder(w).Encode(eaCustomer{ID: 99})
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/index.php/api/v1/appointments", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/index.php/api/v1/appointments", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -109,7 +98,7 @@ func TestHandleBook(t *testing.T) {
|
|||||||
|
|
||||||
ea := &eaClient{base: srv.URL, http: srv.Client()}
|
ea := &eaClient{base: srv.URL, http: srv.Client()}
|
||||||
|
|
||||||
body := `{"nombre":"Ana López","email":"ana@x.com","telefono":"600","mensaje":"hola",
|
body := `{"nombre":"Ana","apellidos":"López Gil","email":"ana@x.com","telefono":"600","mensaje":"hola",
|
||||||
"providerId":1,"serviceIds":[1,2],"date":"2026-07-01","time":"10:00"}`
|
"providerId":1,"serviceIds":[1,2],"date":"2026-07-01","time":"10:00"}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
@@ -129,6 +118,10 @@ func TestHandleBook(t *testing.T) {
|
|||||||
if gotAppt.CustomerID != 99 || gotAppt.ProviderID != 1 || gotAppt.Status != "booked" {
|
if gotAppt.CustomerID != 99 || gotAppt.ProviderID != 1 || gotAppt.Status != "booked" {
|
||||||
t.Errorf("appt = %+v", gotAppt)
|
t.Errorf("appt = %+v", gotAppt)
|
||||||
}
|
}
|
||||||
|
// Nombre y apellidos viajan por separado hasta EA, sin adivinar cuál es cuál.
|
||||||
|
if gotCust.FirstName != "Ana" || gotCust.LastName != "López Gil" {
|
||||||
|
t.Errorf("cliente = %q %q, want \"Ana\" \"López Gil\"", gotCust.FirstName, gotCust.LastName)
|
||||||
|
}
|
||||||
if !strings.Contains(gotAppt.Notes, "Duración total: 75 min") ||
|
if !strings.Contains(gotAppt.Notes, "Duración total: 75 min") ||
|
||||||
!strings.Contains(gotAppt.Notes, "Servicios: Corte + Tinte") {
|
!strings.Contains(gotAppt.Notes, "Servicios: Corte + Tinte") {
|
||||||
t.Errorf("notes = %q", gotAppt.Notes)
|
t.Errorf("notes = %q", gotAppt.Notes)
|
||||||
@@ -155,7 +148,7 @@ func TestHandleBookRejectsUnknownService(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
ea := &eaClient{base: srv.URL, http: srv.Client()}
|
ea := &eaClient{base: srv.URL, http: srv.Client()}
|
||||||
|
|
||||||
body := `{"nombre":"Ana","email":"a@x.com","telefono":"600","providerId":1,"serviceIds":[999],"date":"2026-07-01","time":"10:00"}`
|
body := `{"nombre":"Ana","apellidos":"López","email":"a@x.com","telefono":"600","providerId":1,"serviceIds":[999],"date":"2026-07-01","time":"10:00"}`
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))
|
handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))
|
||||||
|
|
||||||
@@ -166,3 +159,24 @@ func TestHandleBookRejectsUnknownService(t *testing.T) {
|
|||||||
t.Error("se creó una cita con un servicio inexistente")
|
t.Error("se creó una cita con un servicio inexistente")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sin apellidos ⇒ 400 sin llegar a llamar a EA.
|
||||||
|
func TestHandleBookRequiresApellidos(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { called = true })
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
defer srv.Close()
|
||||||
|
ea := &eaClient{base: srv.URL, http: srv.Client()}
|
||||||
|
|
||||||
|
body := `{"nombre":"Ana","email":"a@x.com","telefono":"600","providerId":1,"serviceIds":[1],"date":"2026-07-01","time":"10:00"}`
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
handleBook(ea)(rec, httptest.NewRequest(http.MethodPost, "/api/book", strings.NewReader(body)))
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("status = %d, want 400", rec.Code)
|
||||||
|
}
|
||||||
|
if called {
|
||||||
|
t.Error("se llamó a EA con datos incompletos")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+69
-8
@@ -138,14 +138,18 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="nombre">Nombre *</label>
|
<label for="nombre">Nombre *</label>
|
||||||
<input type="text" id="nombre" name="nombre" autocomplete="name" required>
|
<input type="text" id="nombre" name="nombre" autocomplete="given-name" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="apellidos">Apellidos *</label>
|
||||||
|
<input type="text" id="apellidos" name="apellidos" autocomplete="family-name" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email *</label>
|
<label for="email">Email *</label>
|
||||||
<input type="email" id="email" name="email" autocomplete="email" required>
|
<input type="email" id="email" name="email" autocomplete="email" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="telefono">Teléfono *</label>
|
<label for="telefono">Teléfono *</label>
|
||||||
<input type="tel" id="telefono" name="telefono" autocomplete="tel" required>
|
<input type="tel" id="telefono" name="telefono" autocomplete="tel" required>
|
||||||
@@ -209,7 +213,57 @@
|
|||||||
// === EasyAppointments config ===
|
// === EasyAppointments config ===
|
||||||
// IMPORTANTE: Después de configurar EasyAppointments, ve al admin (http://localhost:8888)
|
// IMPORTANTE: Después de configurar EasyAppointments, ve al admin (http://localhost:8888)
|
||||||
// y crea al menos un Provider. Anota su ID y ponlo aquí.
|
// y crea al menos un Provider. Anota su ID y ponlo aquí.
|
||||||
const PROVIDER_ID = 1;
|
const PROVIDER_ID = 5;
|
||||||
|
|
||||||
|
// ---- Horario desde EasyAppointments (Settings → Business, company_working_plan) ----
|
||||||
|
// Si la petición falla se mantiene el horario estático del HTML.
|
||||||
|
async function loadSchedule() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/ea-api/v1/settings/company_working_plan');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const setting = await res.json();
|
||||||
|
const plan = JSON.parse(setting.value);
|
||||||
|
|
||||||
|
const DAYS = [
|
||||||
|
['monday', 'Lunes'], ['tuesday', 'Martes'], ['wednesday', 'Miércoles'],
|
||||||
|
['thursday', 'Jueves'], ['friday', 'Viernes'], ['saturday', 'Sábado'], ['sunday', 'Domingo']
|
||||||
|
];
|
||||||
|
const fmt = t => t.replace(/^0/, '');
|
||||||
|
// "09:00-18:00 con pausa 14:30-15:00" → "9:00 — 14:30 / 15:00 — 18:00"
|
||||||
|
const dayLabel = p => {
|
||||||
|
if (!p || !p.start || !p.end) return null; // cerrado
|
||||||
|
const breaks = (p.breaks || []).slice().sort((a, b) => a.start.localeCompare(b.start));
|
||||||
|
const segs = [];
|
||||||
|
let cur = p.start;
|
||||||
|
for (const b of breaks) {
|
||||||
|
if (b.start > cur) segs.push(fmt(cur) + ' — ' + fmt(b.start));
|
||||||
|
if (b.end > cur) cur = b.end;
|
||||||
|
}
|
||||||
|
if (cur < p.end) segs.push(fmt(cur) + ' — ' + fmt(p.end));
|
||||||
|
return segs.length ? segs.join(' / ') : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Agrupa días consecutivos con el mismo horario.
|
||||||
|
const groups = [];
|
||||||
|
for (const [key, nombre] of DAYS) {
|
||||||
|
const label = dayLabel(plan[key]);
|
||||||
|
const last = groups[groups.length - 1];
|
||||||
|
if (last && last.label === label) last.dias.push(nombre);
|
||||||
|
else groups.push({ label, dias: [nombre] });
|
||||||
|
}
|
||||||
|
const rows = groups.map(g => {
|
||||||
|
const dias = g.dias.length === 1 ? g.dias[0]
|
||||||
|
: g.dias.length === 2 ? g.dias[0] + ' y ' + g.dias[1]
|
||||||
|
: g.dias[0] + ' a ' + g.dias[g.dias.length - 1];
|
||||||
|
return g.label
|
||||||
|
? `<div class="schedule-row"><span>${esc(dias)}</span><span>${esc(g.label)}</span></div>`
|
||||||
|
: `<div class="schedule-row closed"><span>${esc(dias)}</span><span>Cerrado</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
if (rows) document.querySelector('.schedule').innerHTML = rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('No se pudo cargar el horario desde EA; se muestra el estático.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Service selector state ----
|
// ---- Service selector state ----
|
||||||
let selectedServices = []; // [{id, nombre, duracion}]
|
let selectedServices = []; // [{id, nombre, duracion}]
|
||||||
@@ -293,7 +347,7 @@
|
|||||||
renderServiceSelector(categorias);
|
renderServiceSelector(categorias);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (selectorContainer) selectorContainer.innerHTML = '<p class="services-loading">No se pudieron cargar los servicios desde EasyAppointments.</p>';
|
if (selectorContainer) selectorContainer.innerHTML = '<p class="services-loading">No se pudieron cargar los servicios. Inténtalo más tarde o llámanos.</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +634,11 @@
|
|||||||
if (selectedServices.length === 0) { alert('Selecciona al menos un servicio'); return; }
|
if (selectedServices.length === 0) { alert('Selecciona al menos un servicio'); return; }
|
||||||
if (!selectedDate || !selectedHora) { alert('Selecciona día y hora'); return; }
|
if (!selectedDate || !selectedHora) { alert('Selecciona día y hora'); return; }
|
||||||
|
|
||||||
|
const nombre = form.nombre.value.trim();
|
||||||
|
const apellidos = form.apellidos.value.trim();
|
||||||
|
// EasyAppointments exige nombre y apellido al crear el cliente.
|
||||||
|
if (!nombre || !apellidos) { alert('Escribe tu nombre y tus apellidos'); return; }
|
||||||
|
|
||||||
const email = form.email.value.trim();
|
const email = form.email.value.trim();
|
||||||
if (!email) { alert('El email es obligatorio'); return; }
|
if (!email) { alert('El email es obligatorio'); return; }
|
||||||
|
|
||||||
@@ -591,7 +650,8 @@
|
|||||||
// El servidor (server-side) busca/crea el cliente y crea la cita
|
// El servidor (server-side) busca/crea el cliente y crea la cita
|
||||||
// contra EasyAppointments. El navegador solo envía la selección.
|
// contra EasyAppointments. El navegador solo envía la selección.
|
||||||
const payload = {
|
const payload = {
|
||||||
nombre: form.nombre.value.trim(),
|
nombre: nombre,
|
||||||
|
apellidos: apellidos,
|
||||||
email: email,
|
email: email,
|
||||||
telefono: form.telefono.value.trim(),
|
telefono: form.telefono.value.trim(),
|
||||||
mensaje: form.mensaje.value.trim(),
|
mensaje: form.mensaje.value.trim(),
|
||||||
@@ -616,7 +676,7 @@
|
|||||||
renderSlots(savedDate, savedDow);
|
renderSlots(savedDate, savedDow);
|
||||||
form.style.display = 'none';
|
form.style.display = 'none';
|
||||||
result.className = 'booking-result booking-success';
|
result.className = 'booking-result booking-success';
|
||||||
result.textContent = '¡Reserva confirmada! Te esperamos. Gestionada con EasyAppointments.';
|
result.textContent = '¡Reserva confirmada! Te esperamos.';
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json().catch(() => ({}));
|
const err = await res.json().catch(() => ({}));
|
||||||
result.className = 'booking-result booking-error';
|
result.className = 'booking-result booking-error';
|
||||||
@@ -625,7 +685,7 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
result.className = 'booking-result booking-error';
|
result.className = 'booking-result booking-error';
|
||||||
result.textContent = err.message || 'No se pudo conectar con EasyAppointments.';
|
result.textContent = 'No se pudo completar la reserva. Inténtalo de nuevo o llámanos.';
|
||||||
}
|
}
|
||||||
|
|
||||||
result.style.display = 'block';
|
result.style.display = 'block';
|
||||||
@@ -635,6 +695,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadSchedule();
|
||||||
loadServices();
|
loadServices();
|
||||||
initCalendar();
|
initCalendar();
|
||||||
initBookingForm();
|
initBookingForm();
|
||||||
|
|||||||
Reference in New Issue
Block a user