codex-django-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_django_cli/__init__.py +12 -0
- codex_django_cli/blueprints/apps/default/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/admin/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/apps.py.j2 +0 -0
- codex_django_cli/blueprints/apps/default/forms/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/models/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/modules/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/selector/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/services/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/tests/__init__.py +0 -0
- codex_django_cli/blueprints/apps/default/translations.py.j2 +0 -0
- codex_django_cli/blueprints/apps/default/urls.py.j2 +0 -0
- codex_django_cli/blueprints/apps/default/views/__init__.py +0 -0
- codex_django_cli/blueprints/deploy/shared/.dockerignore.j2 +24 -0
- codex_django_cli/blueprints/deploy/shared/.env.example.j2 +64 -0
- codex_django_cli/blueprints/deploy/shared/backend/Dockerfile.j2 +36 -0
- codex_django_cli/blueprints/deploy/shared/backend/entrypoint.sh.j2 +20 -0
- codex_django_cli/blueprints/deploy/shared/nginx/Dockerfile.local.j2 +14 -0
- codex_django_cli/blueprints/deploy/shared/nginx/nginx-main.conf.j2 +67 -0
- codex_django_cli/blueprints/deploy/shared/nginx/site-local.conf.j2 +66 -0
- codex_django_cli/blueprints/deploy/shared/worker/Dockerfile.j2 +25 -0
- codex_django_cli/blueprints/deploy/stack/docker/base.Dockerfile.j2 +38 -0
- codex_django_cli/blueprints/deploy/stack/docker/django.Dockerfile.j2 +27 -0
- codex_django_cli/blueprints/deploy/stack/docker/entrypoint.sh.j2 +19 -0
- codex_django_cli/blueprints/deploy/stack/docker-compose.apps.yml.j2 +41 -0
- codex_django_cli/blueprints/deploy/stack/docker-compose.infra.yml.j2 +80 -0
- codex_django_cli/blueprints/deploy/stack/docker-compose.local.apps.yml.j2 +41 -0
- codex_django_cli/blueprints/deploy/stack/docker-compose.local.infra.yml.j2 +94 -0
- codex_django_cli/blueprints/deploy/stack/docker-compose.test.yml.j2 +63 -0
- codex_django_cli/blueprints/deploy/stack/nginx/Dockerfile.j2 +12 -0
- codex_django_cli/blueprints/deploy/stack/nginx/conf.d/default.conf.j2 +68 -0
- codex_django_cli/blueprints/deploy/stack/nginx/site.conf.template.j2 +81 -0
- codex_django_cli/blueprints/deploy/stack_workflows/ci-develop.yml.j2 +42 -0
- codex_django_cli/blueprints/deploy/stack_workflows/ci-main.yml.j2 +165 -0
- codex_django_cli/blueprints/deploy/stack_workflows/deploy-cluster.yml.j2 +112 -0
- codex_django_cli/blueprints/deploy/standalone/docker-compose.prod.yml.j2 +118 -0
- codex_django_cli/blueprints/deploy/standalone/docker-compose.test.yml.j2 +86 -0
- codex_django_cli/blueprints/deploy/standalone/docker-compose.yml.j2 +151 -0
- codex_django_cli/blueprints/deploy/standalone/nginx/Dockerfile.j2 +18 -0
- codex_django_cli/blueprints/deploy/standalone/nginx/site.conf.template.j2 +168 -0
- codex_django_cli/blueprints/deploy/standalone_workflows/ci-develop.yml.j2 +42 -0
- codex_django_cli/blueprints/deploy/standalone_workflows/ci-main.yml.j2 +113 -0
- codex_django_cli/blueprints/deploy/standalone_workflows/deploy-production-tag.yml.j2 +211 -0
- codex_django_cli/blueprints/features/booking/booking/__init__.py.j2 +0 -0
- codex_django_cli/blueprints/features/booking/booking/admin.py.j2 +52 -0
- codex_django_cli/blueprints/features/booking/booking/apps.py.j2 +8 -0
- codex_django_cli/blueprints/features/booking/booking/models.py.j2 +142 -0
- codex_django_cli/blueprints/features/booking/booking/selectors.py.j2 +102 -0
- codex_django_cli/blueprints/features/booking/booking/urls.py.j2 +12 -0
- codex_django_cli/blueprints/features/booking/booking/views.py.j2 +174 -0
- codex_django_cli/blueprints/features/booking/booking/wiki.md.j2 +142 -0
- codex_django_cli/blueprints/features/booking/cabinet/templates/cabinet/booking/my_bookings.html +249 -0
- codex_django_cli/blueprints/features/booking/cabinet/views/booking.py.j2 +70 -0
- codex_django_cli/blueprints/features/booking/system/admin/booking_settings.py.j2 +31 -0
- codex_django_cli/blueprints/features/booking/system/models/booking_settings.py.j2 +7 -0
- codex_django_cli/blueprints/features/booking/templates/booking/booking_page.html +137 -0
- codex_django_cli/blueprints/features/booking/templates/booking/partials/step_confirm.html +143 -0
- codex_django_cli/blueprints/features/booking/templates/booking/partials/step_date.html +184 -0
- codex_django_cli/blueprints/features/booking/templates/booking/partials/step_service.html +78 -0
- codex_django_cli/blueprints/features/booking/templates/booking/partials/step_time.html +89 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/adapters.py.j2 +21 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/templates/cabinet/client/my_appointments.html +58 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/templates/cabinet/client/profile.html +107 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/templates/cabinet/client/settings.html +106 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/templates/cabinet/client/settings_notifications.html +106 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/templates/cabinet/client/settings_privacy.html +135 -0
- codex_django_cli/blueprints/features/client_cabinet/cabinet/views/client.py.j2 +49 -0
- codex_django_cli/blueprints/features/client_cabinet/system/models/user_profile.py.j2 +22 -0
- codex_django_cli/blueprints/features/notifications/arq/client.j2 +22 -0
- codex_django_cli/blueprints/features/notifications/feature/models/email_content.j2 +15 -0
- codex_django_cli/blueprints/features/notifications/feature/selectors/email_content.j2 +23 -0
- codex_django_cli/blueprints/features/notifications/feature/services/notification.j2 +66 -0
- codex_django_cli/blueprints/project/cabinet/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/admin/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/apps.py.j2 +11 -0
- codex_django_cli/blueprints/project/cabinet/cabinet.py.j2 +33 -0
- codex_django_cli/blueprints/project/cabinet/forms/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/mock.py.j2 +110 -0
- codex_django_cli/blueprints/project/cabinet/models/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/modules/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/selector/__init__.py +3 -0
- codex_django_cli/blueprints/project/cabinet/selector/users.py.j2 +25 -0
- codex_django_cli/blueprints/project/cabinet/services/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/static/cabinet/css/base.css +11 -0
- codex_django_cli/blueprints/project/cabinet/static/cabinet/css/compiler_config.json +5 -0
- codex_django_cli/blueprints/project/cabinet/static/cabinet/css/theme/tokens.css +30 -0
- codex_django_cli/blueprints/project/cabinet/static/cabinet/js/app/cabinet.js +37 -0
- codex_django_cli/blueprints/project/cabinet/static/cabinet/js/compiler_config.json +7 -0
- codex_django_cli/blueprints/project/cabinet/templates/cabinet/users/detail.html +91 -0
- codex_django_cli/blueprints/project/cabinet/templates/cabinet/users/index.html +97 -0
- codex_django_cli/blueprints/project/cabinet/tests/__init__.py +0 -0
- codex_django_cli/blueprints/project/cabinet/translations.py.j2 +2 -0
- codex_django_cli/blueprints/project/cabinet/urls.py.j2 +22 -0
- codex_django_cli/blueprints/project/cabinet/views/__init__.py +3 -0
- codex_django_cli/blueprints/project/cabinet/views/users.py.j2 +17 -0
- codex_django_cli/blueprints/project/core/__init__.py.j2 +1 -0
- codex_django_cli/blueprints/project/core/apps.py.j2 +15 -0
- codex_django_cli/blueprints/project/core/asgi.py.j2 +7 -0
- codex_django_cli/blueprints/project/core/logger.py.j2 +57 -0
- codex_django_cli/blueprints/project/core/redis.py.j2 +4 -0
- codex_django_cli/blueprints/project/core/settings/__init__.py.j2 +4 -0
- codex_django_cli/blueprints/project/core/settings/base.py.j2 +67 -0
- codex_django_cli/blueprints/project/core/settings/dev.py.j2 +56 -0
- codex_django_cli/blueprints/project/core/settings/modules/__init__.py.j2 +1 -0
- codex_django_cli/blueprints/project/core/settings/modules/admin.py.j2 +72 -0
- codex_django_cli/blueprints/project/core/settings/modules/apps.py.j2 +64 -0
- codex_django_cli/blueprints/project/core/settings/modules/cache.py.j2 +49 -0
- codex_django_cli/blueprints/project/core/settings/modules/codex.py.j2 +39 -0
- codex_django_cli/blueprints/project/core/settings/modules/database.py.j2 +49 -0
- codex_django_cli/blueprints/project/core/settings/modules/internationalization.py.j2 +43 -0
- codex_django_cli/blueprints/project/core/settings/modules/logging.py.j2 +45 -0
- codex_django_cli/blueprints/project/core/settings/modules/middleware.py.j2 +17 -0
- codex_django_cli/blueprints/project/core/settings/modules/security.py.j2 +53 -0
- codex_django_cli/blueprints/project/core/settings/modules/sitemap.py.j2 +14 -0
- codex_django_cli/blueprints/project/core/settings/modules/static.py.j2 +36 -0
- codex_django_cli/blueprints/project/core/settings/modules/templates.py.j2 +29 -0
- codex_django_cli/blueprints/project/core/settings/prod.py.j2 +77 -0
- codex_django_cli/blueprints/project/core/settings/test.py.j2 +40 -0
- codex_django_cli/blueprints/project/core/sitemaps.py.j2 +26 -0
- codex_django_cli/blueprints/project/core/urls.py.j2 +71 -0
- codex_django_cli/blueprints/project/core/wsgi.py.j2 +7 -0
- codex_django_cli/blueprints/project/features/__init__.py.j2 +0 -0
- codex_django_cli/blueprints/project/features/main/admin/__init__.py +0 -0
- codex_django_cli/blueprints/project/features/main/apps.py.j2 +8 -0
- codex_django_cli/blueprints/project/features/main/forms/__init__.py +0 -0
- codex_django_cli/blueprints/project/features/main/models/__init__.py +0 -0
- codex_django_cli/blueprints/project/features/main/sitemaps.py.j2 +23 -0
- codex_django_cli/blueprints/project/features/main/tests/__init__.py +0 -0
- codex_django_cli/blueprints/project/features/main/translations.py.j2 +2 -0
- codex_django_cli/blueprints/project/features/main/urls.py.j2 +8 -0
- codex_django_cli/blueprints/project/features/main/views/__init__.py.j2 +9 -0
- codex_django_cli/blueprints/project/manage.py.j2 +39 -0
- codex_django_cli/blueprints/project/static/css/base/components.css +88 -0
- codex_django_cli/blueprints/project/static/css/base/footer.css +43 -0
- codex_django_cli/blueprints/project/static/css/base/header.css +76 -0
- codex_django_cli/blueprints/project/static/css/base/layout.css +58 -0
- codex_django_cli/blueprints/project/static/css/base/reset.css +65 -0
- codex_django_cli/blueprints/project/static/css/base/tokens.css +45 -0
- codex_django_cli/blueprints/project/static/css/base.css +31 -0
- codex_django_cli/blueprints/project/static/css/compiler_config.json +10 -0
- codex_django_cli/blueprints/project/static/css/pages/contacts.css +42 -0
- codex_django_cli/blueprints/project/static/css/pages/errors.css +11 -0
- codex_django_cli/blueprints/project/static/css/pages/home.css +120 -0
- codex_django_cli/blueprints/project/static/js/app/main.js +8 -0
- codex_django_cli/blueprints/project/static/js/vendor/alpine.min.js +9 -0
- codex_django_cli/blueprints/project/static/js/vendor/htmx.min.js +9 -0
- codex_django_cli/blueprints/project/static/manifest.json +15 -0
- codex_django_cli/blueprints/project/static/robots.txt +4 -0
- codex_django_cli/blueprints/project/system/__init__.py.j2 +1 -0
- codex_django_cli/blueprints/project/system/admin/__init__.py.j2 +15 -0
- codex_django_cli/blueprints/project/system/admin/seo.py.j2 +28 -0
- codex_django_cli/blueprints/project/system/admin/settings.py.j2 +65 -0
- codex_django_cli/blueprints/project/system/admin/static.py.j2 +18 -0
- codex_django_cli/blueprints/project/system/apps.py.j2 +9 -0
- codex_django_cli/blueprints/project/system/forms/__init__.py.j2 +1 -0
- codex_django_cli/blueprints/project/system/management/__init__.py +0 -0
- codex_django_cli/blueprints/project/system/management/commands/__init__.py +0 -0
- codex_django_cli/blueprints/project/system/management/commands/dev.py.j2 +5 -0
- codex_django_cli/blueprints/project/system/management/commands/menu.py.j2 +81 -0
- codex_django_cli/blueprints/project/system/management/commands/runserver_plus.py.j2 +46 -0
- codex_django_cli/blueprints/project/system/management/commands/update_all_content.py.j2 +17 -0
- codex_django_cli/blueprints/project/system/management/commands/update_site_settings.py.j2 +80 -0
- codex_django_cli/blueprints/project/system/models/__init__.py.j2 +15 -0
- codex_django_cli/blueprints/project/system/models/seo.py.j2 +37 -0
- codex_django_cli/blueprints/project/system/models/settings.py.j2 +31 -0
- codex_django_cli/blueprints/project/system/models/static.py.j2 +13 -0
- codex_django_cli/blueprints/project/system/services/__init__.py.j2 +1 -0
- codex_django_cli/blueprints/project/system/urls.py.j2 +7 -0
- codex_django_cli/blueprints/project/system/views/__init__.py.j2 +1 -0
- codex_django_cli/blueprints/project/system/views/errors.py.j2 +13 -0
- codex_django_cli/blueprints/project/templates/base.html.j2 +53 -0
- codex_django_cli/blueprints/project/templates/errors/400.html +9 -0
- codex_django_cli/blueprints/project/templates/errors/403.html +9 -0
- codex_django_cli/blueprints/project/templates/errors/404.html +16 -0
- codex_django_cli/blueprints/project/templates/errors/500.html +16 -0
- codex_django_cli/blueprints/project/templates/includes/_analytics_body.html +2 -0
- codex_django_cli/blueprints/project/templates/includes/_analytics_head.html +2 -0
- codex_django_cli/blueprints/project/templates/includes/_cookie_consent.html +2 -0
- codex_django_cli/blueprints/project/templates/includes/_critical_css.html +38 -0
- codex_django_cli/blueprints/project/templates/includes/_footer.html +57 -0
- codex_django_cli/blueprints/project/templates/includes/_header.html.j2 +75 -0
- codex_django_cli/blueprints/project/templates/includes/_hreflang_tags.html.j2 +11 -0
- codex_django_cli/blueprints/project/templates/includes/_meta.html +44 -0
- codex_django_cli/blueprints/project/templates/main/contacts.html +38 -0
- codex_django_cli/blueprints/project/templates/main/home.html +128 -0
- codex_django_cli/blueprints/project/templates/main/home.html.j2 +129 -0
- codex_django_cli/blueprints/project/templates/sitemap.xml +16 -0
- codex_django_cli/blueprints/repo/.env.example.j2 +15 -0
- codex_django_cli/blueprints/repo/.github/workflows/.gitkeep +1 -0
- codex_django_cli/blueprints/repo/.gitignore +38 -0
- codex_django_cli/blueprints/repo/README.md.j2 +32 -0
- codex_django_cli/blueprints/repo/docs/README.md +5 -0
- codex_django_cli/blueprints/repo/pyproject.toml.j2 +46 -0
- codex_django_cli/blueprints/repo/tools/dev/check.py.j2 +23 -0
- codex_django_cli/blueprints/repo/tools/dev/generate_project_tree.py +15 -0
- codex_django_cli/commands/__init__.py +1 -0
- codex_django_cli/commands/add_app.py +56 -0
- codex_django_cli/commands/booking.py +92 -0
- codex_django_cli/commands/client_cabinet.py +66 -0
- codex_django_cli/commands/deploy.py +96 -0
- codex_django_cli/commands/init.py +216 -0
- codex_django_cli/commands/notifications.py +59 -0
- codex_django_cli/commands/quality.py +116 -0
- codex_django_cli/engine.py +113 -0
- codex_django_cli/main.py +470 -0
- codex_django_cli/prompts.py +258 -0
- codex_django_cli/py.typed +0 -0
- codex_django_cli/utils.py +25 -0
- codex_django_cli-0.2.0.dist-info/METADATA +66 -0
- codex_django_cli-0.2.0.dist-info/RECORD +212 -0
- codex_django_cli-0.2.0.dist-info/WHEEL +4 -0
- codex_django_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Booking views — public booking wizard with HTMX + Alpine.js.
|
|
3
|
+
|
|
4
|
+
Solo mode (single service):
|
|
5
|
+
POST /confirm/ with service_id=<int>
|
|
6
|
+
|
|
7
|
+
Multi-service mode:
|
|
8
|
+
POST /confirm/ with service_ids=<int>&service_ids=<int>&...
|
|
9
|
+
Requires a BookingPersistenceHook implementation — see wiki.md for details.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from datetime import date, datetime
|
|
16
|
+
|
|
17
|
+
from django.http import HttpResponse
|
|
18
|
+
from django.shortcuts import render
|
|
19
|
+
from django.views.decorators.http import require_GET, require_POST
|
|
20
|
+
|
|
21
|
+
from .models import Service, ServiceCategory
|
|
22
|
+
from .selectors import create_booking, get_available_slots, get_calendar_data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@require_GET
|
|
26
|
+
def booking_page_view(request):
|
|
27
|
+
"""Main booking page — renders the wizard shell with step 1 (services)."""
|
|
28
|
+
categories = ServiceCategory.objects.prefetch_related("services").all()
|
|
29
|
+
return render(request, "booking/booking_page.html", {
|
|
30
|
+
"categories": categories,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@require_GET
|
|
35
|
+
def get_calendar_view(request):
|
|
36
|
+
"""HTMX endpoint — returns the calendar partial for a given month."""
|
|
37
|
+
service_id = request.GET.get("service_id")
|
|
38
|
+
year = int(request.GET.get("year", date.today().year))
|
|
39
|
+
month = int(request.GET.get("month", date.today().month))
|
|
40
|
+
selected = request.GET.get("selected_date")
|
|
41
|
+
|
|
42
|
+
selected_date = None
|
|
43
|
+
if selected:
|
|
44
|
+
selected_date = datetime.strptime(selected, "%Y-%m-%d").date()
|
|
45
|
+
|
|
46
|
+
calendar_data = get_calendar_data(
|
|
47
|
+
year=year,
|
|
48
|
+
month=month,
|
|
49
|
+
today=date.today(),
|
|
50
|
+
selected_date=selected_date,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return render(request, "booking/partials/step_date.html", {
|
|
54
|
+
"calendar_data": calendar_data,
|
|
55
|
+
"year": year,
|
|
56
|
+
"month": month,
|
|
57
|
+
"service_id": service_id,
|
|
58
|
+
"selected_date": selected_date,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@require_GET
|
|
63
|
+
def get_slots_view(request):
|
|
64
|
+
"""HTMX endpoint — returns available time slots for a date + service.
|
|
65
|
+
|
|
66
|
+
For multi-service, pass the first (primary) service_id — the engine
|
|
67
|
+
will compute intersected availability during confirm_booking_view.
|
|
68
|
+
"""
|
|
69
|
+
service_id = request.GET.get("service_id")
|
|
70
|
+
target = request.GET.get("date")
|
|
71
|
+
|
|
72
|
+
if not service_id or not target:
|
|
73
|
+
return HttpResponse("Missing parameters", status=400)
|
|
74
|
+
|
|
75
|
+
target_date = datetime.strptime(target, "%Y-%m-%d").date()
|
|
76
|
+
|
|
77
|
+
result = get_available_slots(service_ids=[int(service_id)], target_date=target_date)
|
|
78
|
+
slots = result.get_unique_start_times() if result.solutions else []
|
|
79
|
+
|
|
80
|
+
return render(request, "booking/partials/step_time.html", {
|
|
81
|
+
"slots": slots,
|
|
82
|
+
"service_id": service_id,
|
|
83
|
+
"target_date": target,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@require_POST
|
|
88
|
+
def confirm_booking_view(request):
|
|
89
|
+
"""Final step — creates the booking.
|
|
90
|
+
|
|
91
|
+
Solo mode (single service_id):
|
|
92
|
+
Creates one Appointment, no persistence hook needed.
|
|
93
|
+
|
|
94
|
+
Multi-service mode (multiple service_ids):
|
|
95
|
+
Requires BookingPersistenceHook — implement it in this project
|
|
96
|
+
and pass it as persistence_hook=. See wiki.md for details.
|
|
97
|
+
|
|
98
|
+
POST parameters:
|
|
99
|
+
service_id — single service (solo mode, legacy)
|
|
100
|
+
service_ids — one or more service ids (multi-service mode preferred)
|
|
101
|
+
date — YYYY-MM-DD
|
|
102
|
+
time — time string as returned by get_available_slots()
|
|
103
|
+
master_id — optional, leave blank for "any master"
|
|
104
|
+
master_selections — optional JSON: {"<service_id>": <master_id>}
|
|
105
|
+
"""
|
|
106
|
+
target = request.POST.get("date")
|
|
107
|
+
selected_time = request.POST.get("time")
|
|
108
|
+
|
|
109
|
+
# Resolve service_ids: prefer multi-value list, fall back to single service_id
|
|
110
|
+
raw_ids = request.POST.getlist("service_ids") or [request.POST.get("service_id")]
|
|
111
|
+
service_ids = [int(s) for s in raw_ids if s]
|
|
112
|
+
|
|
113
|
+
if not service_ids or not target or not selected_time:
|
|
114
|
+
return HttpResponse("Missing parameters", status=400)
|
|
115
|
+
|
|
116
|
+
if not request.user.is_authenticated:
|
|
117
|
+
return HttpResponse("Authentication required", status=401)
|
|
118
|
+
|
|
119
|
+
target_date = datetime.strptime(target, "%Y-%m-%d").date()
|
|
120
|
+
|
|
121
|
+
# master_id (solo) or master_selections JSON (multi)
|
|
122
|
+
master_id_raw = request.POST.get("master_id")
|
|
123
|
+
master_id = int(master_id_raw) if master_id_raw else None
|
|
124
|
+
|
|
125
|
+
master_selections = None
|
|
126
|
+
master_selections_raw = request.POST.get("master_selections")
|
|
127
|
+
if master_selections_raw:
|
|
128
|
+
try:
|
|
129
|
+
master_selections = {
|
|
130
|
+
int(k): (int(v) if v else None)
|
|
131
|
+
for k, v in json.loads(master_selections_raw).items()
|
|
132
|
+
}
|
|
133
|
+
except (json.JSONDecodeError, ValueError):
|
|
134
|
+
return HttpResponse("Invalid master_selections format", status=400)
|
|
135
|
+
|
|
136
|
+
persistence_hook = None
|
|
137
|
+
if len(service_ids) > 1:
|
|
138
|
+
# TODO: Implement BookingPersistenceHook for multi-service support.
|
|
139
|
+
# Example:
|
|
140
|
+
# from .hooks import MyBookingPersistenceHook
|
|
141
|
+
# persistence_hook = MyBookingPersistenceHook()
|
|
142
|
+
# See wiki.md — "Multi-service bookings" section for full details.
|
|
143
|
+
return HttpResponse(
|
|
144
|
+
"Multi-service booking requires BookingPersistenceHook. "
|
|
145
|
+
"See booking/wiki.md for implementation guide.",
|
|
146
|
+
status=501,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
result = create_booking(
|
|
151
|
+
service_ids=service_ids,
|
|
152
|
+
target_date=target_date,
|
|
153
|
+
selected_time=selected_time,
|
|
154
|
+
master_id=master_id,
|
|
155
|
+
master_selections=master_selections,
|
|
156
|
+
client=request.user,
|
|
157
|
+
extra_fields={"service_id": service_ids[0]},
|
|
158
|
+
persistence_hook=persistence_hook,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return render(request, "booking/partials/step_confirm.html", {
|
|
162
|
+
"error": str(e),
|
|
163
|
+
"service_ids": service_ids,
|
|
164
|
+
"date": target,
|
|
165
|
+
"time": selected_time,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
# Solo mode returns a single Appointment; multi mode returns a list via hook.
|
|
169
|
+
appointments = result if isinstance(result, list) else [result]
|
|
170
|
+
|
|
171
|
+
return render(request, "booking/partials/step_confirm.html", {
|
|
172
|
+
"appointments": appointments,
|
|
173
|
+
"success": True,
|
|
174
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Booking Module
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
1. Add `"booking"` to `INSTALLED_APPS`
|
|
6
|
+
2. Run migrations: `python manage.py makemigrations booking && python manage.py migrate`
|
|
7
|
+
3. Add URLs to root `urls.py`:
|
|
8
|
+
```python
|
|
9
|
+
path("booking/", include("booking.urls")),
|
|
10
|
+
```
|
|
11
|
+
4. Add cabinet URL to `cabinet/urls.py`:
|
|
12
|
+
```python
|
|
13
|
+
from cabinet.views.booking import my_bookings_view
|
|
14
|
+
path("my/bookings/", my_bookings_view, name="my_bookings"),
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Admin Setup
|
|
18
|
+
|
|
19
|
+
1. Create a `BookingSettings` model in `system/models/` extending `AbstractBookingSettings`
|
|
20
|
+
2. Register it in admin — configure `step_minutes`, `default_buffer_between_minutes`, etc.
|
|
21
|
+
3. Add masters via the Master admin (each master needs a linked User)
|
|
22
|
+
4. Set up working days for each master (Mon-Sun schedule)
|
|
23
|
+
5. Add services with durations and link them to masters
|
|
24
|
+
|
|
25
|
+
## How It Works
|
|
26
|
+
|
|
27
|
+
The booking wizard is a multi-step form:
|
|
28
|
+
|
|
29
|
+
1. **Select Service** — client picks a service
|
|
30
|
+
2. **Select Date** — calendar view, HTMX loads available dates
|
|
31
|
+
3. **Select Time** — available slots rendered as buttons
|
|
32
|
+
4. **Confirm** — summary and final submission
|
|
33
|
+
|
|
34
|
+
All slot computation is handled by `codex_django.booking` engine via
|
|
35
|
+
the adapter pattern — no booking logic lives in this app.
|
|
36
|
+
|
|
37
|
+
## Customizing Templates
|
|
38
|
+
|
|
39
|
+
Override any template by placing your version in your project's `templates/` dir:
|
|
40
|
+
|
|
41
|
+
- `booking/booking_page.html` — main wrapper
|
|
42
|
+
- `booking/partials/step_service.html` — service selection step
|
|
43
|
+
- `booking/partials/step_date.html` — calendar step
|
|
44
|
+
- `booking/partials/step_time.html` — time slots step
|
|
45
|
+
- `booking/partials/step_confirm.html` — confirmation step
|
|
46
|
+
- `cabinet/booking/my_bookings.html` — cabinet bookings list
|
|
47
|
+
|
|
48
|
+
## CSS Variables
|
|
49
|
+
|
|
50
|
+
The public booking templates use CSS custom properties for theming:
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
:root {
|
|
54
|
+
--booking-primary: #2563eb;
|
|
55
|
+
--booking-primary-hover: #1d4ed8;
|
|
56
|
+
--booking-bg: #ffffff;
|
|
57
|
+
--booking-bg-secondary: #f8fafc;
|
|
58
|
+
--booking-text: #1e293b;
|
|
59
|
+
--booking-text-muted: #64748b;
|
|
60
|
+
--booking-border: #e2e8f0;
|
|
61
|
+
--booking-radius: 8px;
|
|
62
|
+
--booking-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Override these in your site CSS to match your brand.
|
|
67
|
+
|
|
68
|
+
## URL Configuration
|
|
69
|
+
|
|
70
|
+
Public booking page:
|
|
71
|
+
- `GET /booking/` — wizard page
|
|
72
|
+
- `GET /booking/calendar/?service_id=1&year=2026&month=3` — calendar partial (HTMX)
|
|
73
|
+
- `GET /booking/slots/?service_id=1&date=2026-03-25` — time slots partial (HTMX)
|
|
74
|
+
- `POST /booking/confirm/` — create booking
|
|
75
|
+
|
|
76
|
+
Cabinet:
|
|
77
|
+
- `GET /cabinet/my/bookings/` — client's bookings list
|
|
78
|
+
|
|
79
|
+
## Multi-service Bookings
|
|
80
|
+
|
|
81
|
+
By default the booking wizard runs in **solo mode** (one service per booking).
|
|
82
|
+
To support booking multiple services in a single session, implement `BookingPersistenceHook`.
|
|
83
|
+
|
|
84
|
+
### 1. Implement the hook
|
|
85
|
+
|
|
86
|
+
Create `booking/hooks.py` in your project:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from __future__ import annotations
|
|
90
|
+
from typing import Any
|
|
91
|
+
from .models import Appointment
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MyBookingPersistenceHook:
|
|
95
|
+
"""Persists a multi-service chain as individual Appointment rows."""
|
|
96
|
+
|
|
97
|
+
def persist_chain(
|
|
98
|
+
self,
|
|
99
|
+
solution: Any,
|
|
100
|
+
service_ids: list[int],
|
|
101
|
+
client: Any,
|
|
102
|
+
extra_fields: dict[str, Any] | None = None,
|
|
103
|
+
) -> list[Appointment]:
|
|
104
|
+
appointments = []
|
|
105
|
+
for item in solution.items:
|
|
106
|
+
appointments.append(
|
|
107
|
+
Appointment.objects.create(
|
|
108
|
+
master_id=item.master_id,
|
|
109
|
+
service_id=item.service_id,
|
|
110
|
+
start_time=item.start_time,
|
|
111
|
+
end_time=item.end_time,
|
|
112
|
+
client=client,
|
|
113
|
+
**(extra_fields or {}),
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return appointments
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. Wire it into the confirm view
|
|
120
|
+
|
|
121
|
+
In `booking/views.py`, find the TODO comment and replace it:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from .hooks import MyBookingPersistenceHook
|
|
125
|
+
|
|
126
|
+
# inside confirm_booking_view, replace the TODO block:
|
|
127
|
+
if len(service_ids) > 1:
|
|
128
|
+
persistence_hook = MyBookingPersistenceHook()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 3. Update the confirm form
|
|
132
|
+
|
|
133
|
+
Your form should send multiple `service_ids` values:
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<input type="hidden" name="service_ids" value="3">
|
|
137
|
+
<input type="hidden" name="service_ids" value="7">
|
|
138
|
+
<!-- optional per-service master selection as JSON -->
|
|
139
|
+
<input type="hidden" name="master_selections" value='{"3": 10, "7": null}'>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A `null` value for a service means "any available master".
|
codex_django_cli/blueprints/features/booking/cabinet/templates/cabinet/booking/my_bookings.html
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
{% extends "cabinet/base_client.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{% trans "My Bookings" %}{% endblock %}
|
|
5
|
+
{% block page_title %}{% trans "My Bookings" %}{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block sidebar_nav %}
|
|
8
|
+
<li>
|
|
9
|
+
<a href="{% url 'cabinet:my_appointments' %}"
|
|
10
|
+
class="cab-nav__item d-flex align-items-center px-3 py-2 rounded mx-2
|
|
11
|
+
{% if active_section == 'my' %}cab-nav__item--active{% endif %}"
|
|
12
|
+
hx-get="{% url 'cabinet:my_appointments' %}"
|
|
13
|
+
hx-target="#cab-content"
|
|
14
|
+
hx-push-url="true"
|
|
15
|
+
style="color: var(--cab-sidebar-text); transition: background 0.15s;">
|
|
16
|
+
<span class="bi bi-calendar-check fs-5 flex-shrink-0"></span>
|
|
17
|
+
<span class="cab-nav__label ms-3" style="white-space: nowrap;">{% trans "My Appointments" %}</span>
|
|
18
|
+
</a>
|
|
19
|
+
</li>
|
|
20
|
+
<li>
|
|
21
|
+
<a href="{% url 'cabinet:my_bookings' %}"
|
|
22
|
+
class="cab-nav__item d-flex align-items-center px-3 py-2 rounded mx-2
|
|
23
|
+
{% if active_section == 'my_bookings' %}cab-nav__item--active{% endif %}"
|
|
24
|
+
hx-get="{% url 'cabinet:my_bookings' %}"
|
|
25
|
+
hx-target="#cab-content"
|
|
26
|
+
hx-push-url="true"
|
|
27
|
+
style="color: var(--cab-sidebar-text); transition: background 0.15s;">
|
|
28
|
+
<span class="bi bi-journal-bookmark fs-5 flex-shrink-0"></span>
|
|
29
|
+
<span class="cab-nav__label ms-3" style="white-space: nowrap;">{% trans "My Bookings" %}</span>
|
|
30
|
+
</a>
|
|
31
|
+
</li>
|
|
32
|
+
<li>
|
|
33
|
+
<a href="{% url 'cabinet:profile' %}"
|
|
34
|
+
class="cab-nav__item d-flex align-items-center px-3 py-2 rounded mx-2
|
|
35
|
+
{% if active_section == 'profile' %}cab-nav__item--active{% endif %}"
|
|
36
|
+
hx-get="{% url 'cabinet:profile' %}"
|
|
37
|
+
hx-target="#cab-content"
|
|
38
|
+
hx-push-url="true"
|
|
39
|
+
style="color: var(--cab-sidebar-text); transition: background 0.15s;">
|
|
40
|
+
<span class="bi bi-person fs-5 flex-shrink-0"></span>
|
|
41
|
+
<span class="cab-nav__label ms-3" style="white-space: nowrap;">{% trans "Profile" %}</span>
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
{% endblock %}
|
|
45
|
+
|
|
46
|
+
{% block sidebar_settings_link %}
|
|
47
|
+
<a href="{% url 'cabinet:settings' %}"
|
|
48
|
+
class="cab-nav__item d-flex align-items-center px-3 py-2 mx-2 rounded"
|
|
49
|
+
style="color: var(--cab-sidebar-text); transition: background 0.15s;">
|
|
50
|
+
<span class="bi bi-gear fs-5 flex-shrink-0"></span>
|
|
51
|
+
<span class="cab-nav__label ms-3" style="white-space: nowrap; font-size: 0.875rem;">{% trans "Settings" %}</span>
|
|
52
|
+
</a>
|
|
53
|
+
{% endblock %}
|
|
54
|
+
|
|
55
|
+
{% block cabinet_content %}
|
|
56
|
+
|
|
57
|
+
{# ─── Stats row ─── #}
|
|
58
|
+
<div class="row g-3 mb-4">
|
|
59
|
+
<div class="col-md-4">
|
|
60
|
+
<div class="card border-0 shadow-sm h-100">
|
|
61
|
+
<div class="card-body d-flex align-items-center gap-3">
|
|
62
|
+
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
|
63
|
+
style="width:48px;height:48px;background:#ede9fe;flex-shrink:0;">
|
|
64
|
+
<span class="bi bi-calendar-check fs-5" style="color:#7c3aed;"></span>
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<div class="text-muted" style="font-size:.75rem;">{% trans "Upcoming" %}</div>
|
|
68
|
+
<div class="fw-bold fs-4">{{ stats.upcoming }}</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="col-md-4">
|
|
74
|
+
<div class="card border-0 shadow-sm h-100">
|
|
75
|
+
<div class="card-body d-flex align-items-center gap-3">
|
|
76
|
+
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
|
77
|
+
style="width:48px;height:48px;background:#dcfce7;flex-shrink:0;">
|
|
78
|
+
<span class="bi bi-check-circle fs-5" style="color:#16a34a;"></span>
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<div class="text-muted" style="font-size:.75rem;">{% trans "Completed" %}</div>
|
|
82
|
+
<div class="fw-bold fs-4">{{ stats.completed }}</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="col-md-4">
|
|
88
|
+
<div class="card border-0 shadow-sm h-100">
|
|
89
|
+
<div class="card-body d-flex align-items-center gap-3">
|
|
90
|
+
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
|
91
|
+
style="width:48px;height:48px;background:#fee2e2;flex-shrink:0;">
|
|
92
|
+
<span class="bi bi-x-circle fs-5" style="color:#dc2626;"></span>
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<div class="text-muted" style="font-size:.75rem;">{% trans "Cancelled" %}</div>
|
|
96
|
+
<div class="fw-bold fs-4">{{ stats.cancelled }}</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{# ─── Status filter tabs ─── #}
|
|
104
|
+
<div class="d-flex gap-2 mb-4 flex-wrap">
|
|
105
|
+
<a href="{% url 'cabinet:my_bookings' %}"
|
|
106
|
+
class="btn btn-sm {% if not status_filter %}btn-dark{% else %}btn-outline-secondary{% endif %}"
|
|
107
|
+
style="font-size:.8rem;border-radius:20px;">
|
|
108
|
+
{% trans "All" %}
|
|
109
|
+
</a>
|
|
110
|
+
<a href="{% url 'cabinet:my_bookings' %}?status=pending"
|
|
111
|
+
class="btn btn-sm {% if status_filter == 'pending' %}btn-dark{% else %}btn-outline-secondary{% endif %}"
|
|
112
|
+
style="font-size:.8rem;border-radius:20px;">
|
|
113
|
+
{% trans "Pending" %}
|
|
114
|
+
</a>
|
|
115
|
+
<a href="{% url 'cabinet:my_bookings' %}?status=confirmed"
|
|
116
|
+
class="btn btn-sm {% if status_filter == 'confirmed' %}btn-dark{% else %}btn-outline-secondary{% endif %}"
|
|
117
|
+
style="font-size:.8rem;border-radius:20px;">
|
|
118
|
+
{% trans "Confirmed" %}
|
|
119
|
+
</a>
|
|
120
|
+
<a href="{% url 'cabinet:my_bookings' %}?status=completed"
|
|
121
|
+
class="btn btn-sm {% if status_filter == 'completed' %}btn-dark{% else %}btn-outline-secondary{% endif %}"
|
|
122
|
+
style="font-size:.8rem;border-radius:20px;">
|
|
123
|
+
{% trans "Completed" %}
|
|
124
|
+
</a>
|
|
125
|
+
<a href="{% url 'cabinet:my_bookings' %}?status=cancelled"
|
|
126
|
+
class="btn btn-sm {% if status_filter == 'cancelled' %}btn-dark{% else %}btn-outline-secondary{% endif %}"
|
|
127
|
+
style="font-size:.8rem;border-radius:20px;">
|
|
128
|
+
{% trans "Cancelled" %}
|
|
129
|
+
</a>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{# ─── Bookings table ─── #}
|
|
133
|
+
<div class="card border-0 shadow-sm">
|
|
134
|
+
<div class="card-header bg-white border-0 pt-3 pb-0 d-flex align-items-center justify-content-between">
|
|
135
|
+
<h6 class="fw-semibold mb-0">{% trans "My Bookings" %}</h6>
|
|
136
|
+
<a href="{% url 'booking:booking_page' %}" class="btn btn-sm btn-primary">
|
|
137
|
+
+ {% trans "Book Now" %}
|
|
138
|
+
</a>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="card-body p-0">
|
|
141
|
+
<table class="table table-hover mb-0">
|
|
142
|
+
<thead class="table-light">
|
|
143
|
+
<tr>
|
|
144
|
+
<th class="ps-4" style="font-size:.75rem;font-weight:600;color:#64748b;">{% trans "DATE AND TIME" %}</th>
|
|
145
|
+
<th style="font-size:.75rem;font-weight:600;color:#64748b;">{% trans "SERVICE" %}</th>
|
|
146
|
+
<th style="font-size:.75rem;font-weight:600;color:#64748b;">{% trans "SPECIALIST" %}</th>
|
|
147
|
+
<th style="font-size:.75rem;font-weight:600;color:#64748b;">{% trans "STATUS" %}</th>
|
|
148
|
+
<th></th>
|
|
149
|
+
</tr>
|
|
150
|
+
</thead>
|
|
151
|
+
<tbody>
|
|
152
|
+
{% for appt in page_obj %}
|
|
153
|
+
<tr>
|
|
154
|
+
<td class="ps-4">
|
|
155
|
+
<div class="fw-semibold">{{ appt.datetime_start|date:"j F Y" }}</div>
|
|
156
|
+
<div class="text-muted" style="font-size:.8rem;">{{ appt.datetime_start|time:"H:i" }}</div>
|
|
157
|
+
</td>
|
|
158
|
+
<td>{{ appt.service.name }}</td>
|
|
159
|
+
<td>
|
|
160
|
+
<div class="d-flex align-items-center gap-2">
|
|
161
|
+
<span class="rounded-circle d-flex align-items-center justify-content-center fw-bold text-white"
|
|
162
|
+
style="width:28px;height:28px;background:var(--cab-primary);font-size:.7rem;">
|
|
163
|
+
{{ appt.master.name|make_list|first }}
|
|
164
|
+
</span>
|
|
165
|
+
{{ appt.master.name }}
|
|
166
|
+
</div>
|
|
167
|
+
</td>
|
|
168
|
+
<td>
|
|
169
|
+
{% if appt.status == 'confirmed' %}
|
|
170
|
+
<span class="badge" style="background:#dcfce7;color:#16a34a;">{% trans "Confirmed" %}</span>
|
|
171
|
+
{% elif appt.status == 'pending' %}
|
|
172
|
+
<span class="badge" style="background:#fef9c3;color:#ca8a04;">{% trans "Pending" %}</span>
|
|
173
|
+
{% elif appt.status == 'completed' %}
|
|
174
|
+
<span class="badge" style="background:#dcfce7;color:#16a34a;">{% trans "Completed" %}</span>
|
|
175
|
+
{% elif appt.status == 'cancelled' %}
|
|
176
|
+
<span class="badge" style="background:#fee2e2;color:#dc2626;">{% trans "Cancelled" %}</span>
|
|
177
|
+
{% elif appt.status == 'no_show' %}
|
|
178
|
+
<span class="badge" style="background:#f1f5f9;color:#64748b;">{% trans "No Show" %}</span>
|
|
179
|
+
{% else %}
|
|
180
|
+
<span class="badge" style="background:#f1f5f9;color:#64748b;">{{ appt.get_status_display }}</span>
|
|
181
|
+
{% endif %}
|
|
182
|
+
</td>
|
|
183
|
+
<td class="text-end pe-3">
|
|
184
|
+
{% if appt.status == 'pending' or appt.status == 'confirmed' %}
|
|
185
|
+
<button class="btn btn-sm btn-outline-danger"
|
|
186
|
+
hx-post="{% url 'cabinet:cancel_booking' appt.pk %}"
|
|
187
|
+
hx-target="#cab-content"
|
|
188
|
+
hx-confirm="{% trans 'Cancel this booking?' %}">
|
|
189
|
+
{% trans "Cancel" %}
|
|
190
|
+
</button>
|
|
191
|
+
{% endif %}
|
|
192
|
+
</td>
|
|
193
|
+
</tr>
|
|
194
|
+
{% empty %}
|
|
195
|
+
<tr>
|
|
196
|
+
<td colspan="5" class="text-center py-5">
|
|
197
|
+
<span class="bi bi-calendar-x fs-1 text-muted d-block mb-2"></span>
|
|
198
|
+
<p class="text-muted mb-0">{% trans "No bookings found" %}</p>
|
|
199
|
+
</td>
|
|
200
|
+
</tr>
|
|
201
|
+
{% endfor %}
|
|
202
|
+
</tbody>
|
|
203
|
+
</table>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{% if page_obj.has_other_pages %}
|
|
207
|
+
<div class="card-footer bg-white border-0 d-flex align-items-center justify-content-between py-3 px-4">
|
|
208
|
+
<span class="text-muted" style="font-size:.8rem;">
|
|
209
|
+
{% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
|
|
210
|
+
Showing {{ start }}–{{ end }} of {{ total }}
|
|
211
|
+
{% endblocktrans %}
|
|
212
|
+
</span>
|
|
213
|
+
<nav>
|
|
214
|
+
<ul class="pagination pagination-sm mb-0">
|
|
215
|
+
{% if page_obj.has_previous %}
|
|
216
|
+
<li class="page-item">
|
|
217
|
+
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}">«</a>
|
|
218
|
+
</li>
|
|
219
|
+
{% else %}
|
|
220
|
+
<li class="page-item disabled">
|
|
221
|
+
<a class="page-link" href="#">«</a>
|
|
222
|
+
</li>
|
|
223
|
+
{% endif %}
|
|
224
|
+
|
|
225
|
+
{% for num in page_obj.paginator.page_range %}
|
|
226
|
+
<li class="page-item {% if page_obj.number == num %}active{% endif %}">
|
|
227
|
+
<a class="page-link" href="?page={{ num }}{% if status_filter %}&status={{ status_filter }}{% endif %}"
|
|
228
|
+
{% if page_obj.number == num %}style="background:var(--cab-primary);border-color:var(--cab-primary);"{% endif %}>
|
|
229
|
+
{{ num }}
|
|
230
|
+
</a>
|
|
231
|
+
</li>
|
|
232
|
+
{% endfor %}
|
|
233
|
+
|
|
234
|
+
{% if page_obj.has_next %}
|
|
235
|
+
<li class="page-item">
|
|
236
|
+
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if status_filter %}&status={{ status_filter }}{% endif %}">»</a>
|
|
237
|
+
</li>
|
|
238
|
+
{% else %}
|
|
239
|
+
<li class="page-item disabled">
|
|
240
|
+
<a class="page-link" href="#">»</a>
|
|
241
|
+
</li>
|
|
242
|
+
{% endif %}
|
|
243
|
+
</ul>
|
|
244
|
+
</nav>
|
|
245
|
+
</div>
|
|
246
|
+
{% endif %}
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{% endblock %}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cabinet booking views — client-facing "My Bookings" page.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from django.contrib.auth.decorators import login_required
|
|
8
|
+
from django.core.paginator import Paginator
|
|
9
|
+
from django.http import HttpResponse
|
|
10
|
+
from django.shortcuts import render
|
|
11
|
+
from django.views.decorators.http import require_GET, require_POST
|
|
12
|
+
|
|
13
|
+
from booking.models import Appointment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@login_required
|
|
17
|
+
@require_GET
|
|
18
|
+
def my_bookings_view(request):
|
|
19
|
+
"""List the current user's appointments with status filter and pagination."""
|
|
20
|
+
status_filter = request.GET.get("status", "")
|
|
21
|
+
qs = Appointment.objects.filter(client=request.user).select_related("master", "service")
|
|
22
|
+
|
|
23
|
+
if status_filter and status_filter in dict(Appointment.STATUS_CHOICES if hasattr(Appointment, "STATUS_CHOICES") else []):
|
|
24
|
+
qs = qs.filter(status=status_filter)
|
|
25
|
+
|
|
26
|
+
paginator = Paginator(qs, 10)
|
|
27
|
+
page = paginator.get_page(request.GET.get("page", 1))
|
|
28
|
+
|
|
29
|
+
# Count stats
|
|
30
|
+
user_appointments = Appointment.objects.filter(client=request.user)
|
|
31
|
+
stats = {
|
|
32
|
+
"upcoming": user_appointments.filter(status__in=["pending", "confirmed"]).count(),
|
|
33
|
+
"completed": user_appointments.filter(status="completed").count(),
|
|
34
|
+
"cancelled": user_appointments.filter(status="cancelled").count(),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return render(request, "cabinet/booking/my_bookings.html", {
|
|
38
|
+
"page_obj": page,
|
|
39
|
+
"status_filter": status_filter,
|
|
40
|
+
"stats": stats,
|
|
41
|
+
"active_section": "my_bookings",
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@login_required
|
|
46
|
+
@require_POST
|
|
47
|
+
def cancel_booking_view(request, pk):
|
|
48
|
+
"""Cancel an appointment via HTMX."""
|
|
49
|
+
try:
|
|
50
|
+
appointment = Appointment.objects.get(pk=pk, client=request.user)
|
|
51
|
+
except Appointment.DoesNotExist:
|
|
52
|
+
return HttpResponse("Not found", status=404)
|
|
53
|
+
|
|
54
|
+
if appointment.status in ("pending", "confirmed"):
|
|
55
|
+
appointment.status = "cancelled"
|
|
56
|
+
appointment.save(update_fields=["status"])
|
|
57
|
+
|
|
58
|
+
return render(request, "cabinet/booking/my_bookings.html", {
|
|
59
|
+
"page_obj": Paginator(
|
|
60
|
+
Appointment.objects.filter(client=request.user).select_related("master", "service"),
|
|
61
|
+
10,
|
|
62
|
+
).get_page(1),
|
|
63
|
+
"status_filter": "",
|
|
64
|
+
"stats": {
|
|
65
|
+
"upcoming": Appointment.objects.filter(client=request.user, status__in=["pending", "confirmed"]).count(),
|
|
66
|
+
"completed": Appointment.objects.filter(client=request.user, status="completed").count(),
|
|
67
|
+
"cancelled": Appointment.objects.filter(client=request.user, status="cancelled").count(),
|
|
68
|
+
},
|
|
69
|
+
"active_section": "my_bookings",
|
|
70
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from system.models import BookingSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(BookingSettings)
|
|
7
|
+
class BookingSettingsAdmin(admin.ModelAdmin):
|
|
8
|
+
fieldsets = [
|
|
9
|
+
(
|
|
10
|
+
"Time Grid",
|
|
11
|
+
{
|
|
12
|
+
"fields": [
|
|
13
|
+
"step_minutes",
|
|
14
|
+
"default_buffer_between_minutes",
|
|
15
|
+
"min_advance_minutes",
|
|
16
|
+
"max_advance_days",
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
),
|
|
20
|
+
(
|
|
21
|
+
"Default Working Hours",
|
|
22
|
+
{
|
|
23
|
+
"fields": [
|
|
24
|
+
"work_start_weekdays",
|
|
25
|
+
"work_end_weekdays",
|
|
26
|
+
"work_start_saturday",
|
|
27
|
+
"work_end_saturday",
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
]
|