From f2a0460c88429537ce5630b9ca7b0374ee4d1f89 Mon Sep 17 00:00:00 2001 From: ialcarazsalazar Date: Sun, 15 Feb 2026 01:54:44 +0000 Subject: [PATCH] feat: add employee scheduling and operational reports Add weekly shift scheduling grid with morning/afternoon/night/off cycles and bulk save. Add operational reports with occupancy, revenue by room type, booking sources, and guest satisfaction KPIs using CSS-based visualizations. Register both route sets in backend app.js and frontend App.jsx. Co-Authored-By: Claude Opus 4.6 --- backend/hotel_hacienda/src/app.js | 4 + .../operational-reports.controller.js | 125 +++++++ .../src/controllers/schedules.controller.js | 79 +++++ .../src/routes/operational-reports.routes.js | 10 + .../src/routes/schedules.routes.js | 10 + frontend/Frontend-Hotel/src/App.jsx | 4 + .../src/pages/Reports/OperationalReports.jsx | 312 +++++++++++++++++ .../src/pages/Schedules/Schedules.jsx | 331 ++++++++++++++++++ .../src/services/operationalReportService.js | 5 + .../src/services/scheduleService.js | 4 + 10 files changed, 884 insertions(+) create mode 100644 backend/hotel_hacienda/src/controllers/operational-reports.controller.js create mode 100644 backend/hotel_hacienda/src/controllers/schedules.controller.js create mode 100644 backend/hotel_hacienda/src/routes/operational-reports.routes.js create mode 100644 backend/hotel_hacienda/src/routes/schedules.routes.js create mode 100644 frontend/Frontend-Hotel/src/pages/Reports/OperationalReports.jsx create mode 100644 frontend/Frontend-Hotel/src/pages/Schedules/Schedules.jsx create mode 100644 frontend/Frontend-Hotel/src/services/operationalReportService.js create mode 100644 frontend/Frontend-Hotel/src/services/scheduleService.js diff --git a/backend/hotel_hacienda/src/app.js b/backend/hotel_hacienda/src/app.js index 9a0005e..a4ce7e0 100644 --- a/backend/hotel_hacienda/src/app.js +++ b/backend/hotel_hacienda/src/app.js @@ -41,6 +41,8 @@ const guestsRoutes = require('./routes/guests.routes'); const housekeepingRoutes = require('./routes/housekeeping.routes'); const roomserviceRoutes = require('./routes/roomservice.routes'); const eventsRoutes = require('./routes/events.routes'); +const schedulesRoutes = require('./routes/schedules.routes'); +const operationalReportsRoutes = require('./routes/operational-reports.routes'); //Prefijo - Auth routes are public (no middleware) app.use('/api/auth', authRoutes); @@ -68,5 +70,7 @@ app.use('/api/guests', authMiddleware, guestsRoutes); app.use('/api/housekeeping', authMiddleware, housekeepingRoutes); app.use('/api/room-service', authMiddleware, roomserviceRoutes); app.use('/api/events', authMiddleware, eventsRoutes); +app.use('/api/schedules', authMiddleware, schedulesRoutes); +app.use('/api/reports', authMiddleware, operationalReportsRoutes); module.exports = app; diff --git a/backend/hotel_hacienda/src/controllers/operational-reports.controller.js b/backend/hotel_hacienda/src/controllers/operational-reports.controller.js new file mode 100644 index 0000000..475f5ff --- /dev/null +++ b/backend/hotel_hacienda/src/controllers/operational-reports.controller.js @@ -0,0 +1,125 @@ +const pool = require('../db/connection'); + +const getOccupancyReport = async (req, res) => { + try { + const { period = 'month' } = req.query; + let interval; + switch (period) { + case 'week': interval = '7 days'; break; + case 'month': interval = '30 days'; break; + case 'quarter': interval = '90 days'; break; + case 'year': interval = '365 days'; break; + default: interval = '30 days'; + } + + const totalRooms = await pool.query('SELECT COUNT(*) as count FROM rooms'); + const total = parseInt(totalRooms.rows[0].count) || 1; + + const result = await pool.query(` + SELECT DATE(rsl.changed_at) as day, + COUNT(CASE WHEN rsl.new_status = 'occupied' THEN 1 END) as occupied_count + FROM room_status_log rsl + WHERE rsl.changed_at >= CURRENT_DATE - INTERVAL '${interval}' + GROUP BY DATE(rsl.changed_at) + ORDER BY day + `); + + const occupancyData = result.rows.map(row => ({ + day: row.day, + occupancy: Math.round((parseInt(row.occupied_count) / total) * 100) + })); + + // Current occupancy + const currentOccupied = await pool.query("SELECT COUNT(*) as count FROM rooms WHERE status = 'occupied'"); + const currentRate = Math.round((parseInt(currentOccupied.rows[0].count) / total) * 100); + + res.json({ occupancyData, currentRate, totalRooms: total }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener reporte de ocupacion' }); + } +}; + +const getRevenueReport = async (req, res) => { + try { + const { period = 'month' } = req.query; + let interval; + switch (period) { + case 'week': interval = '7 days'; break; + case 'month': interval = '30 days'; break; + case 'quarter': interval = '90 days'; break; + case 'year': interval = '365 days'; break; + default: interval = '30 days'; + } + + const result = await pool.query(` + SELECT rm.bed_type as room_type, COALESCE(SUM(r.total_amount), 0) as revenue + FROM reservations r + JOIN rooms rm ON rm.id_room = r.room_id + WHERE r.status = 'checked_out' AND r.check_out >= CURRENT_DATE - INTERVAL '${interval}' + GROUP BY rm.bed_type + ORDER BY revenue DESC + `); + + const totalRevenue = result.rows.reduce((sum, r) => sum + parseFloat(r.revenue), 0); + res.json({ revenueByType: result.rows, totalRevenue }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener reporte de ingresos' }); + } +}; + +const getBookingSourcesReport = async (req, res) => { + try { + const { period = 'month' } = req.query; + let interval; + switch (period) { + case 'week': interval = '7 days'; break; + case 'month': interval = '30 days'; break; + case 'quarter': interval = '90 days'; break; + case 'year': interval = '365 days'; break; + default: interval = '30 days'; + } + + const result = await pool.query(` + SELECT channel, COUNT(*) as count, COALESCE(SUM(total_amount), 0) as revenue + FROM reservations + WHERE created_at >= CURRENT_DATE - INTERVAL '${interval}' + GROUP BY channel + ORDER BY count DESC + `); + + const total = result.rows.reduce((sum, r) => sum + parseInt(r.count), 0); + const sources = result.rows.map(r => ({ + channel: r.channel, + count: parseInt(r.count), + revenue: parseFloat(r.revenue), + percentage: total > 0 ? Math.round((parseInt(r.count) / total) * 100) : 0 + })); + + res.json({ sources, totalBookings: total }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener fuentes de reservacion' }); + } +}; + +const getSatisfactionReport = async (req, res) => { + try { + const result = await pool.query(` + SELECT AVG(rating) as avg_rating, COUNT(rating) as total_ratings, + COUNT(CASE WHEN rating = 5 THEN 1 END) as five_star, + COUNT(CASE WHEN rating = 4 THEN 1 END) as four_star, + COUNT(CASE WHEN rating = 3 THEN 1 END) as three_star, + COUNT(CASE WHEN rating <= 2 THEN 1 END) as low_star + FROM guest_stays + WHERE rating IS NOT NULL + `); + res.json({ satisfaction: result.rows[0] }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener satisfaccion' }); + } +}; + +module.exports = { getOccupancyReport, getRevenueReport, getBookingSourcesReport, getSatisfactionReport }; diff --git a/backend/hotel_hacienda/src/controllers/schedules.controller.js b/backend/hotel_hacienda/src/controllers/schedules.controller.js new file mode 100644 index 0000000..344e272 --- /dev/null +++ b/backend/hotel_hacienda/src/controllers/schedules.controller.js @@ -0,0 +1,79 @@ +const pool = require('../db/connection'); + +const getSchedules = async (req, res) => { + try { + const { week_start, department } = req.query; + if (!week_start) return res.status(400).json({ message: 'week_start es requerido' }); + + const weekEnd = new Date(week_start); + weekEnd.setDate(weekEnd.getDate() + 6); + + let query = ` + SELECT es.*, e.first_name, e.last_name + FROM employee_schedules es + JOIN employees e ON e.id_employee = es.employee_id + WHERE es.schedule_date BETWEEN $1 AND $2 + `; + const params = [week_start, weekEnd.toISOString().split('T')[0]]; + + query += ' ORDER BY e.first_name, es.schedule_date'; + const result = await pool.query(query, params); + res.json({ schedules: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener horarios' }); + } +}; + +const saveSchedules = async (req, res) => { + try { + const { entries } = req.body; // Array of { employee_id, schedule_date, shift_type } + + for (const entry of entries) { + const { employee_id, schedule_date, shift_type } = entry; + const startTime = shift_type === 'morning' ? '07:00' : shift_type === 'afternoon' ? '15:00' : shift_type === 'night' ? '23:00' : null; + const endTime = shift_type === 'morning' ? '15:00' : shift_type === 'afternoon' ? '23:00' : shift_type === 'night' ? '07:00' : null; + + await pool.query( + `INSERT INTO employee_schedules (employee_id, schedule_date, shift_type, start_time, end_time) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (employee_id, schedule_date) DO UPDATE SET shift_type = $3, start_time = $4, end_time = $5`, + [employee_id, schedule_date, shift_type, startTime, endTime] + ); + } + + res.json({ message: 'Horarios guardados correctamente' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al guardar horarios' }); + } +}; + +const getEmployeeSchedule = async (req, res) => { + try { + const { id } = req.params; + const { from_date, to_date } = req.query; + const result = await pool.query( + 'SELECT * FROM employee_schedules WHERE employee_id = $1 AND schedule_date BETWEEN $2 AND $3 ORDER BY schedule_date', + [id, from_date, to_date] + ); + res.json({ schedules: result.rows }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Error al obtener horario del empleado' }); + } +}; + +const getEmployeesForScheduling = async (req, res) => { + try { + const result = await pool.query( + 'SELECT id_employee, first_name, last_name FROM employees WHERE status_employee = true ORDER BY first_name LIMIT 100' + ); + res.json({ employees: result.rows }); + } catch (error) { + console.error(error); + res.json({ employees: [] }); + } +}; + +module.exports = { getSchedules, saveSchedules, getEmployeeSchedule, getEmployeesForScheduling }; diff --git a/backend/hotel_hacienda/src/routes/operational-reports.routes.js b/backend/hotel_hacienda/src/routes/operational-reports.routes.js new file mode 100644 index 0000000..ece3701 --- /dev/null +++ b/backend/hotel_hacienda/src/routes/operational-reports.routes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('../controllers/operational-reports.controller'); + +router.get('/occupancy', ctrl.getOccupancyReport); +router.get('/revenue', ctrl.getRevenueReport); +router.get('/booking-sources', ctrl.getBookingSourcesReport); +router.get('/satisfaction', ctrl.getSatisfactionReport); + +module.exports = router; diff --git a/backend/hotel_hacienda/src/routes/schedules.routes.js b/backend/hotel_hacienda/src/routes/schedules.routes.js new file mode 100644 index 0000000..807a05e --- /dev/null +++ b/backend/hotel_hacienda/src/routes/schedules.routes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('../controllers/schedules.controller'); + +router.get('/', ctrl.getSchedules); +router.get('/employees', ctrl.getEmployeesForScheduling); +router.get('/employee/:id', ctrl.getEmployeeSchedule); +router.post('/', ctrl.saveSchedules); + +module.exports = router; diff --git a/frontend/Frontend-Hotel/src/App.jsx b/frontend/Frontend-Hotel/src/App.jsx index 7734b70..04ec40f 100644 --- a/frontend/Frontend-Hotel/src/App.jsx +++ b/frontend/Frontend-Hotel/src/App.jsx @@ -49,6 +49,8 @@ import RoomServiceOrders from "./pages/RoomService/RoomServiceOrders.jsx"; import MenuManager from "./pages/RoomService/MenuManager.jsx"; import NewOrder from "./pages/RoomService/NewOrder.jsx"; import EventsVenues from "./pages/Events/EventsVenues.jsx"; +import Schedules from "./pages/Schedules/Schedules.jsx"; +import OperationalReports from "./pages/Reports/OperationalReports.jsx"; import "./styles/global.css"; //SubmenĂș de Hotel @@ -151,6 +153,8 @@ export default function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/Frontend-Hotel/src/pages/Reports/OperationalReports.jsx b/frontend/Frontend-Hotel/src/pages/Reports/OperationalReports.jsx new file mode 100644 index 0000000..672170d --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/Reports/OperationalReports.jsx @@ -0,0 +1,312 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { + getOccupancyReport, + getRevenueReport, + getBookingSourcesReport, + getSatisfactionReport, +} from "../../services/operationalReportService"; + +const PERIODS = ["week", "month", "quarter", "year"]; + +export default function OperationalReports() { + const { t } = useTranslation(); + const [period, setPeriod] = useState("month"); + const [occupancy, setOccupancy] = useState({ currentRate: 0, totalRooms: 0 }); + const [revenue, setRevenue] = useState({ revenueByType: [], totalRevenue: 0 }); + const [sources, setSources] = useState({ sources: [], totalBookings: 0 }); + const [satisfaction, setSatisfaction] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const [occRes, revRes, srcRes, satRes] = await Promise.all([ + getOccupancyReport({ period }), + getRevenueReport({ period }), + getBookingSourcesReport({ period }), + getSatisfactionReport(), + ]); + setOccupancy(occRes.data || { currentRate: 0, totalRooms: 0 }); + setRevenue(revRes.data || { revenueByType: [], totalRevenue: 0 }); + setSources(srcRes.data || { sources: [], totalBookings: 0 }); + setSatisfaction(satRes.data?.satisfaction || null); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }, [period]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const formatCurrency = (val) => { + const num = parseFloat(val) || 0; + return "$" + num.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 }); + }; + + const avgRating = satisfaction?.avg_rating ? parseFloat(satisfaction.avg_rating).toFixed(1) : "N/A"; + const totalRatings = satisfaction?.total_ratings ? parseInt(satisfaction.total_ratings) : 0; + + const renderStars = (rating) => { + const numRating = parseFloat(rating) || 0; + const full = Math.floor(numRating); + const hasHalf = numRating - full >= 0.5; + const stars = []; + for (let i = 0; i < 5; i++) { + if (i < full) { + stars.push( + + ★ + + ); + } else if (i === full && hasHalf) { + stars.push( + + ★ + + ); + } else { + stars.push( + + ☆ + + ); + } + } + return stars; + }; + + const maxRevenue = Math.max( + ...revenue.revenueByType.map((r) => parseFloat(r.revenue) || 0), + 1 + ); + + if (loading) { + return ( +
+

