Commit 449c59b9 authored by Administrator's avatar Administrator

Update 326 files via Son of Anton

parent 6810c876
# ============================================
# THE CLUB ERP — Environment Variables
# ============================================
# Django
DJANGO_SETTINGS_MODULE=config.settings.dev
SECRET_KEY=change-me-to-a-very-long-random-string-at-least-50-chars
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# Database
DB_NAME=theclub
DB_USER=theclub
DB_PASSWORD=theclub_secret_2024
DB_HOST=db
DB_PORT=5432
# Redis
REDIS_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/1
CELERY_RESULT_BACKEND=redis://redis:6379/2
# MinIO / S3
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin123
MINIO_ENDPOINT=minio:9000
MINIO_BUCKET_NAME=theclub-media
MINIO_USE_SSL=False
# JWT
JWT_ACCESS_TOKEN_LIFETIME_MINUTES=60
JWT_REFRESH_TOKEN_LIFETIME_DAYS=7
# SMS Gateway (placeholder)
SMS_GATEWAY_URL=https://sms.example.com/api/send
SMS_GATEWAY_API_KEY=your-sms-api-key
SMS_GATEWAY_SENDER_ID=THECLUB
# Email (optional)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=True
# Sentry (optional)
SENTRY_DSN=
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
\ No newline at end of file
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY backend/requirements/base.txt /tmp/requirements/base.txt
COPY backend/requirements/prod.txt /tmp/requirements/prod.txt
RUN pip install --no-cache-dir -r /tmp/requirements/base.txt
COPY backend/ /app/
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
\ No newline at end of file
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY backend/requirements/base.txt /tmp/requirements/base.txt
RUN pip install --no-cache-dir -r /tmp/requirements/base.txt
COPY backend/ /app/
CMD ["celery", "-A", "config", "worker", "-l", "info"]
\ No newline at end of file
# THE CLUB ERP
## نظام إدارة النادي — THE CLUB
A complete Club ERP system built with Django 5.1, Next.js 14, PostgreSQL, Redis, and Celery.
### Quick Start
\ No newline at end of file
# Admin for archive — implemented in Phase 2
\ No newline at end of file
from django.apps import AppConfig
class ArchiveConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.archive'
verbose_name = 'الأرشيف'
\ No newline at end of file
# Models for archive — implemented in Phase 2
\ No newline at end of file
# Serializers for archive — implemented in Phase 2
\ No newline at end of file
# Tests for archive — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for archive — implemented in Phase 2
\ No newline at end of file
# Admin for audit — implemented in Phase 2
\ No newline at end of file
from django.apps import AppConfig
class AuditConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.audit'
verbose_name = 'سجل المراجعة'
\ No newline at end of file
"""
THE CLUB ERP — Audit IP Middleware
===================================
Captures IP address and user-agent for audit logging.
"""
class AuditIPMiddleware:
"""Middleware that attaches IP and user-agent to the request for audit logging."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Extract IP address
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
request.audit_ip = x_forwarded_for.split(',')[0].strip()
else:
request.audit_ip = request.META.get('REMOTE_ADDR', '0.0.0.0')
# Extract user-agent
request.audit_user_agent = request.META.get('HTTP_USER_AGENT', '')
response = self.get_response(request)
return response
\ No newline at end of file
# Models for audit — implemented in Phase 2
\ No newline at end of file
# Serializers for audit — implemented in Phase 2
\ No newline at end of file
# Tests for audit — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for audit — implemented in Phase 2
\ No newline at end of file
from django.apps import AppConfig
class AuthenticationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.authentication'
verbose_name = 'المصادقة'
\ No newline at end of file
# Custom authentication backends — implemented in Phase 2
\ No newline at end of file
# Serializers for authentication — implemented in Phase 2
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""Placeholder — full implementation in Phase 2."""
pass
\ No newline at end of file
# Tests for authentication — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for authentication — implemented in Phase 2
\ No newline at end of file
# Admin for branches — implemented in Phase 2
\ No newline at end of file
from django.apps import AppConfig
class BranchesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.branches'
verbose_name = 'الفروع'
\ No newline at end of file
# Models for branches — implemented in Phase 2
\ No newline at end of file
# Management command: seed_branches — implemented in Phase 2
\ No newline at end of file
# Serializers for branches — implemented in Phase 2
\ No newline at end of file
# Tests for branches — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for branches — implemented in Phase 2
\ No newline at end of file
# Admin for carnets — implemented in Phase 9
\ No newline at end of file
from django.apps import AppConfig
class CarnetsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.carnets'
verbose_name = 'الكارنيهات'
\ No newline at end of file
# QR code + carnet image generator — implemented in Phase 9
\ No newline at end of file
# Models for carnets — implemented in Phase 9
\ No newline at end of file
# Serializers for carnets — implemented in Phase 9
\ No newline at end of file
# Tests for carnets — implemented in Phase 9
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for carnets — implemented in Phase 9
\ No newline at end of file
# Admin for children — implemented in Phase 4
\ No newline at end of file
from django.apps import AppConfig
class ChildrenConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.children'
verbose_name = 'الأبناء'
\ No newline at end of file
# Child fee calculator — implemented in Phase 4
\ No newline at end of file
# Models for children — implemented in Phase 4
\ No newline at end of file
# Serializers for children — implemented in Phase 4
\ No newline at end of file
# Tests for children — implemented in Phase 4
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for children — implemented in Phase 4
\ No newline at end of file
from django.apps import AppConfig
class DashboardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.dashboard'
verbose_name = 'لوحة التحكم'
\ No newline at end of file
# Serializers for dashboard — implemented in Phase 10
\ No newline at end of file
# Tests for dashboard — implemented in Phase 10
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for dashboard — implemented in Phase 10
\ No newline at end of file
# Admin for death_cases — implemented in Phase 8
\ No newline at end of file
from django.apps import AppConfig
class DeathCasesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.death_cases'
verbose_name = 'حالات الوفاة'
\ No newline at end of file
# Models for death_cases — implemented in Phase 8
\ No newline at end of file
# Serializers for death_cases — implemented in Phase 8
\ No newline at end of file
# Death case services — implemented in Phase 8
\ No newline at end of file
# Tests for death_cases — implemented in Phase 8
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for death_cases — implemented in Phase 8
\ No newline at end of file
# Admin for divorce_cases — implemented in Phase 8
\ No newline at end of file
from django.apps import AppConfig
class DivorceCasesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.divorce_cases'
verbose_name = 'حالات الطلاق'
\ No newline at end of file
# Divorce fee calculator — implemented in Phase 8
\ No newline at end of file
# Models for divorce_cases — implemented in Phase 8
\ No newline at end of file
# Serializers for divorce_cases — implemented in Phase 8
\ No newline at end of file
# Tests for divorce_cases — implemented in Phase 8
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for divorce_cases — implemented in Phase 8
\ No newline at end of file
# Admin for documents — implemented in Phase 9
\ No newline at end of file
from django.apps import AppConfig
class DocumentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.documents'
verbose_name = 'المستندات'
\ No newline at end of file
# Models for documents — implemented in Phase 9
\ No newline at end of file
# Serializers for documents — implemented in Phase 9
\ No newline at end of file
# Tests for documents — implemented in Phase 9
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for documents — implemented in Phase 9
\ No newline at end of file
# Admin for fines — implemented in Phase 7
\ No newline at end of file
from django.apps import AppConfig
class FinesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.fines'
verbose_name = 'الغرامات والجزاءات'
\ No newline at end of file
# Late fine calculator — implemented in Phase 7
\ No newline at end of file
# Models for fines — implemented in Phase 7
\ No newline at end of file
# Serializers for fines — implemented in Phase 7
\ No newline at end of file
# Tests for fines — implemented in Phase 7
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for fines — implemented in Phase 7
\ No newline at end of file
# Admin for foreign_members — implemented in Phase 5
\ No newline at end of file
from django.apps import AppConfig
class ForeignMembersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.foreign_members'
verbose_name = 'الأعضاء الأجانب'
\ No newline at end of file
# Models for foreign_members — implemented in Phase 5
\ No newline at end of file
# Serializers for foreign_members — implemented in Phase 5
\ No newline at end of file
# Tests for foreign_members — implemented in Phase 5
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for foreign_members — implemented in Phase 5
\ No newline at end of file
# Admin for forms_engine — implemented in Phase 3
\ No newline at end of file
from django.apps import AppConfig
class FormsEngineConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.forms_engine'
verbose_name = 'محرك الاستمارات'
\ No newline at end of file
# Models for forms_engine — implemented in Phase 3
\ No newline at end of file
# Management command: seed_forms — implemented in Phase 3
\ No newline at end of file
# Serializers for forms_engine — implemented in Phase 3
\ No newline at end of file
# Tests for forms_engine — implemented in Phase 3
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Form validator — implemented in Phase 3
\ No newline at end of file
# Views for forms_engine — implemented in Phase 3
\ No newline at end of file
# Admin for honorary — implemented in Phase 5
\ No newline at end of file
from django.apps import AppConfig
class HonoraryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.honorary'
verbose_name = 'العضوية الشرفية'
\ No newline at end of file
# Models for honorary — implemented in Phase 5
\ No newline at end of file
# Serializers for honorary — implemented in Phase 5
\ No newline at end of file
# Tests for honorary — implemented in Phase 5
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for honorary — implemented in Phase 5
\ No newline at end of file
# Admin for installments — implemented in Phase 6
\ No newline at end of file
from django.apps import AppConfig
class InstallmentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.installments'
verbose_name = 'الأقساط'
\ No newline at end of file
# Installment calculator — implemented in Phase 6
\ No newline at end of file
# Models for installments — implemented in Phase 6
\ No newline at end of file
# Serializers for installments — implemented in Phase 6
\ No newline at end of file
# Tests for installments — implemented in Phase 6
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for installments — implemented in Phase 6
\ No newline at end of file
# Admin for interviews — implemented in Phase 8
\ No newline at end of file
from django.apps import AppConfig
class InterviewsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.interviews'
verbose_name = 'المقابلات'
\ No newline at end of file
# Models for interviews — implemented in Phase 8
\ No newline at end of file
# Serializers for interviews — implemented in Phase 8
\ No newline at end of file
# Tests for interviews — implemented in Phase 8
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for interviews — implemented in Phase 8
\ No newline at end of file
# Admin for members — implemented in Phase 4
\ No newline at end of file
from django.apps import AppConfig
class MembersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.members'
verbose_name = 'الأعضاء'
def ready(self):
import apps.members.signals # noqa
\ No newline at end of file
# Filters for members — implemented in Phase 4
\ No newline at end of file
# Models for members — implemented in Phase 4
\ No newline at end of file
# Serializers for members — implemented in Phase 4
\ No newline at end of file
# Signals for members — implemented in Phase 4
\ No newline at end of file
# Tests for members — implemented in Phase 4
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for members — implemented in Phase 4
\ No newline at end of file
# Admin for notifications — implemented in Phase 9
\ No newline at end of file
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.notifications'
verbose_name = 'الإشعارات والرسائل'
\ No newline at end of file
# Models for notifications — implemented in Phase 9
\ No newline at end of file
# Management command: seed_sms_templates — implemented in Phase 9
\ No newline at end of file
# Serializers for notifications — implemented in Phase 9
\ No newline at end of file
# SMS service — implemented in Phase 9
\ No newline at end of file
# Tests for notifications — implemented in Phase 9
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for notifications — implemented in Phase 9
\ No newline at end of file
# Admin for payments — implemented in Phase 6
\ No newline at end of file
from django.apps import AppConfig
class PaymentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.payments'
verbose_name = 'المدفوعات'
\ No newline at end of file
# Models for payments — implemented in Phase 6
\ No newline at end of file
# Serializers for payments — implemented in Phase 6
\ No newline at end of file
# Tests for payments — implemented in Phase 6
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for payments — implemented in Phase 6
\ No newline at end of file
# Admin for pricing — implemented in Phase 3
\ No newline at end of file
from django.apps import AppConfig
class PricingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.pricing'
verbose_name = 'التسعير'
\ No newline at end of file
# Price calculator — implemented in Phase 3
\ No newline at end of file
# Models for pricing — implemented in Phase 3
\ No newline at end of file
# Management command: seed_pricing — implemented in Phase 3
\ No newline at end of file
# Serializers for pricing — implemented in Phase 3
\ No newline at end of file
# Tests for pricing — implemented in Phase 3
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for pricing — implemented in Phase 3
\ No newline at end of file
# Admin for receipts — implemented in Phase 6
\ No newline at end of file
from django.apps import AppConfig
class ReceiptsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.receipts'
verbose_name = 'الإيصالات'
\ No newline at end of file
# Models for receipts — implemented in Phase 6
\ No newline at end of file
# Serializers for receipts — implemented in Phase 6
\ No newline at end of file
# Tests for receipts — implemented in Phase 6
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for receipts — implemented in Phase 6
\ No newline at end of file
# Admin for reports — implemented in Phase 10
\ No newline at end of file
from django.apps import AppConfig
class ReportsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.reports'
verbose_name = 'التقارير'
\ No newline at end of file
# Base report generator — implemented in Phase 10
\ No newline at end of file
# Children report generator — implemented in Phase 10
\ No newline at end of file
# Financial report generator — implemented in Phase 10
\ No newline at end of file
# Membership report generator — implemented in Phase 10
\ No newline at end of file
# Operations report generator — implemented in Phase 10
\ No newline at end of file
# Models for reports — implemented in Phase 10
\ No newline at end of file
# Serializers for reports — implemented in Phase 10
\ No newline at end of file
# Tests for reports — implemented in Phase 10
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for reports — implemented in Phase 10
\ No newline at end of file
# Admin for roles — implemented in Phase 2
\ No newline at end of file
from django.apps import AppConfig
class RolesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.roles'
verbose_name = 'الأدوار والصلاحيات'
\ No newline at end of file
# Models for roles — implemented in Phase 2
\ No newline at end of file
# Management command: seed_roles — implemented in Phase 2
\ No newline at end of file
# Serializers for roles — implemented in Phase 2
\ No newline at end of file
# Tests for roles — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for roles — implemented in Phase 2
\ No newline at end of file
# Admin for rules_engine — implemented in Phase 3
\ No newline at end of file
from django.apps import AppConfig
class RulesEngineConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.rules_engine'
verbose_name = 'محرك القواعد'
\ No newline at end of file
# Rule evaluator — implemented in Phase 3
\ No newline at end of file
# Models for rules_engine — implemented in Phase 3
\ No newline at end of file
# Management command: seed_rules — implemented in Phase 3
\ No newline at end of file
# Serializers for rules_engine — implemented in Phase 3
\ No newline at end of file
# Tests for rules_engine — implemented in Phase 3
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for rules_engine — implemented in Phase 3
\ No newline at end of file
# Admin for seasonal — implemented in Phase 5
\ No newline at end of file
from django.apps import AppConfig
class SeasonalConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.seasonal'
verbose_name = 'العضوية الموسمية'
\ No newline at end of file
# Models for seasonal — implemented in Phase 5
\ No newline at end of file
# Serializers for seasonal — implemented in Phase 5
\ No newline at end of file
# Tests for seasonal — implemented in Phase 5
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for seasonal — implemented in Phase 5
\ No newline at end of file
# Admin for service_catalog — implemented in Phase 3
\ No newline at end of file
from django.apps import AppConfig
class ServiceCatalogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.service_catalog'
verbose_name = 'كتالوج الخدمات'
\ No newline at end of file
# Models for service_catalog — implemented in Phase 3
\ No newline at end of file
# Management command: seed_catalog — implemented in Phase 3
\ No newline at end of file
# Serializers for service_catalog — implemented in Phase 3
\ No newline at end of file
# Tests for service_catalog — implemented in Phase 3
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for service_catalog — implemented in Phase 3
\ No newline at end of file
# Admin for settings_app — implemented in Phase 2
\ No newline at end of file
from django.apps import AppConfig
class SettingsAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.settings_app'
verbose_name = 'إعدادات النظام'
\ No newline at end of file
# Models for settings_app — implemented in Phase 2
\ No newline at end of file
# Management command: seed_settings — implemented in Phase 2
\ No newline at end of file
# Serializers for settings_app — implemented in Phase 2
\ No newline at end of file
# Tests for settings_app — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for settings_app — implemented in Phase 2
\ No newline at end of file
# Admin for sports — implemented in Phase 5
\ No newline at end of file
from django.apps import AppConfig
class SportsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.sports'
verbose_name = 'العضوية الرياضية'
\ No newline at end of file
# Models for sports — implemented in Phase 5
\ No newline at end of file
# Serializers for sports — implemented in Phase 5
\ No newline at end of file
# Tests for sports — implemented in Phase 5
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for sports — implemented in Phase 5
\ No newline at end of file
# Admin for spouses — implemented in Phase 4
\ No newline at end of file
from django.apps import AppConfig
class SpousesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.spouses'
verbose_name = 'الأزواج'
\ No newline at end of file
# Spouse fee calculator — implemented in Phase 4
\ No newline at end of file
# Models for spouses — implemented in Phase 4
\ No newline at end of file
# Serializers for spouses — implemented in Phase 4
\ No newline at end of file
# Tests for spouses — implemented in Phase 4
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for spouses — implemented in Phase 4
\ No newline at end of file
# Admin for subscriptions — implemented in Phase 7
\ No newline at end of file
from django.apps import AppConfig
class SubscriptionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.subscriptions'
verbose_name = 'الاشتراكات السنوية'
\ No newline at end of file
# Subscription generator — implemented in Phase 7
\ No newline at end of file
# Models for subscriptions — implemented in Phase 7
\ No newline at end of file
# Serializers for subscriptions — implemented in Phase 7
\ No newline at end of file
# Tests for subscriptions — implemented in Phase 7
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for subscriptions — implemented in Phase 7
\ No newline at end of file
# Admin for temporary_members — implemented in Phase 5
\ No newline at end of file
from django.apps import AppConfig
class TemporaryMembersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.temporary_members'
verbose_name = 'الأعضاء المؤقتون'
\ No newline at end of file
# Models for temporary_members — implemented in Phase 5
\ No newline at end of file
# Serializers for temporary_members — implemented in Phase 5
\ No newline at end of file
# Tests for temporary_members — implemented in Phase 5
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for temporary_members — implemented in Phase 5
\ No newline at end of file
# Admin for transfers — implemented in Phase 8
\ No newline at end of file
from django.apps import AppConfig
class TransfersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.transfers'
verbose_name = 'التحويلات والفصل'
\ No newline at end of file
# Transfer fee calculator — implemented in Phase 8
\ No newline at end of file
# Models for transfers — implemented in Phase 8
\ No newline at end of file
# Serializers for transfers — implemented in Phase 8
\ No newline at end of file
# Transfer services — implemented in Phase 8
\ No newline at end of file
# Tests for transfers — implemented in Phase 8
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for transfers — implemented in Phase 8
\ No newline at end of file
# Admin for users — implemented in Phase 2
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import Employee
@admin.register(Employee)
class EmployeeAdmin(UserAdmin):
list_display = ['username', 'full_name_ar', 'is_active']
\ No newline at end of file
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'
verbose_name = 'الموظفون'
def ready(self):
import apps.users.signals # noqa
\ No newline at end of file
# Models for users — implemented in Phase 2
from django.contrib.auth.models import AbstractUser
from django.db import models
class Employee(AbstractUser):
"""Placeholder custom user model — full implementation in Phase 2."""
full_name_ar = models.CharField(max_length=200, verbose_name="الاسم بالعربي", default="")
class Meta:
verbose_name = "موظف"
verbose_name_plural = "الموظفون"
def __str__(self):
return self.full_name_ar or self.username
\ No newline at end of file
# Permissions for users — implemented in Phase 2
\ No newline at end of file
# Serializers for users — implemented in Phase 2
\ No newline at end of file
# Signals for users — implemented in Phase 2
\ No newline at end of file
# Tests for users — implemented in Phase 2
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for users — implemented in Phase 2
\ No newline at end of file
# Admin for waivers — implemented in Phase 8
\ No newline at end of file
from django.apps import AppConfig
class WaiversConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.waivers'
verbose_name = 'التنازلات'
\ No newline at end of file
# Models for waivers — implemented in Phase 8
\ No newline at end of file
# Serializers for waivers — implemented in Phase 8
\ No newline at end of file
# Tests for waivers — implemented in Phase 8
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for waivers — implemented in Phase 8
\ No newline at end of file
# Admin for workflows — implemented in Phase 3
\ No newline at end of file
from django.apps import AppConfig
class WorkflowsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.workflows'
verbose_name = 'سير العمل'
\ No newline at end of file
# State machine engine — implemented in Phase 3
\ No newline at end of file
# Models for workflows — implemented in Phase 3
\ No newline at end of file
# Management command: seed_workflows — implemented in Phase 3
\ No newline at end of file
# Serializers for workflows — implemented in Phase 3
\ No newline at end of file
# Tests for workflows — implemented in Phase 3
\ No newline at end of file
urlpatterns = []
\ No newline at end of file
# Views for workflows — implemented in Phase 3
\ No newline at end of file
from .celery import app as celery_app
__all__ = ('celery_app',)
\ No newline at end of file
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
application = get_asgi_application()
\ No newline at end of file
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
app = Celery('theclub')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')
\ No newline at end of file
import os
environment = os.environ.get('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
\ No newline at end of file
"""
THE CLUB ERP — Base Settings
============================
All shared settings across environments.
"""
import os
from pathlib import Path
from datetime import timedelta
# ─── Paths ────────────────────────────────────────────
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# ─── Security ─────────────────────────────────────────
SECRET_KEY = os.environ.get('SECRET_KEY', 'insecure-dev-key-change-in-production')
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes')
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1,0.0.0.0').split(',')
# ─── Applications ─────────────────────────────────────
INSTALLED_APPS = [
# Django built-in
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'django_filters',
'corsheaders',
'simple_history',
'auditlog',
'guardian',
'django_celery_beat',
'django_json_widget',
'import_export',
'storages',
# Project apps — Layer 1: Foundation
'apps.users',
'apps.authentication',
'apps.roles',
'apps.branches',
'apps.audit',
'apps.settings_app',
'apps.archive',
# Project apps — Layer 2: Engines
'apps.rules_engine',
'apps.pricing',
'apps.service_catalog',
'apps.forms_engine',
'apps.workflows',
# Project apps — Layer 3: Core Members
'apps.members',
'apps.spouses',
'apps.children',
'apps.temporary_members',
'apps.seasonal',
'apps.sports',
'apps.honorary',
'apps.foreign_members',
# Project apps — Layer 4: Financial
'apps.payments',
'apps.receipts',
'apps.installments',
'apps.subscriptions',
'apps.fines',
# Project apps — Layer 5: Operations
'apps.transfers',
'apps.divorce_cases',
'apps.death_cases',
'apps.waivers',
'apps.interviews',
'apps.carnets',
'apps.documents',
# Project apps — Layer 6: Communication & Reporting
'apps.notifications',
'apps.reports',
'apps.dashboard',
]
# ─── Custom User Model ────────────────────────────────
AUTH_USER_MODEL = 'users.Employee'
# ─── Middleware ────────────────────────────────────────
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
'auditlog.middleware.AuditlogMiddleware',
'apps.audit.middleware.AuditIPMiddleware',
]
# ─── URL Config ───────────────────────────────────────
ROOT_URLCONF = 'config.urls'
# ─── Templates ────────────────────────────────────────
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# ─── Database ─────────────────────────────────────────
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'theclub'),
'USER': os.environ.get('DB_USER', 'theclub'),
'PASSWORD': os.environ.get('DB_PASSWORD', 'theclub_secret_2024'),
'HOST': os.environ.get('DB_HOST', 'db'),
'PORT': os.environ.get('DB_PORT', '5432'),
'OPTIONS': {
'connect_timeout': 10,
},
}
}
# ─── Cache ────────────────────────────────────────────
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://redis:6379/0'),
}
}
# ─── Password Validation ──────────────────────────────
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 8}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# ─── Authentication Backends ──────────────────────────
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
]
# ─── Internationalization ─────────────────────────────
LANGUAGE_CODE = 'ar'
TIME_ZONE = 'Africa/Cairo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
LANGUAGES = [
('ar', 'العربية'),
('en', 'English'),
]
# ─── Static Files ─────────────────────────────────────
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# ─── Media Files ──────────────────────────────────────
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# ─── Default Primary Key ──────────────────────────────
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ─── REST Framework ───────────────────────────────────
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 25,
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '30/minute',
'user': '120/minute',
},
'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S%z',
'DATE_FORMAT': '%Y-%m-%d',
}
# ─── JWT Settings ─────────────────────────────────────
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(
minutes=int(os.environ.get('JWT_ACCESS_TOKEN_LIFETIME_MINUTES', 60))
),
'REFRESH_TOKEN_LIFETIME': timedelta(
days=int(os.environ.get('JWT_REFRESH_TOKEN_LIFETIME_DAYS', 7))
),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'TOKEN_OBTAIN_SERIALIZER': 'apps.authentication.serializers.CustomTokenObtainPairSerializer',
}
# ─── CORS ─────────────────────────────────────────────
CORS_ALLOWED_ORIGINS = [
'http://localhost:3000',
'http://127.0.0.1:3000',
]
CORS_ALLOW_CREDENTIALS = True
# ─── Celery ───────────────────────────────────────────
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://redis:6379/1')
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://redis:6379/2')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Africa/Cairo'
CELERY_ENABLE_UTC = True
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# ─── django-simple-history ────────────────────────────
SIMPLE_HISTORY_HISTORY_ID_USE_UUID = False
SIMPLE_HISTORY_REVERT_DISABLED = True
# ─── django-auditlog ─────────────────────────────────
AUDITLOG_INCLUDE_ALL_MODELS = False
# ─── File Upload ──────────────────────────────────────
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024
# ─── Session ──────────────────────────────────────────
SESSION_COOKIE_AGE = 30 * 60 # 30 minutes
SESSION_SAVE_EVERY_REQUEST = True
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
# ─── Password Hashing ────────────────────────────────
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]
# ─── Logging ──────────────────────────────────────────
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
'apps': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
},
}
\ No newline at end of file
"""
THE CLUB ERP — Development Settings
"""
from .base import * # noqa: F401,F403
DEBUG = True
INSTALLED_APPS += [ # noqa: F405
'django_extensions',
]
# Allow all origins in dev
CORS_ALLOW_ALL_ORIGINS = True
# Disable throttling in dev
REST_FRAMEWORK['DEFAULT_THROTTLE_CLASSES'] = [] # noqa: F405
# Email to console in dev
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# More verbose logging in dev
LOGGING['loggers']['apps']['level'] = 'DEBUG' # noqa: F405
# Use local filesystem for media in dev
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
\ No newline at end of file
"""
THE CLUB ERP — Production Settings
"""
import os
from .base import * # noqa: F401,F403
DEBUG = False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# ─── Security ─────────────────────────────────────────
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_SSL_REDIRECT = os.environ.get('SECURE_SSL_REDIRECT', 'True').lower() == 'true'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# ─── S3 Storage (MinIO) ──────────────────────────────
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_ACCESS_KEY_ID = os.environ.get('MINIO_ACCESS_KEY')
AWS_SECRET_ACCESS_KEY = os.environ.get('MINIO_SECRET_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('MINIO_BUCKET_NAME', 'theclub-media')
AWS_S3_ENDPOINT_URL = f"http://{os.environ.get('MINIO_ENDPOINT', 'minio:9000')}"
AWS_S3_USE_SSL = os.environ.get('MINIO_USE_SSL', 'False').lower() == 'true'
AWS_S3_FILE_OVERWRITE = False
AWS_DEFAULT_ACL = None
AWS_QUERYSTRING_AUTH = True
AWS_S3_SIGNATURE_VERSION = 's3v4'
# ─── CORS ─────────────────────────────────────────────
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',')
# ─── Sentry ───────────────────────────────────────────
SENTRY_DSN = os.environ.get('SENTRY_DSN')
if SENTRY_DSN:
import sentry_sdk
sentry_sdk.init(
dsn=SENTRY_DSN,
traces_sample_rate=0.1,
profiles_sample_rate=0.1,
)
# ─── Logging ──────────────────────────────────────────
LOGGING['loggers']['apps']['level'] = 'WARNING' # noqa: F405
\ No newline at end of file
"""
THE CLUB ERP — Master URL Configuration
========================================
All API endpoints are versioned under /api/v1/
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
# ─── Admin Customization ─────────────────────────────
admin.site.site_header = 'THE CLUB — لوحة التحكم'
admin.site.site_title = 'THE CLUB ERP'
admin.site.index_title = 'إدارة النظام'
urlpatterns = [
# Django Admin
path('admin/', admin.site.urls),
# API v1
path('api/v1/auth/', include('apps.authentication.urls')),
path('api/v1/users/', include('apps.users.urls')),
path('api/v1/roles/', include('apps.roles.urls')),
path('api/v1/branches/', include('apps.branches.urls')),
path('api/v1/audit/', include('apps.audit.urls')),
path('api/v1/settings/', include('apps.settings_app.urls')),
path('api/v1/rules/', include('apps.rules_engine.urls')),
path('api/v1/pricing/', include('apps.pricing.urls')),
path('api/v1/catalog/', include('apps.service_catalog.urls')),
path('api/v1/forms/', include('apps.forms_engine.urls')),
path('api/v1/workflows/', include('apps.workflows.urls')),
path('api/v1/members/', include('apps.members.urls')),
path('api/v1/spouses/', include('apps.spouses.urls')),
path('api/v1/children/', include('apps.children.urls')),
path('api/v1/temporary/', include('apps.temporary_members.urls')),
path('api/v1/seasonal/', include('apps.seasonal.urls')),
path('api/v1/sports/', include('apps.sports.urls')),
path('api/v1/honorary/', include('apps.honorary.urls')),
path('api/v1/foreign/', include('apps.foreign_members.urls')),
path('api/v1/payments/', include('apps.payments.urls')),
path('api/v1/receipts/', include('apps.receipts.urls')),
path('api/v1/installments/', include('apps.installments.urls')),
path('api/v1/subscriptions/', include('apps.subscriptions.urls')),
path('api/v1/fines/', include('apps.fines.urls')),
path('api/v1/transfers/', include('apps.transfers.urls')),
path('api/v1/divorce/', include('apps.divorce_cases.urls')),
path('api/v1/death/', include('apps.death_cases.urls')),
path('api/v1/waivers/', include('apps.waivers.urls')),
path('api/v1/interviews/', include('apps.interviews.urls')),
path('api/v1/carnets/', include('apps.carnets.urls')),
path('api/v1/documents/', include('apps.documents.urls')),
path('api/v1/notifications/', include('apps.notifications.urls')),
path('api/v1/reports/', include('apps.reports.urls')),
path('api/v1/dashboard/', include('apps.dashboard.urls')),
path('api/v1/archive/', include('apps.archive.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
application = get_wsgi_application()
\ No newline at end of file
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
\ No newline at end of file
# ============================================
# THE CLUB ERP — Base Requirements
# ============================================
# Django Core
Django==5.1
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
django-filter==24.3
django-cors-headers==4.4.0
# History & Audit
django-simple-history==3.7.0
django-auditlog==3.0.0
# Permissions
django-guardian==2.4.0
# State Machine
django-fsm==3.0.0
# Storage
django-storages==1.14.4
boto3==1.35.0
# Admin Enhancements
django-json-widget==2.0.1
django-import-export==4.1.1
# Celery
celery==5.4.0
celery[redis]==5.4.0
django-celery-beat==2.6.0
flower==2.0.1
# Cache / Broker
redis==5.0.8
# Database
psycopg[binary]==3.2.1
# PDF Generation
WeasyPrint==62.3
# Excel
openpyxl==3.1.5
# QR Code
qrcode==7.4.2
Pillow==10.4.0
# Utilities
python-dateutil==2.9.0
jsonschema==4.23.0
bcrypt==4.2.0
# ASGI / WebSockets
channels==4.1.0
channels-redis==4.2.0
uvicorn[standard]==0.30.6
# Static files
whitenoise==6.7.0
\ No newline at end of file
-r base.txt
# Debug
django-debug-toolbar==4.4.6
django-extensions==3.2.3
ipython==8.26.0
# Testing
pytest==8.3.2
pytest-django==4.8.0
pytest-cov==5.0.0
factory-boy==3.3.0
faker==28.0.0
\ No newline at end of file
-r base.txt
# Production Server
gunicorn==22.0.0
# Monitoring
sentry-sdk==2.13.0
\ No newline at end of file
/* THE CLUB ERP — Custom Admin Styles (RTL Arabic) */
body {
direction: rtl;
text-align: right;
font-family: 'Cairo', 'Segoe UI', Tahoma, sans-serif;
}
/* Header */
#header {
background: #0D7377 !important;
color: #fff;
}
/* Navigation */
.module h2, .module caption, .inline-group h2 {
background: #0D7377 !important;
color: #fff !important;
}
/* Links */
a:link, a:visited {
color: #0D7377;
}
/* Buttons */
.button, input[type=submit], input[type=button], .submit-row input {
background: #0D7377 !important;
border: none;
}
.button:hover, input[type=submit]:hover {
background: #095355 !important;
}
/* Breadcrumbs */
div.breadcrumbs {
background: #14A3A8 !important;
}
/* Table headers */
table thead th {
background: #0D7377 !important;
color: #fff !important;
}
/* Alternating rows */
table tbody tr:nth-child(even) {
background-color: #F9FAFB;
}
/* Selected rows */
table tbody tr.selected {
background-color: #E6F4F4 !important;
}
\ No newline at end of file
# Age milestone tasks — implemented in Phase 10
\ No newline at end of file
# Daily backup task — implemented in Phase 10
\ No newline at end of file
# Form expiry tasks — implemented in Phase 10
\ No newline at end of file
# Honorary expiry task — implemented in Phase 10
\ No newline at end of file
# Late fine tasks — implemented in Phase 10
\ No newline at end of file
# Membership dropper task — implemented in Phase 10
\ No newline at end of file
# Passport expiry task — implemented in Phase 10
\ No newline at end of file
"""
THE CLUB ERP — Celery Beat Schedule
=====================================
All automated background jobs.
"""
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'form-expiry-checker': {
'task': 'tasks.form_expiry.check_expired_forms',
'schedule': crontab(minute=0), # every hour
},
'age-milestone-checker': {
'task': 'tasks.age_milestones.check_age_milestones',
'schedule': crontab(hour=0, minute=30), # daily at 00:30
},
'male-child-auto-freeze': {
'task': 'tasks.age_milestones.freeze_males_at_25',
'schedule': crontab(hour=1, minute=0), # daily at 01:00
},
'temp-member-auto-remove': {
'task': 'tasks.age_milestones.remove_temp_at_25',
'schedule': crontab(hour=1, minute=30), # daily at 01:30
},
'spouse-reclassification': {
'task': 'tasks.spouse_reclassification.reclassify_spouses_at_21',
'schedule': crontab(hour=2, minute=0), # daily at 02:00
},
'honorary-expiry': {
'task': 'tasks.honorary_expiry.check_honorary_expiry',
'schedule': crontab(hour=3, minute=0), # daily at 03:00
},
'seasonal-expiry': {
'task': 'tasks.seasonal_expiry.check_seasonal_expiry',
'schedule': crontab(hour=3, minute=30), # daily at 03:30
},
'passport-expiry-alert': {
'task': 'tasks.passport_expiry.check_passport_expiry',
'schedule': crontab(hour=4, minute=0, day_of_week=1), # weekly Monday
},
'subscription-reminder': {
'task': 'tasks.subscription_reminders.send_reminders',
'schedule': crontab(hour=9, minute=0, day_of_month=1), # 1st of each month
},
'installment-reminder': {
'task': 'tasks.subscription_reminders.send_installment_reminders',
'schedule': crontab(hour=9, minute=30), # daily at 09:30
},
'late-fine-calculator': {
'task': 'tasks.late_fines.calculate_late_fines',
'schedule': crontab(hour=5, minute=0), # daily at 05:00
},
'membership-dropper': {
'task': 'tasks.membership_dropper.drop_memberships',
'schedule': crontab(hour=6, minute=0, day_of_month=1, month_of_year=7), # July 1st
},
'daily-backup': {
'task': 'tasks.daily_backup.run_backup',
'schedule': crontab(hour=2, minute=0), # daily at 02:00
},
}
\ No newline at end of file
# Seasonal expiry task — implemented in Phase 10
\ No newline at end of file
# Spouse reclassification task — implemented in Phase 10
\ No newline at end of file
# Subscription reminder tasks — implemented in Phase 10
\ No newline at end of file
{% extends "admin/base_site.html" %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" href="/static/admin/css/custom_admin.css">
<style>
/* RTL Support for Django Admin */
body {
direction: rtl;
font-family: 'Cairo', 'Segoe UI', Tahoma, sans-serif;
}
#header {
background: #0D7377;
}
#header h1, #header h1 a:link, #header h1 a:visited {
color: #fff;
}
.module h2, .module caption, .inline-group h2 {
background: #0D7377;
}
a:link, a:visited {
color: #0D7377;
}
.button, input[type=submit], input[type=button], .submit-row input {
background: #0D7377;
}
.button:hover, input[type=submit]:hover {
background: #095355;
}
div.breadcrumbs {
background: #14A3A8;
}
</style>
{% endblock %}
{% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">THE CLUB — لوحة التحكم</a>
</h1>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<style>
/* Report base styles — implemented in Phase 10 */
body { font-family: 'Cairo', sans-serif; direction: rtl; }
</style>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
\ No newline at end of file
{% extends "reports/base_report.html" %}
{% block content %}
<!-- Receipt template — implemented in Phase 10 -->
{% endblock %}
\ No newline at end of file
"""
THE CLUB ERP — Age Calculator
===============================
All age calculations use server time. Never trust client-side dates.
"""
import math
from datetime import date
from django.utils import timezone
from dateutil.relativedelta import relativedelta
def calculate_age(date_of_birth: date) -> int:
"""
Calculate age in full years from date_of_birth to current server date.
Returns 0 if date_of_birth is None.
"""
if not date_of_birth:
return 0
today = timezone.now().date()
age = today.year - date_of_birth.year
# Adjust if birthday hasn't occurred yet this year
if (today.month, today.day) < (date_of_birth.month, date_of_birth.day):
age -= 1
return max(age, 0)
def calculate_age_display(date_of_birth: date) -> str:
"""
Return age as 'XX سنة و YY شهر' string.
"""
if not date_of_birth:
return 'غير محدد'
today = timezone.now().date()
delta = relativedelta(today, date_of_birth)
years = delta.years
months = delta.months
if years == 0 and months == 0:
return 'أقل من شهر'
elif years == 0:
return f'{months} شهر'
elif months == 0:
return f'{years} سنة'
else:
return f'{years} سنة و {months} شهر'
def calculate_age_at_date(date_of_birth: date, at_date: date) -> int:
"""
Calculate age in full years at a specific date.
"""
if not date_of_birth or not at_date:
return 0
age = at_date.year - date_of_birth.year
if (at_date.month, at_date.day) < (date_of_birth.month, date_of_birth.day):
age -= 1
return max(age, 0)
def calculate_years_between(start: date, end: date) -> int:
"""
Calculate years between two dates, using CEILING (partial year = full year).
Used for spouse annual fee calculation.
"""
if not start or not end:
return 0
if end <= start:
return 0
delta = relativedelta(end, start)
years = delta.years
# If there are any remaining months or days, round UP
if delta.months > 0 or delta.days > 0:
years += 1
return max(years, 1) # Minimum 1 year
def months_between(start: date, end: date) -> int:
"""
Calculate total months between two dates.
"""
if not start or not end:
return 0
delta = relativedelta(end, start)
return delta.years * 12 + delta.months
def days_until_birthday(date_of_birth: date) -> int:
"""
Calculate days until next birthday.
"""
if not date_of_birth:
return -1
today = timezone.now().date()
next_birthday = date_of_birth.replace(year=today.year)
if next_birthday < today:
next_birthday = next_birthday.replace(year=today.year + 1)
return (next_birthday - today).days
\ No newline at end of file
"""
THE CLUB ERP — Arabic Text Utilities
=======================================
Helpers for Arabic text processing, RTL, and normalization.
"""
import re
def normalize_arabic(text: str) -> str:
"""
Normalize Arabic text by removing diacritics (tashkeel)
and normalizing alef variants.
"""
if not text:
return ''
# Remove tashkeel (diacritics)
text = re.sub(r'[\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED]', '', text)
# Normalize alef variants
text = re.sub(r'[إأآا]', 'ا', text)
# Normalize taa marbouta
text = text.replace('ة', 'ه')
return text.strip()
def is_arabic_text(text: str) -> bool:
"""
Check if text contains primarily Arabic characters.
"""
if not text:
return False
arabic_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]')
arabic_chars = len(arabic_pattern.findall(text))
total_alpha = sum(1 for c in text if c.isalpha())
if total_alpha == 0:
return False
return (arabic_chars / total_alpha) > 0.5
def arabic_number(number) -> str:
"""
Convert Western Arabic numerals to Eastern Arabic numerals for display.
0123456789 → ٠١٢٣٤٥٦٧٨٩
"""
eastern = '٠١٢٣٤٥٦٧٨٩'
western = '0123456789'
table = str.maketrans(western, eastern)
return str(number).translate(table)
def format_arabic_currency(amount, currency='ج.م') -> str:
"""
Format a number as Arabic currency.
Example: 150000 → '١٥٠,٠٠٠ ج.م'
"""
from utils.decimal_utils import money
formatted = f'{money(amount):,.2f}'
return f'{formatted} {currency}'
\ No newline at end of file
"""
THE CLUB ERP — System Constants
=================================
All dropdown values, lookup tables, and system-wide constants.
"""
# ─── Egyptian Governorates ────────────────────────────
GOVERNORATES = [
('cairo', 'القاهرة', 'Cairo'),
('alexandria', 'الإسكندرية', 'Alexandria'),
('port_said', 'بورسعيد', 'Port Said'),
('suez', 'السويس', 'Suez'),
('damietta', 'دمياط', 'Damietta'),
('dakahlia', 'الدقهلية', 'Dakahlia'),
('sharqia', 'الشرقية', 'Sharqia'),
('qalyubia', 'القليوبية', 'Qalyubia'),
('kafr_el_sheikh', 'كفر الشيخ', 'Kafr El Sheikh'),
('gharbia', 'الغربية', 'Gharbia'),
('menoufia', 'المنوفية', 'Menoufia'),
('beheira', 'البحيرة', 'Beheira'),
('ismailia', 'الإسماعيلية', 'Ismailia'),
('giza', 'الجيزة', 'Giza'),
('beni_suef', 'بني سويف', 'Beni Suef'),
('fayoum', 'الفيوم', 'Fayoum'),
('minya', 'المنيا', 'Minya'),
('assiut', 'أسيوط', 'Assiut'),
('sohag', 'سوهاج', 'Sohag'),
('qena', 'قنا', 'Qena'),
('aswan', 'أسوان', 'Aswan'),
('luxor', 'الأقصر', 'Luxor'),
('red_sea', 'البحر الأحمر', 'Red Sea'),
('new_valley', 'الوادي الجديد', 'New Valley'),
('matrouh', 'مطروح', 'Matrouh'),
('north_sinai', 'شمال سيناء', 'North Sinai'),
('south_sinai', 'جنوب سيناء', 'South Sinai'),
]
GOVERNORATE_CHOICES = [(g[0], g[1]) for g in GOVERNORATES]
# ─── Religions ────────────────────────────────────────
RELIGIONS = [
('MUSLIM', 'مسلم'),
('CHRISTIAN', 'مسيحي'),
('OTHER', 'أخرى'),
]
# ─── Qualifications ───────────────────────────────────
QUALIFICATIONS = [
('HIGH', 'مؤهل عالي'),
('MEDIUM', 'مؤهل متوسط'),
('NONE', 'بدون مؤهل'),
]
# ─── Gender ───────────────────────────────────────────
GENDERS = [
('MALE', 'ذكر'),
('FEMALE', 'أنثى'),
]
# ─── Marital Status ───────────────────────────────────
MARITAL_STATUSES = [
('SINGLE', 'أعزب'),
('MARRIED', 'متزوج'),
('DIVORCED', 'مطلق'),
('WIDOWED', 'أرمل'),
]
# ─── Residence Types ──────────────────────────────────
RESIDENCE_TYPES = [
('RENTED', 'إيجار'),
('OWNED', 'تمليك'),
('OTHER', 'أخرى'),
]
# ─── Employment Types ─────────────────────────────────
EMPLOYMENT_TYPES = [
('EMPLOYED', 'موظف'),
('SELF_EMPLOYED', 'أعمال حرة'),
('PROFESSIONS', 'مهن حرة'),
('OTHER', 'أخرى'),
]
# ─── ID Types ─────────────────────────────────────────
ID_TYPES = [
('NATIONAL_ID', 'بطاقة رقم قومي'),
('PASSPORT', 'جواز سفر'),
('MILITARY_ID', 'بطاقة عسكرية'),
]
# ─── Correspondence Address Types ─────────────────────
CORRESPONDENCE_TYPES = [
('WORK', 'العمل'),
('RESIDENCE', 'السكن'),
('OTHER', 'أخرى'),
]
# ─── Referral Sources ─────────────────────────────────
REFERRAL_SOURCES = [
('social_media', 'مواقع التواصل الاجتماعي'),
('tv', 'إعلان التليفزيون'),
('friend', 'من خلال صديق'),
('radio', 'إعلان الراديو'),
('outdoor', 'إعلانات الطريق'),
]
# ─── Membership Types ─────────────────────────────────
MEMBERSHIP_TYPES = [
('WORKING', 'عضو عامل'),
('DEPENDENT', 'عضو تابع'),
('TEMPORARY', 'عضو مؤقت'),
('SEASONAL', 'عضو موسمي'),
('SPORTS', 'عضو رياضي'),
('HONORARY', 'عضو شرفي'),
('FOREIGN', 'عضو أجنبي'),
]
# ─── Member Status ────────────────────────────────────
MEMBER_STATUSES = [
('POTENTIAL', 'عضوية محتملة'),
('POTENTIAL_REJECTED', 'عضوية محتملة — مرفوض'),
('PAYMENT_PENDING', 'في انتظار السداد'),
('ACTIVE', 'نشط'),
('FROZEN', 'مجمد'),
('SUSPENDED', 'موقوف'),
('BANNED', 'ممنوع'),
('DROPPED', 'مسقط'),
('TERMINATED', 'مفصول'),
('TRANSFERRED', 'محول'),
('DECEASED', 'متوفى'),
]
# ─── Member Categories ────────────────────────────────
MEMBER_CATEGORIES = [
('BASE', 'أساس عضوية'),
('ACQUIRED', 'مكتسب عضوية'),
]
# ─── Payment Types ────────────────────────────────────
PAYMENT_TYPES = [
('MEMBERSHIP_FEE', 'رسوم عضوية'),
('FORM_FEE', 'رسوم استمارة'),
('ADDITION_FEE', 'رسوم إضافة'),
('ANNUAL_SUBSCRIPTION', 'اشتراك سنوي'),
('FINE', 'غرامة'),
('PENALTY', 'جزاء'),
('INSTALLMENT', 'قسط'),
('DOWN_PAYMENT', 'مقدم'),
('CARNET_REPLACEMENT', 'بدل فاقد كارنيه'),
('SEASONAL_FEE', 'رسوم موسمية'),
('WAIVER_FEE', 'رسوم تنازل'),
('TRANSFER_FEE', 'رسوم تحويل'),
('OTHER', 'أخرى'),
]
# ─── Payment Methods ──────────────────────────────────
PAYMENT_METHODS = [
('CASH', 'نقدي'),
('CHECK', 'شيك'),
('VISA', 'فيزا'),
('BANK_TRANSFER', 'تحويل بنكي'),
]
# ─── Countries (Top used + comprehensive) ─────────────
COUNTRIES = [
('EG', 'مصر', 'Egypt'),
('SA', 'المملكة العربية السعودية', 'Saudi Arabia'),
('AE', 'الإمارات العربية المتحدة', 'UAE'),
('KW', 'الكويت', 'Kuwait'),
('QA', 'قطر', 'Qatar'),
('BH', 'البحرين', 'Bahrain'),
('OM', 'عمان', 'Oman'),
('JO', 'الأردن', 'Jordan'),
('LB', 'لبنان', 'Lebanon'),
('SY', 'سوريا', 'Syria'),
('IQ', 'العراق', 'Iraq'),
('PS', 'فلسطين', 'Palestine'),
('LY', 'ليبيا', 'Libya'),
('TN', 'تونس', 'Tunisia'),
('DZ', 'الجزائر', 'Algeria'),
('MA', 'المغرب', 'Morocco'),
('SD', 'السودان', 'Sudan'),
('YE', 'اليمن', 'Yemen'),
('US', 'الولايات المتحدة', 'United States'),
('GB', 'المملكة المتحدة', 'United Kingdom'),
('FR', 'فرنسا', 'France'),
('DE', 'ألمانيا', 'Germany'),
('IT', 'إيطاليا', 'Italy'),
('ES', 'إسبانيا', 'Spain'),
('CA', 'كندا', 'Canada'),
('AU', 'أستراليا', 'Australia'),
('TR', 'تركيا', 'Turkey'),
('IN', 'الهند', 'India'),
('CN', 'الصين', 'China'),
('JP', 'اليابان', 'Japan'),
('KR', 'كوريا الجنوبية', 'South Korea'),
('BR', 'البرازيل', 'Brazil'),
('RU', 'روسيا', 'Russia'),
('ZA', 'جنوب أفريقيا', 'South Africa'),
('NG', 'نيجيريا', 'Nigeria'),
('OTHER', 'أخرى', 'Other'),
]
COUNTRY_CHOICES = [(c[0], c[1]) for c in COUNTRIES]
NATIONALITY_CHOICES = COUNTRY_CHOICES
\ No newline at end of file
"""
THE CLUB ERP — Date Utilities
================================
Financial year: July 1 to June 30.
All date logic is centralized here.
"""
from datetime import date
from django.utils import timezone
def current_financial_year() -> str:
"""
Returns the current financial year as 'YYYY/YYYY'.
Financial year starts July 1.
If month >= 7: year/year+1
If month < 7: year-1/year
"""
today = timezone.now().date()
if today.month >= 7:
return f'{today.year}/{today.year + 1}'
else:
return f'{today.year - 1}/{today.year}'
def financial_year_start(year_str: str) -> date:
"""
Returns July 1 of the start year.
Input: '2024/2025' → date(2024, 7, 1)
"""
start_year = int(year_str.split('/')[0])
return date(start_year, 7, 1)
def financial_year_end(year_str: str) -> date:
"""
Returns June 30 of the end year.
Input: '2024/2025' → date(2025, 6, 30)
"""
end_year = int(year_str.split('/')[1])
return date(end_year, 6, 30)
def is_within_collection_window() -> bool:
"""
Returns True if current month is July, August, or September.
Collection window: 3 months from July 1.
"""
return timezone.now().month in (7, 8, 9)
def remaining_months_in_financial_year() -> int:
"""
From current date to June 30 of the current financial year.
"""
today = timezone.now().date()
fy = current_financial_year()
end = financial_year_end(fy)
if today >= end:
return 0
months = (end.year - today.year) * 12 + (end.month - today.month)
return max(months, 0)
def is_late_payment_period() -> bool:
"""
Returns True if current date is after October 1 of the current financial year.
Late payment fines start from October 1.
"""
today = timezone.now().date()
fy = current_financial_year()
start_year = int(fy.split('/')[0])
october_first = date(start_year, 10, 1)
return today >= october_first
def parse_financial_year(year_str: str) -> tuple:
"""
Parse '2024/2025' into (2024, 2025).
"""
parts = year_str.split('/')
return int(parts[0]), int(parts[1])
\ No newline at end of file
"""
THE CLUB ERP — Decimal Utilities
==================================
All money operations use Decimal. Never float. Never.
"""
from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
def money(value) -> Decimal:
"""
Convert any input to Decimal with 2 decimal places, rounded HALF_UP.
"""
if value is None:
return Decimal('0.00')
try:
if isinstance(value, float):
# Convert float to string first to avoid floating point issues
d = Decimal(str(value))
elif isinstance(value, Decimal):
d = value
else:
d = Decimal(str(value))
except (InvalidOperation, ValueError):
return Decimal('0.00')
return d.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
def percentage_of(base: Decimal, pct: Decimal) -> Decimal:
"""
Calculate percentage of a base amount.
Returns base * (pct / 100), rounded to 2 decimal places.
"""
base = money(base)
pct = Decimal(str(pct))
result = base * (pct / Decimal('100'))
return result.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
def sum_money(*amounts) -> Decimal:
"""
Sum multiple money amounts safely.
"""
total = Decimal('0.00')
for amount in amounts:
total += money(amount)
return total.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
def is_zero(amount) -> bool:
"""Check if a money amount is zero."""
return money(amount) == Decimal('0.00')
\ No newline at end of file
"""
THE CLUB ERP — Base Model Mixin
================================
Every single model in the system MUST inherit from TimeStampedModel.
"""
from django.db import models
from django.conf import settings
class TimeStampedModel(models.Model):
"""Abstract base model with timestamps and audit fields."""
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاريخ الإنشاء")
updated_at = models.DateTimeField(auto_now=True, verbose_name="تاريخ التعديل")
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="%(class)s_created",
verbose_name="أنشأ بواسطة"
)
updated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="%(class)s_updated",
verbose_name="عدّل بواسطة"
)
class Meta:
abstract = True
\ No newline at end of file
"""
THE CLUB ERP — Egyptian National ID Parser
============================================
Parses 14-digit Egyptian National IDs to extract:
- Date of birth, Gender, Governorate, Check digit validation
Reference: Master Document Section 2
"""
import calendar
from datetime import date
from typing import Optional
GOVERNORATE_MAP = {
'01': ('القاهرة', 'Cairo'),
'02': ('الإسكندرية', 'Alexandria'),
'03': ('بورسعيد', 'Port Said'),
'04': ('السويس', 'Suez'),
'11': ('دمياط', 'Damietta'),
'12': ('الدقهلية', 'Dakahlia'),
'13': ('الشرقية', 'Sharqia'),
'14': ('القليوبية', 'Qalyubia'),
'15': ('كفر الشيخ', 'Kafr El Sheikh'),
'16': ('الغربية', 'Gharbia'),
'17': ('المنوفية', 'Menoufia'),
'18': ('البحيرة', 'Beheira'),
'19': ('الإسماعيلية', 'Ismailia'),
'21': ('الجيزة', 'Giza'),
'22': ('بني سويف', 'Beni Suef'),
'23': ('الفيوم', 'Fayoum'),
'24': ('المنيا', 'Minya'),
'25': ('أسيوط', 'Assiut'),
'26': ('سوهاج', 'Sohag'),
'27': ('قنا', 'Qena'),
'28': ('أسوان', 'Aswan'),
'29': ('الأقصر', 'Luxor'),
'31': ('البحر الأحمر', 'Red Sea'),
'32': ('الوادي الجديد', 'New Valley'),
'33': ('مطروح', 'Matrouh'),
'34': ('شمال سيناء', 'North Sinai'),
'35': ('جنوب سيناء', 'South Sinai'),
'88': ('خارج الجمهورية', 'Born Abroad'),
}
def validate_check_digit(nid: str) -> bool:
"""
Validate Egyptian National ID check digit using the weighted sum algorithm.
Positions 0-12 are data digits, position 13 is the check digit.
"""
if len(nid) != 14 or not nid.isdigit():
return False
weights = [2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
total = 0
for i in range(13):
product = int(nid[i]) * weights[i]
if product >= 10:
product = (product // 10) + (product % 10)
total += product
check = (10 - (total % 10)) % 10
return check == int(nid[13])
def parse_national_id(nid: str) -> dict:
"""
Parse a 14-digit Egyptian National ID and extract all encoded information.
Returns a dict with:
valid: bool
date_of_birth: date or None
gender: 'MALE' or 'FEMALE' or None
governorate_code: str or None
governorate_name_ar: str or None
governorate_name_en: str or None
century: int or None
error: str or None
"""
result = {
'valid': False,
'date_of_birth': None,
'gender': None,
'governorate_code': None,
'governorate_name_ar': None,
'governorate_name_en': None,
'century': None,
'error': None,
}
# Step 1: Basic format validation
if not nid or not isinstance(nid, str):
result['error'] = 'الرقم القومي مطلوب'
return result
nid = nid.strip()
if len(nid) != 14:
result['error'] = 'الرقم القومي يجب أن يكون 14 رقم'
return result
if not nid.isdigit():
result['error'] = 'الرقم القومي يجب أن يحتوي على أرقام فقط'
return result
# Step 2: Century code
century_code = int(nid[0])
if century_code not in (2, 3):
result['error'] = 'رمز القرن غير صحيح (يجب أن يكون 2 أو 3)'
return result
century_base = 1900 if century_code == 2 else 2000
result['century'] = century_code
# Step 3: Extract date components
year = int(nid[1:3])
month = int(nid[3:5])
day = int(nid[5:7])
full_year = century_base + year
# Step 4: Validate month
if month < 1 or month > 12:
result['error'] = f'الشهر غير صحيح: {month}'
return result
# Step 5: Validate day for the given month/year
max_day = calendar.monthrange(full_year, month)[1]
if day < 1 or day > max_day:
result['error'] = f'اليوم غير صحيح: {day} لشهر {month}/{full_year}'
return result
# Step 6: Construct date of birth
try:
dob = date(full_year, month, day)
except ValueError as e:
result['error'] = f'تاريخ الميلاد غير صحيح: {str(e)}'
return result
# Sanity check: DOB should not be in the future
if dob > date.today():
result['error'] = 'تاريخ الميلاد لا يمكن أن يكون في المستقبل'
return result
result['date_of_birth'] = dob
# Step 7: Governorate
gov_code = nid[7:9]
if gov_code in GOVERNORATE_MAP:
result['governorate_code'] = gov_code
result['governorate_name_ar'] = GOVERNORATE_MAP[gov_code][0]
result['governorate_name_en'] = GOVERNORATE_MAP[gov_code][1]
else:
result['error'] = f'كود المحافظة غير معروف: {gov_code}'
return result
# Step 8: Gender from sequence number (positions 9-12)
sequence = int(nid[9:13])
result['gender'] = 'MALE' if sequence % 2 == 1 else 'FEMALE'
# Step 9: Check digit validation
if not validate_check_digit(nid):
result['error'] = 'رقم التحقق غير صحيح'
return result
# All validations passed
result['valid'] = True
return result
def extract_gender_from_nid(nid: str) -> Optional[str]:
"""Quick gender extraction without full validation."""
if len(nid) == 14 and nid.isdigit():
return 'MALE' if int(nid[9:13]) % 2 == 1 else 'FEMALE'
return None
def extract_dob_from_nid(nid: str) -> Optional[date]:
"""Quick DOB extraction without full validation."""
parsed = parse_national_id(nid)
return parsed.get('date_of_birth')
\ No newline at end of file
"""
THE CLUB ERP — Field Validators
=================================
Reusable validators for model fields and serializers.
"""
import re
from django.core.exceptions import ValidationError
from utils.national_id import parse_national_id
def validate_egyptian_national_id(value: str) -> None:
"""Validate an Egyptian National ID (14 digits with check digit)."""
if not value:
return
result = parse_national_id(value)
if not result['valid']:
raise ValidationError(
result.get('error', 'الرقم القومي غير صحيح'),
code='invalid_national_id'
)
def validate_egyptian_mobile(value: str) -> None:
"""Validate an Egyptian mobile number (11 digits starting with 01)."""
if not value:
return
cleaned = re.sub(r'[\s\-\(\)]', '', value)
if not re.match(r'^01[0-9]{9}$', cleaned):
raise ValidationError(
'رقم المحمول يجب أن يكون 11 رقم ويبدأ بـ 01',
code='invalid_mobile'
)
def validate_passport_number(value: str) -> None:
"""Validate a passport number (5-20 alphanumeric characters)."""
if not value:
return
if not re.match(r'^[A-Za-z0-9]{5,20}$', value):
raise ValidationError(
'رقم جواز السفر يجب أن يكون من 5 إلى 20 حرف أو رقم',
code='invalid_passport'
)
def validate_arabic_text(value: str) -> None:
"""Validate that text contains only Arabic characters, spaces, and common Arabic punctuation."""
if not value:
return
# Allow: Arabic chars, spaces, common punctuation, numbers
pattern = r'^[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\s\.\,\،\؟\!\(\)\-\d]+$'
if not re.match(pattern, value):
raise ValidationError(
'النص يجب أن يحتوي على حروف عربية فقط',
code='invalid_arabic'
)
def validate_phone_number(value: str) -> None:
"""Validate a general phone number."""
if not value:
return
cleaned = re.sub(r'[\s\-\(\)\+]', '', value)
if not cleaned.isdigit() or len(cleaned) < 7 or len(cleaned) > 15:
raise ValidationError(
'رقم الهاتف غير صحيح',
code='invalid_phone'
)
\ No newline at end of file
{
"schemaVersion": 2,
"dockerComposeFileLocation": "./docker-compose.prod.yml"
}
\ No newline at end of file
version: '3.9'
services:
db:
restart: always
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data_prod:/var/lib/postgresql/data
ports: []
redis:
restart: always
ports: []
minio:
restart: always
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
volumes:
- minio_data_prod:/data
ports: []
backend:
restart: always
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4 --threads 2 --timeout 120
volumes: []
ports:
- "8000:8000"
environment:
- DJANGO_SETTINGS_MODULE=config.settings.prod
celery_worker:
restart: always
command: celery -A config worker -l warning -Q default,celery --concurrency=4
volumes: []
celery_beat:
restart: always
volumes: []
flower:
restart: always
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./backend/static:/var/www/static:ro
depends_on:
- backend
- frontend
restart: always
volumes:
postgres_data_prod:
minio_data_prod:
\ No newline at end of file
version: '3.9'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: theclub
POSTGRES_USER: theclub
POSTGRES_PASSWORD: ${DB_PASSWORD:-theclub_secret_2024}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U theclub"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin123}
volumes:
- minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 30s
timeout: 20s
retries: 3
backend:
build:
context: .
dockerfile: Dockerfile
command: >
sh -c "python manage.py migrate --noinput &&
python manage.py runserver 0.0.0.0:8000"
volumes:
- ./backend:/app
ports:
- "8000:8000"
env_file:
- .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_started
celery_worker:
build:
context: .
dockerfile: Dockerfile.celery
command: celery -A config worker -l info -Q default,celery
volumes:
- ./backend:/app
env_file:
- .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
celery_beat:
build:
context: .
dockerfile: Dockerfile.celery
command: celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
volumes:
- ./backend:/app
env_file:
- .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
flower:
build:
context: .
dockerfile: Dockerfile.celery
command: celery -A config flower --port=5555
ports:
- "5555:5555"
env_file:
- .env
depends_on:
redis:
condition: service_healthy
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
command: npm run dev
volumes:
- ./frontend:/app
- /app/node_modules
- /app/.next
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
volumes:
postgres_data:
minio_data:
\ No newline at end of file
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
\ No newline at end of file
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
};
module.exports = nextConfig;
\ No newline at end of file
{
"name": "theclub-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.5.4",
"antd": "^5.20.0",
"@ant-design/icons": "^5.4.0",
"zustand": "^4.5.4",
"@tanstack/react-query": "^5.51.21",
"axios": "^1.7.3",
"next-intl": "^3.17.2",
"react-hook-form": "^7.52.1",
"zod": "^3.23.8",
"@hookform/resolvers": "^3.9.0",
"dayjs": "^1.11.12",
"recharts": "^2.12.7",
"qrcode.react": "^3.1.0"
},
"devDependencies": {
"@types/node": "^20.14.14",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.5",
"tailwindcss": "^3.4.7",
"postcss": "^8.4.41",
"autoprefixer": "^10.4.20"
}
}
\ No newline at end of file
@tailwind base;
@tailwind components;
@tailwind utilities;
/* THE CLUB ERP — Global Styles */
:root {
--color-primary: #0D7377;
--color-primary-light: #14A3A8;
--color-primary-dark: #095355;
--color-bg: #FFFFFF;
--color-surface: #F5F7FA;
--color-text-primary: #1A1A2E;
--color-text-secondary: #6B7280;
--color-border: #E5E7EB;
--color-success: #059669;
--color-warning: #D97706;
--color-error: #DC2626;
--color-info: #0284C7;
}
html {
direction: rtl;
}
body {
font-family: 'Cairo', 'Tajawal', 'Segoe UI', Tahoma, sans-serif;
background-color: var(--color-surface);
color: var(--color-text-primary);
}
\ No newline at end of file
export default function Home() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
direction: 'rtl',
fontFamily: 'Cairo, sans-serif'
}}>
<div style={{ textAlign: 'center' }}>
<h1 style={{ fontSize: '3rem', color: '#0D7377' }}>THE CLUB</h1>
<p style={{ fontSize: '1.5rem', color: '#6B7280' }}>نظام إدارة النادي</p>
<p style={{ marginTop: '2rem', color: '#6B7280' }}>
Phase 1 Complete — Scaffold Ready
</p>
</div>
</div>
);
}
\ No newline at end of file
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
\ No newline at end of file
upstream backend {
server backend:8000;
}
upstream frontend {
server frontend:3000;
}
server {
listen 80;
server_name _;
client_max_body_size 20M;
# Static files
location /static/ {
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Django Admin & API
location /admin/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Frontend
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment