Spaces:
Sleeping
Sleeping
| """ | |
| ================================================================================ | |
| LEAVE FLOW — Database Models & Engine Configuration | |
| ================================================================================ | |
| PURPOSE: | |
| Defines all SQLAlchemy ORM models and the database engine connection. | |
| This is the single source of truth for the database schema. | |
| SYSTEM DESIGN: | |
| Uses SQLite by default (zero-config for development) with optional PostgreSQL | |
| support via DATABASE_URL env var. SQLAlchemy handles the ORM mapping, | |
| so all data access in the app goes through these model classes. | |
| TABLES: | |
| employees → Employee accounts & profile data | |
| leave_records → Leave applications, approvals, cancellations | |
| notifications → In-app notification messages | |
| holidays → Company holiday calendar | |
| CALLED BY: | |
| - backend/main.py → Creates tables via Base.metadata.create_all() | |
| - backend/auth.py → Queries Employee for authentication | |
| - backend/seed.py → Inserts seed data | |
| - backend/routers/*.py → All API endpoints use get_db() for sessions | |
| - ai/agents/tools.py → All LangGraph tool functions query via Session | |
| - ai/engine/rag.py → (Indirectly through tools) | |
| RELATIONSHIPS: | |
| Employee (1) ──→ (N) LeaveRecord (employee_id foreign key) | |
| Employee (1) ──→ (N) Notification (user_id) | |
| No foreign key constraints (SQLite-friendly, app-enforced logical FKs) | |
| LEAVE BALANCE JSON STRUCTURE (stored on Employee): | |
| { | |
| "sick": {"taken": N, "limit": 12}, | |
| "casual": {"taken": N, "limit": 24}, | |
| "business": {"taken": N, "limit": 20}, | |
| "emergency":{"taken": N, "limit": 10}, | |
| "family": {"taken": N, "limit": 10}, | |
| "paid": {"taken": N, "limit": 100}, | |
| "unpaid": {"taken": N, "limit": 999}, | |
| "totalAccrued": 0, | |
| "totalTaken": 0 | |
| } | |
| NOTE: | |
| - leave_balance uses JSON column for flexibility (no separate balance table) | |
| - Documents (employee docs, leave attachments) are stored as base64 Text | |
| - Retry logic at the bottom handles race conditions during table creation | |
| ================================================================================ | |
| """ | |
| import os | |
| from dotenv import load_dotenv | |
| load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env")) | |
| from sqlalchemy import create_engine, Column, String, JSON, DateTime, Boolean, Text | |
| from sqlalchemy.orm import declarative_base, sessionmaker | |
| from sqlalchemy.ext.hybrid import hybrid_property | |
| from datetime import datetime | |
| import uuid | |
| load_dotenv() | |
| # ---- Database Connection ---- | |
| # Supports SQLite (default) and PostgreSQL | |
| # SQLite uses ./leaveflow.db relative to CWD | |
| PORT = os.getenv("PORT", "8000") | |
| DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///./leaveflow_{PORT}.db") | |
| connect_args = {} | |
| if DATABASE_URL.startswith("sqlite"): | |
| connect_args["check_same_thread"] = False | |
| engine = create_engine(DATABASE_URL, connect_args=connect_args) | |
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
| Base = declarative_base() | |
| def _parse_date_value(value: str | None): | |
| """Parse a stored date string in either YYYY-MM-DD or DD-MM-YYYY format.""" | |
| if not value: | |
| return None | |
| raw = str(value).strip()[:10] | |
| for fmt in ("%Y-%m-%d", "%d-%m-%Y"): | |
| try: | |
| return datetime.strptime(raw, fmt) | |
| except ValueError: | |
| pass | |
| return None | |
| def generate_id(prefix="L"): | |
| """ | |
| Generates unique IDs in format: {prefix}-{8 hex chars} | |
| Examples: L-4e4f1fe8, N-a3b2c1d0, H-ff001122 | |
| Used across all tables for primary keys. | |
| """ | |
| return f"{prefix}-{uuid.uuid4().hex[:8]}" | |
| def get_db(): | |
| """ | |
| FastAPI dependency that yields a database session. | |
| Automatically closes the session after the request completes. | |
| Usage in routes: | |
| async def my_endpoint(db: Session = Depends(get_db)): | |
| """ | |
| db = SessionLocal() | |
| try: | |
| yield db | |
| finally: | |
| db.close() | |
| # ===================================================================== | |
| # EMPLOYEE MODEL | |
| # ===================================================================== | |
| # Stores: | |
| # - Authentication credentials (hashed password + plain for creds display) | |
| # - Profile info (name, email, phone, DOJ, designation, gender) | |
| # - HR document (base64 PDF/upload) | |
| # - Project tag for manager grouping | |
| # - Leave balance as JSON blob | |
| # ===================================================================== | |
| class Employee(Base): | |
| __tablename__ = "employees" | |
| id = Column(String, primary_key=True, index=True) | |
| name = Column(String, nullable=False) | |
| email = Column(String, unique=True, nullable=False, index=True) | |
| password = Column(String, nullable=False) # bcrypt-hashed | |
| plain_password = Column(String, nullable=True) # shown once after creation | |
| role = Column(String, nullable=False, default="employee") # employee | manager | hr | |
| phone = Column(String, default="") | |
| dob = Column(String, default="") # Date of birth (DD-MM-YYYY) | |
| doj = Column(String, default="") # Date of joining (DD-MM-YYYY) | |
| address = Column(String, default="") | |
| nationality = Column(String, default="") | |
| designation = Column(String, default="") | |
| project_tag = Column(String, nullable=True) # Ties employee to a project | |
| manager_id = Column(String, nullable=True) # Logical FK to manager Employee.id | |
| gender = Column(String, default="") | |
| document = Column(Text, nullable=True) # base64 encoded document | |
| leave_balance = Column(JSON, default=lambda: { | |
| "sick": {"taken": 0, "limit": 12}, | |
| "casual": {"taken": 0, "limit": 24}, | |
| "business": {"taken": 0, "limit": 20}, | |
| "emergency": {"taken": 0, "limit": 10}, | |
| "family": {"taken": 0, "limit": 10}, | |
| "paid": {"taken": 0, "limit": 100}, | |
| "unpaid": {"taken": 0, "limit": 999}, | |
| "totalAccrued": 0, | |
| "totalTaken": 0, | |
| }) | |
| created_at = Column(DateTime, default=datetime.utcnow) | |
| def is_tagged(self): | |
| """Backwards-compatible helper for older code paths.""" | |
| return bool(self.project_tag) | |
| def is_tagged(cls): | |
| return cls.project_tag.isnot(None) | |
| # ===================================================================== | |
| # LEAVE RECORD MODEL | |
| # ===================================================================== | |
| # Tracks every leave application through its lifecycle: | |
| # pending → approved (or rejected) → cancellation_requested → approved/rejected | |
| # Stores reason, attached documents, and audit trail (applied_on, approved_by) | |
| # ===================================================================== | |
| class LeaveRecord(Base): | |
| __tablename__ = "leave_records" | |
| id = Column(String, primary_key=True, index=True, default=lambda: generate_id("L")) | |
| employee_id = Column(String, nullable=False, index=True) | |
| employee_name = Column(String, nullable=False) | |
| type = Column(String, nullable=False) # sick | casual | business | emergency | family | unpaid | |
| start_date = Column(String, nullable=False) # DD-MM-YYYY or YYYY-MM-DD | |
| end_date = Column(String, nullable=False) | |
| reason = Column(Text, default="") | |
| document = Column(Text, nullable=True) # base64 attachment for this leave | |
| status = Column(String, nullable=False, default="pending") | |
| applied_on = Column(String, default=lambda: datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")) | |
| approved_by = Column(String, nullable=True) | |
| cancellation_reason = Column(Text, nullable=True) | |
| rejection_reason = Column(Text, nullable=True) | |
| notified_manager = Column(Boolean, default=False) | |
| def leave_type(self): | |
| """Backwards-compatible alias for the older schema.""" | |
| return self.type | |
| def leave_type(cls): | |
| return cls.type | |
| def no_of_days(self): | |
| """Compute leave duration from the stored start/end dates.""" | |
| start = _parse_date_value(self.start_date) | |
| end = _parse_date_value(self.end_date) | |
| if start and end: | |
| return max(1, (end - start).days + 1) | |
| return 1 | |
| def is_auto_approved(self): | |
| return self.status == "auto-approved" | |
| def is_auto_approved(cls): | |
| return cls.status == "auto-approved" | |
| def cancellation_requested(self): | |
| return self.status == "cancellation_requested" | |
| def cancellation_requested(cls): | |
| return cls.status == "cancellation_requested" | |
| def cancellation_status(self): | |
| if self.status == "cancellation_requested": | |
| return "Pending review" | |
| if self.status == "approved": | |
| return "Approved" | |
| if self.status == "rejected": | |
| return "Rejected" | |
| return None | |
| # ===================================================================== | |
| # NOTIFICATION MODEL | |
| # ===================================================================== | |
| # In-app notifications for leave approvals, rejections, cancellations. | |
| # Supports multiple types: leave_approved, leave_rejected, etc. | |
| # Email notifications are sent via email_service.py (separate). | |
| # ===================================================================== | |
| class Notification(Base): | |
| __tablename__ = "notifications" | |
| id = Column(String, primary_key=True, index=True, default=lambda: generate_id("N")) | |
| user_id = Column(String, nullable=False, index=True) | |
| title = Column(String, nullable=False) | |
| message = Column(Text, default="") | |
| type = Column(String, default="in-app") | |
| to = Column(String, nullable=True) # recipient role/email | |
| email = Column(String, nullable=True) | |
| read = Column(Boolean, default=False) | |
| created_at = Column(String, default=lambda: datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")) | |
| # ===================================================================== | |
| # HOLIDAY MODEL | |
| # ===================================================================== | |
| # Company holidays used for calendar display and leave calculations. | |
| # Managed by HR through the calendar modal in the UI. | |
| # ===================================================================== | |
| class Holiday(Base): | |
| __tablename__ = "holidays" | |
| id = Column(String, primary_key=True, index=True, default=lambda: generate_id("H")) | |
| date = Column(String, nullable=False) # YYYY-MM-DD | |
| name = Column(String, nullable=False) # Holiday name | |
| # ---- Table Creation with Retry ---- | |
| # In concurrent startup scenarios (multiple server instances), | |
| # SQLite can throw "database is locked". Retry 5 times with backoff. | |
| import time | |
| for attempt in range(5): | |
| try: | |
| Base.metadata.create_all(bind=engine) | |
| break | |
| except Exception: | |
| if attempt < 4: | |
| time.sleep(attempt + 1) | |
| else: | |
| raise | |