{t("common.loading")}

+
+ ); + } + + return ( +
+
+

{t("reports.title")}

+
+ + {/* Period Selector */} +
+ {PERIODS.map((p) => ( + + ))} +
+ + {/* KPI Row */} +
+ {/* Occupancy Rate */} +
+ {t("reports.occupancyRate")} + {occupancy.currentRate}% + + {occupancy.totalRooms} {t("common.room")}s + +
+ + {/* Total Revenue */} +
+ {t("reports.revenue")} + {formatCurrency(revenue.totalRevenue)} + + {t(`reports.period.${period}`)} + +
+ + {/* Guest Satisfaction */} +
+ {t("reports.guestSatisfaction")} +
+ {avgRating} +
{renderStars(avgRating)}
+
+ + {totalRatings} ratings + +
+ + {/* Total Bookings */} +
+ {t("reports.bookingSources")} + {sources.totalBookings} + + {t(`reports.period.${period}`)} + +
+
+ + {/* Charts Section */} +
+ {/* Revenue by Room Type */} +
+

+ {t("reports.revenueByRoomType")} +

+ {revenue.revenueByType.length === 0 ? ( +

+ {t("common.noResults")} +

+ ) : ( +
+ {revenue.revenueByType.map((item, idx) => { + const pct = Math.round((parseFloat(item.revenue) / maxRevenue) * 100); + return ( +
+
+ + {item.room_type || "Unknown"} + + + {formatCurrency(item.revenue)} + +
+
+
+
+
+ ); + })} +
+ )} +
+ + {/* Booking Sources */} +
+

+ {t("reports.bookingSourceDistribution")} +

+ {sources.sources.length === 0 ? ( +

+ {t("common.noResults")} +

+ ) : ( +
+ {sources.sources.map((src, idx) => ( +
+
+ + {src.channel || "Unknown"} + +
+ + {src.count} + + + {src.percentage}% + +
+
+
+
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/Frontend-Hotel/src/pages/Schedules/Schedules.jsx b/frontend/Frontend-Hotel/src/pages/Schedules/Schedules.jsx new file mode 100644 index 0000000..009811c --- /dev/null +++ b/frontend/Frontend-Hotel/src/pages/Schedules/Schedules.jsx @@ -0,0 +1,331 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { + getSchedules, + saveSchedules, + getEmployeesForScheduling, +} from "../../services/scheduleService"; + +const SHIFT_CYCLE = ["morning", "afternoon", "night", "off"]; + +function getMonday(d) { + const date = new Date(d); + const day = date.getDay(); + const diff = date.getDate() - day + (day === 0 ? -6 : 1); + date.setDate(diff); + date.setHours(0, 0, 0, 0); + return date; +} + +function formatDate(date) { + return date.toISOString().split("T")[0]; +} + +function addDays(date, days) { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +} + +const DAY_LABELS_EN = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; +const DAY_LABELS_ES = ["Lun", "Mar", "Mie", "Jue", "Vie", "Sab", "Dom"]; + +export default function Schedules() { + const { t, i18n } = useTranslation(); + const [weekStart, setWeekStart] = useState(() => getMonday(new Date())); + const [employees, setEmployees] = useState([]); + const [grid, setGrid] = useState({}); + const [changes, setChanges] = useState({}); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const weekEnd = addDays(weekStart, 6); + const dayLabels = i18n.language === "es" ? DAY_LABELS_ES : DAY_LABELS_EN; + + const weekDates = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const [empRes, schedRes] = await Promise.all([ + getEmployeesForScheduling(), + getSchedules({ week_start: formatDate(weekStart) }), + ]); + const emps = empRes.data.employees || []; + setEmployees(emps); + + const schedules = schedRes.data.schedules || []; + const newGrid = {}; + for (const emp of emps) { + newGrid[emp.id_employee] = {}; + for (const wd of weekDates) { + newGrid[emp.id_employee][formatDate(wd)] = "off"; + } + } + for (const s of schedules) { + const dateKey = s.schedule_date?.split("T")[0] || s.schedule_date; + if (newGrid[s.employee_id]) { + newGrid[s.employee_id][dateKey] = s.shift_type || "off"; + } + } + setGrid(newGrid); + setChanges({}); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }, [weekStart]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleCellClick = (employeeId, dateStr) => { + const current = grid[employeeId]?.[dateStr] || "off"; + const idx = SHIFT_CYCLE.indexOf(current); + const next = SHIFT_CYCLE[(idx + 1) % SHIFT_CYCLE.length]; + + setGrid((prev) => ({ + ...prev, + [employeeId]: { + ...prev[employeeId], + [dateStr]: next, + }, + })); + + const key = `${employeeId}_${dateStr}`; + setChanges((prev) => ({ + ...prev, + [key]: { employee_id: employeeId, schedule_date: dateStr, shift_type: next }, + })); + }; + + const handleSave = async () => { + const entries = Object.values(changes); + if (entries.length === 0) return; + + setSaving(true); + try { + await saveSchedules(entries); + setChanges({}); + fetchData(); + } catch (error) { + console.error(error); + } finally { + setSaving(false); + } + }; + + const prevWeek = () => setWeekStart((prev) => addDays(prev, -7)); + const nextWeek = () => setWeekStart((prev) => addDays(prev, 7)); + + const getShiftDisplay = (shift) => { + switch (shift) { + case "morning": + return "M 7-15"; + case "afternoon": + return "T 15-23"; + case "night": + return "N 23-7"; + default: + return "---"; + } + }; + + const getShiftClass = (shift) => { + switch (shift) { + case "morning": + return "shift-morning"; + case "afternoon": + return "shift-afternoon"; + case "night": + return "shift-night"; + default: + return "shift-off"; + } + }; + + const hasChanges = Object.keys(changes).length > 0; + + if (loading) { + return ( +
+

{t("common.loading")}

+
+ ); + } + + return ( +
+
+

{t("schedules.title")}

+
+ {hasChanges && ( + + {Object.keys(changes).length} {i18n.language === "es" ? "cambios pendientes" : "pending changes"} + + )} + +
+
+ + {/* Week Navigation */} +
+ + + {formatDate(weekStart)} — {formatDate(weekEnd)} + + +
+ + {/* Schedule Grid */} +
+ + + + + {weekDates.map((wd, i) => ( + + ))} + + + + {employees.length === 0 ? ( + + + + ) : ( + employees.map((emp) => ( + + + {weekDates.map((wd) => { + const dateStr = formatDate(wd); + const shift = grid[emp.id_employee]?.[dateStr] || "off"; + const changeKey = `${emp.id_employee}_${dateStr}`; + const isChanged = changes[changeKey] !== undefined; + + return ( + + ); + })} + + )) + )} + +
{t("schedules.employee")} +
{dayLabels[i]}
+
+ {formatDate(wd).slice(5)} +
+
+ {t("common.noResults")} +
+ {emp.first_name} {emp.last_name} + handleCellClick(emp.id_employee, dateStr)} + > + + {getShiftDisplay(shift)} + + {isChanged && ( + + )} +
+
+ + {/* Legend */} +
+ {SHIFT_CYCLE.map((shift) => ( +
+ + {getShiftDisplay(shift)} + + + {t(`schedules.shifts.${shift}`)} + +
+ ))} +
+
+ ); +} diff --git a/frontend/Frontend-Hotel/src/services/operationalReportService.js b/frontend/Frontend-Hotel/src/services/operationalReportService.js new file mode 100644 index 0000000..56309c5 --- /dev/null +++ b/frontend/Frontend-Hotel/src/services/operationalReportService.js @@ -0,0 +1,5 @@ +import api from "./api"; +export const getOccupancyReport = (params) => api.get("/reports/occupancy", { params }); +export const getRevenueReport = (params) => api.get("/reports/revenue", { params }); +export const getBookingSourcesReport = (params) => api.get("/reports/booking-sources", { params }); +export const getSatisfactionReport = () => api.get("/reports/satisfaction"); diff --git a/frontend/Frontend-Hotel/src/services/scheduleService.js b/frontend/Frontend-Hotel/src/services/scheduleService.js new file mode 100644 index 0000000..c3bebee --- /dev/null +++ b/frontend/Frontend-Hotel/src/services/scheduleService.js @@ -0,0 +1,4 @@ +import api from "./api"; +export const getSchedules = (params) => api.get("/schedules", { params }); +export const saveSchedules = (entries) => api.post("/schedules", { entries }); +export const getEmployeesForScheduling = () => api.get("/schedules/employees");