Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added modules/elm/__init__.py
Empty file.
Empty file added modules/elm/api/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions modules/elm/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""URL patterns for Employee Leave Management."""

from django.urls import path

from modules.elm.api.views import (
DecideLeaveApplicationView,
MyLeaveApplicationsView,
SubmitLeaveApplicationView,
)

app_name = "elm"

urlpatterns = [
path(
"my-leaves/",
MyLeaveApplicationsView.as_view(),
name="my-leaves",
),
path(
"apply/",
SubmitLeaveApplicationView.as_view(),
name="apply",
),
path(
"applications/<uuid:application_id>/decision/",
DecideLeaveApplicationView.as_view(),
name="decide-application",
),
]
138 changes: 138 additions & 0 deletions modules/elm/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""API views for Employee Leave Management."""

from datetime import date
from uuid import UUID

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from modules.elm.models import LeaveApplication
from modules.elm.services.applications import (
LeaveApplicationError,
decide_leave,
submit_leave,
)
from modules.elm.services.authz import require


def serialize_application(application):
"""Convert a leave application to a JSON-compatible dictionary."""
return {
"id": str(application.id),
"employee_id": application.employee_id,
"leave_type": application.leave_type,
"start_date": application.start_date.isoformat(),
"end_date": application.end_date.isoformat(),
"reason": application.reason,
"status": application.status,
"approver_id": application.approver_id,
"decision_note": application.decision_note,
"submitted_at": (
application.submitted_at.isoformat()
if application.submitted_at
else None
),
"decided_at": (
application.decided_at.isoformat()
if application.decided_at
else None
),
}


def get_actor(request):
"""Return the authenticated principal supplied by the platform."""
return getattr(request, "principal", None)


class MyLeaveApplicationsView(APIView):
"""List leave applications belonging to the current employee."""

def get(self, request):
actor = get_actor(request)
require(actor, "elm.application.view_self")

employee_id = str(getattr(actor, "id", ""))

if not employee_id:
return Response(
{"detail": "Employee identity is unavailable."},
status=status.HTTP_400_BAD_REQUEST,
)

applications = LeaveApplication.objects.filter(
employee_id=employee_id
)

return Response(
[serialize_application(item) for item in applications]
)


class SubmitLeaveApplicationView(APIView):
"""Submit a leave application."""

def post(self, request):
actor = get_actor(request)
require(actor, "elm.application.create")

data = request.data

try:
start_date = date.fromisoformat(data.get("start_date", ""))
end_date = date.fromisoformat(data.get("end_date", ""))

application = submit_leave(
employee_id=str(getattr(actor, "id", "")),
leave_type=str(data.get("leave_type", "")),
start_date=start_date,
end_date=end_date,
reason=str(data.get("reason", "")),
)
except (ValueError, TypeError, LeaveApplicationError) as exc:
return Response(
{"detail": str(exc)},
status=status.HTTP_400_BAD_REQUEST,
)

return Response(
serialize_application(application),
status=status.HTTP_201_CREATED,
)


class DecideLeaveApplicationView(APIView):
"""Approve or reject a submitted leave application."""

def post(self, request, application_id):
actor = get_actor(request)
require(actor, "elm.application.approve")

try:
UUID(str(application_id))
decision = request.data.get("decision", "")
if decision not in ("approve", "reject"):
return Response(
{"detail": "Decision must be 'approve' or 'reject'."},
status=status.HTTP_400_BAD_REQUEST,
)

application = decide_leave(
application_id=application_id,
approver_id=str(getattr(actor, "id", "")),
approve=(decision == "approve"),
decision_note=str(request.data.get("decision_note", "")),
)
except LeaveApplication.DoesNotExist:
return Response(
{"detail": "Leave application not found."},
status=status.HTTP_404_NOT_FOUND,
)
except (ValueError, TypeError, LeaveApplicationError) as exc:
return Response(
{"detail": str(exc)},
status=status.HTTP_400_BAD_REQUEST,
)

return Response(serialize_application(application))
9 changes: 9 additions & 0 deletions modules/elm/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

from django.apps import AppConfig


class ElmConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "modules.elm"
label = "elm"
verbose_name = "Employee Leave Management"
Empty file added modules/elm/domain/__init__.py
Empty file.
42 changes: 42 additions & 0 deletions modules/elm/domain/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

"""Domain types for Employee Leave Management."""

from dataclasses import dataclass
from datetime import date
from enum import StrEnum
from uuid import UUID


class LeaveStatus(StrEnum):
DRAFT = "draft"
SUBMITTED = "submitted"
PENDING_APPROVAL = "pending_approval"
APPROVED = "approved"
REJECTED = "rejected"
WITHDRAWN = "withdrawn"
CANCELLED = "cancelled"
EXTENSION_REQUESTED = "extension_requested"
RESUMED = "resumed"


@dataclass(frozen=True)
class LeavePeriod:
start_date: date
end_date: date

def __post_init__(self) -> None:
if self.end_date < self.start_date:
raise ValueError("Leave end date cannot be before start date.")

@property
def calendar_days(self) -> int:
return (self.end_date - self.start_date).days + 1


@dataclass(frozen=True)
class LeaveApplicationSummary:
id: UUID
employee_id: str
leave_type: str
period: LeavePeriod
status: LeaveStatus
37 changes: 37 additions & 0 deletions modules/elm/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 5.2.18 on 2026-10-12 13:18

import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='LeaveApplication',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('employee_id', models.CharField(db_index=True, max_length=128)),
('leave_type', models.CharField(max_length=32)),
('start_date', models.DateField()),
('end_date', models.DateField()),
('reason', models.TextField()),
('status', models.CharField(choices=[('draft', 'Draft'), ('submitted', 'Submitted'), ('pending_approval', 'Pending Approval'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('withdrawn', 'Withdrawn'), ('cancelled', 'Cancelled'), ('extension_requested', 'Extension Requested'), ('resumed', 'Resumed')], db_index=True, default='draft', max_length=32)),
('approver_id', models.CharField(blank=True, default='', max_length=128)),
('decision_note', models.TextField(blank=True, default='')),
('submitted_at', models.DateTimeField(blank=True, null=True)),
('decided_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['employee_id', 'status'], name='elm_emp_status_idx'), models.Index(fields=['start_date', 'end_date'], name='elm_leave_dates_idx')],
},
),
]
Empty file.
6 changes: 6 additions & 0 deletions modules/elm/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

"""Database models for Employee Leave Management."""

from .leave_application import LeaveApplication

__all__ = ["LeaveApplication"]
62 changes: 62 additions & 0 deletions modules/elm/models/leave_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

"""Database model for Employee Leave Management applications."""

import uuid

from django.db import models


class LeaveApplication(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
SUBMITTED = "submitted", "Submitted"
PENDING_APPROVAL = "pending_approval", "Pending Approval"
APPROVED = "approved", "Approved"
REJECTED = "rejected", "Rejected"
WITHDRAWN = "withdrawn", "Withdrawn"
CANCELLED = "cancelled", "Cancelled"
EXTENSION_REQUESTED = "extension_requested", "Extension Requested"
RESUMED = "resumed", "Resumed"

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

employee_id = models.CharField(max_length=128, db_index=True)
leave_type = models.CharField(max_length=32)
start_date = models.DateField()
end_date = models.DateField()
reason = models.TextField()

status = models.CharField(
max_length=32,
choices=Status.choices,
default=Status.DRAFT,
db_index=True,
)

approver_id = models.CharField(max_length=128, blank=True, default="")
decision_note = models.TextField(blank=True, default="")
submitted_at = models.DateTimeField(null=True, blank=True)
decided_at = models.DateTimeField(null=True, blank=True)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
app_label = "elm"
ordering = ["-created_at"]
indexes = [
models.Index(
fields=["employee_id", "status"],
name="elm_emp_status_idx",
),
models.Index(
fields=["start_date", "end_date"],
name="elm_leave_dates_idx",
),
]

def __str__(self):
return (
f"{self.employee_id}: {self.leave_type} "
f"({self.start_date} to {self.end_date})"
)
Loading