mirror of
http://88.130.71.182:3000/BlitTech/badoHair_be.git
synced 2026-06-13 08:49:46 +00:00
176 lines
6.0 KiB
Python
176 lines
6.0 KiB
Python
from datetime import date, timedelta, datetime, time
|
|
|
|
import asyncpg
|
|
import pytz
|
|
|
|
from app.config import get_settings
|
|
from app.exceptions import NotFoundError, AppError
|
|
from app.models.bookings import WeeklyScheduleCreate, SlotCreate
|
|
|
|
|
|
def _fmt_time(t) -> str:
|
|
if t is None:
|
|
return ""
|
|
s = str(t)
|
|
return s[:5]
|
|
|
|
|
|
async def get_schedule(db: asyncpg.Connection) -> list[dict]:
|
|
rows = await db.fetch("SELECT * FROM weekly_schedule WHERE is_active = true ORDER BY day_of_week, start_time")
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
async def create_schedule_entry(db: asyncpg.Connection, data: WeeklyScheduleCreate) -> dict:
|
|
row = await db.fetchrow(
|
|
"""
|
|
INSERT INTO weekly_schedule (day_of_week, start_time, end_time, slot_duration_minutes)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *
|
|
""",
|
|
data.day_of_week, data.start_time, data.end_time, data.slot_duration_minutes,
|
|
)
|
|
return dict(row)
|
|
|
|
|
|
async def delete_schedule_entry(db: asyncpg.Connection, schedule_id: str):
|
|
result = await db.execute("DELETE FROM weekly_schedule WHERE id = $1", schedule_id)
|
|
if result == "DELETE 0":
|
|
raise NotFoundError("schedule entry")
|
|
|
|
|
|
async def generate_slots(db: asyncpg.Connection, from_date: date, to_date: date) -> int:
|
|
if (to_date - from_date).days > 90:
|
|
raise AppError("RANGE_TOO_LARGE", "Date range cannot exceed 90 days", 400)
|
|
|
|
schedule = await db.fetch("SELECT * FROM weekly_schedule WHERE is_active = true")
|
|
if not schedule:
|
|
raise AppError("NO_SCHEDULE", "No active weekly schedule configured", 400)
|
|
|
|
blocked = await db.fetch(
|
|
"SELECT date FROM blocked_dates WHERE date BETWEEN $1 AND $2",
|
|
from_date, to_date,
|
|
)
|
|
blocked_set = {r["date"] for r in blocked}
|
|
|
|
tz = pytz.timezone(get_settings().BUSINESS_TIMEZONE)
|
|
created = 0
|
|
current = from_date
|
|
|
|
async with db.transaction():
|
|
while current <= to_date:
|
|
if current in blocked_set:
|
|
current += timedelta(days=1)
|
|
continue
|
|
|
|
day_schedules = [s for s in schedule if s["day_of_week"] == current.weekday()]
|
|
|
|
for sched in day_schedules:
|
|
cursor = datetime.combine(current, sched["start_time"])
|
|
end_limit = datetime.combine(current, sched["end_time"])
|
|
duration = timedelta(minutes=sched["slot_duration_minutes"])
|
|
|
|
while cursor + duration <= end_limit:
|
|
slot_end = cursor + duration
|
|
exists = await db.fetchval(
|
|
"SELECT 1 FROM time_slots WHERE date = $1 AND start_time = $2",
|
|
current, cursor.time(),
|
|
)
|
|
if not exists:
|
|
await db.execute(
|
|
"INSERT INTO time_slots (date, start_time, end_time) VALUES ($1, $2, $3)",
|
|
current, cursor.time(), slot_end.time(),
|
|
)
|
|
created += 1
|
|
cursor += duration
|
|
|
|
current += timedelta(days=1)
|
|
|
|
return created
|
|
|
|
|
|
async def create_slot(db: asyncpg.Connection, data: SlotCreate) -> dict:
|
|
if data.start_time >= data.end_time:
|
|
raise AppError("INVALID_SLOT", "start_time must be before end_time", 400)
|
|
row = await db.fetchrow(
|
|
"INSERT INTO time_slots (date, start_time, end_time) VALUES ($1, $2, $3) RETURNING *",
|
|
data.date, data.start_time, data.end_time,
|
|
)
|
|
return dict(row)
|
|
|
|
|
|
async def delete_slot(db: asyncpg.Connection, slot_id: str):
|
|
booked = await db.fetchval(
|
|
"SELECT 1 FROM bookings WHERE slot_id = $1 AND status IN ('pending', 'confirmed')",
|
|
slot_id,
|
|
)
|
|
if booked:
|
|
raise AppError("SLOT_BOOKED", "Cannot delete a slot with active bookings", 409)
|
|
result = await db.execute("DELETE FROM time_slots WHERE id = $1", slot_id)
|
|
if result == "DELETE 0":
|
|
raise NotFoundError("slot")
|
|
|
|
|
|
async def update_slot(db: asyncpg.Connection, slot_id: str, is_blocked: bool, block_reason: str | None) -> dict:
|
|
row = await db.fetchrow(
|
|
"UPDATE time_slots SET is_blocked = $2, block_reason = $3 WHERE id = $1 RETURNING *",
|
|
slot_id, is_blocked, block_reason,
|
|
)
|
|
if not row:
|
|
raise NotFoundError("slot")
|
|
return dict(row)
|
|
|
|
|
|
async def list_slots(
|
|
db: asyncpg.Connection,
|
|
from_date: date,
|
|
to_date: date,
|
|
available_only: bool = False,
|
|
) -> list[dict]:
|
|
query = """
|
|
SELECT
|
|
ts.id, ts.date, ts.start_time, ts.end_time,
|
|
ts.is_blocked, ts.block_reason,
|
|
EXISTS(
|
|
SELECT 1 FROM bookings b
|
|
WHERE b.slot_id = ts.id AND b.status IN ('pending', 'confirmed')
|
|
) AS is_booked
|
|
FROM time_slots ts
|
|
WHERE ts.date BETWEEN $1 AND $2
|
|
"""
|
|
params: list = [from_date, to_date]
|
|
|
|
if available_only:
|
|
query += " AND ts.is_blocked = false"
|
|
query += """
|
|
AND NOT EXISTS(
|
|
SELECT 1 FROM blocked_dates bd WHERE bd.date = ts.date
|
|
)
|
|
AND NOT EXISTS(
|
|
SELECT 1 FROM bookings b WHERE b.slot_id = ts.id AND b.status IN ('pending', 'confirmed')
|
|
)
|
|
AND ts.date >= CURRENT_DATE
|
|
"""
|
|
|
|
query += " ORDER BY ts.date, ts.start_time"
|
|
rows = await db.fetch(query, *params)
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
async def get_blocked_dates(db: asyncpg.Connection) -> list[dict]:
|
|
rows = await db.fetch("SELECT * FROM blocked_dates ORDER BY date")
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
async def add_blocked_date(db: asyncpg.Connection, date: date, reason: str | None) -> dict:
|
|
row = await db.fetchrow(
|
|
"INSERT INTO blocked_dates (date, reason) VALUES ($1, $2) ON CONFLICT (date) DO UPDATE SET reason = $2 RETURNING *",
|
|
date, reason,
|
|
)
|
|
return dict(row)
|
|
|
|
|
|
async def remove_blocked_date(db: asyncpg.Connection, blocked_date_id: str):
|
|
result = await db.execute("DELETE FROM blocked_dates WHERE id = $1", blocked_date_id)
|
|
if result == "DELETE 0":
|
|
raise NotFoundError("blocked date")
|