HH/apps/projects/forms.py
2026-03-15 23:48:45 +03:00

399 lines
16 KiB
Python

"""
QI Projects Forms
Forms for creating and managing Quality Improvement projects and tasks.
"""
from django import forms
from django.forms import inlineformset_factory
from django.utils.translation import gettext_lazy as _
from apps.core.form_mixins import HospitalFieldMixin
from apps.accounts.models import User
from apps.organizations.models import Department, Hospital
from .models import QIProject, QIProjectTask
class QIProjectForm(HospitalFieldMixin, forms.ModelForm):
"""
Form for creating and editing QI Projects.
Hospital field visibility:
- PX Admins: See dropdown with all hospitals
- Others: Hidden field, auto-set to user's hospital
"""
class Meta:
model = QIProject
fields = [
"name",
"name_ar",
"description",
"hospital",
"department",
"project_lead",
"team_members",
"status",
"start_date",
"target_completion_date",
"outcome_description",
]
widgets = {
"name": forms.TextInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"placeholder": _("Project name"),
}
),
"name_ar": forms.TextInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"placeholder": _("اسم المشروع"),
"dir": "rtl",
}
),
"description": forms.Textarea(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm resize-none",
"rows": 4,
"placeholder": _("Describe the project objectives and scope..."),
}
),
"hospital": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"department": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"project_lead": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"team_members": forms.SelectMultiple(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white h-40"
}
),
"status": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"start_date": forms.DateInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"type": "date",
}
),
"target_completion_date": forms.DateInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"type": "date",
}
),
"outcome_description": forms.Textarea(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm resize-none",
"rows": 3,
"placeholder": _("Document project outcomes and results..."),
}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter department choices based on hospital
hospital_id = None
if self.data.get("hospital"):
hospital_id = self.data.get("hospital")
elif self.initial.get("hospital"):
hospital_id = self.initial.get("hospital")
elif self.instance and self.instance.pk and self.instance.hospital:
hospital_id = self.instance.hospital.id
elif self.user and self.user.is_px_admin():
tenant_hospital = getattr(self.request, "tenant_hospital", None)
if tenant_hospital:
hospital_id = tenant_hospital.id
elif self.user and self.user.hospital:
hospital_id = self.user.hospital.id
if hospital_id:
self.fields["department"].queryset = Department.objects.filter(
hospital_id=hospital_id, status="active"
).order_by("name")
# Filter user choices based on hospital
self.fields["project_lead"].queryset = User.objects.filter(
hospital_id=hospital_id, is_active=True
).order_by("first_name", "last_name")
self.fields["team_members"].queryset = User.objects.filter(
hospital_id=hospital_id, is_active=True
).order_by("first_name", "last_name")
else:
self.fields["department"].queryset = Department.objects.none()
self.fields["project_lead"].queryset = User.objects.none()
self.fields["team_members"].queryset = User.objects.none()
class QIProjectTaskForm(forms.ModelForm):
"""
Form for creating and editing QI Project tasks.
"""
class Meta:
model = QIProjectTask
fields = ["title", "description", "assigned_to", "status", "due_date", "order"]
widgets = {
"title": forms.TextInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"placeholder": _("Task title"),
}
),
"description": forms.Textarea(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm resize-none",
"rows": 3,
"placeholder": _("Task description..."),
}
),
"assigned_to": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"status": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"due_date": forms.DateInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"type": "date",
}
),
"order": forms.NumberInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"min": 0,
}
),
}
def __init__(self, *args, **kwargs):
self.project = kwargs.pop("project", None)
super().__init__(*args, **kwargs)
# Make order field not required (has default value of 0)
self.fields["order"].required = False
# Filter assigned_to choices based on project hospital
if self.project and self.project.hospital:
self.fields["assigned_to"].queryset = User.objects.filter(
hospital=self.project.hospital, is_active=True
).order_by("first_name", "last_name")
else:
self.fields["assigned_to"].queryset = User.objects.none()
class QIProjectTemplateForm(HospitalFieldMixin, forms.ModelForm):
"""
Form for creating and editing QI Project templates.
Hospital field visibility:
- PX Admins: See dropdown with all hospitals
- Others: Hidden field, auto-set to user's hospital
Templates can be:
- Global (hospital=None) - available to all
- Hospital-specific - available only to that hospital
"""
class Meta:
model = QIProject
fields = ["name", "name_ar", "description", "hospital", "department", "target_completion_date"]
widgets = {
"name": forms.TextInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"placeholder": _("Template name"),
}
),
"name_ar": forms.TextInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"placeholder": _("اسم القالب"),
"dir": "rtl",
}
),
"description": forms.Textarea(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm resize-none",
"rows": 4,
"placeholder": _("Describe the project template..."),
}
),
"hospital": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"department": forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
"target_completion_date": forms.DateInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"type": "date",
}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make hospital optional for templates (global templates)
self.fields["hospital"].required = False
self.fields["hospital"].empty_label = _("Global (All Hospitals)")
# Filter department choices based on hospital
hospital_id = None
if self.data.get("hospital"):
hospital_id = self.data.get("hospital")
elif self.initial.get("hospital"):
hospital_id = self.initial.get("hospital")
elif self.instance and self.instance.pk and self.instance.hospital:
hospital_id = self.instance.hospital.id
elif self.user and self.user.is_px_admin():
tenant_hospital = getattr(self.request, "tenant_hospital", None)
if tenant_hospital:
hospital_id = tenant_hospital.id
elif self.user and self.user.hospital:
hospital_id = self.user.hospital.id
if hospital_id:
self.fields["department"].queryset = Department.objects.filter(
hospital_id=hospital_id, status="active"
).order_by("name")
else:
self.fields["department"].queryset = Department.objects.none()
class ConvertToProjectForm(forms.Form):
"""
Form for converting a PX Action to a QI Project.
Allows selecting a template and customizing the project details.
"""
template = forms.ModelChoiceField(
queryset=QIProject.objects.none(),
required=False,
empty_label=_("Blank Project"),
label=_("Project Template"),
widget=forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
)
project_name = forms.CharField(
max_length=200,
label=_("Project Name"),
widget=forms.TextInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"placeholder": _("Enter project name"),
}
),
)
project_lead = forms.ModelChoiceField(
queryset=User.objects.none(),
required=True,
label=_("Project Lead"),
widget=forms.Select(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm bg-white"
}
),
)
target_completion_date = forms.DateField(
required=False,
label=_("Target Completion Date"),
widget=forms.DateInput(
attrs={
"class": "w-full px-4 py-2.5 rounded-xl border border-slate-200 focus:border-navy focus:ring-2 focus:ring-navy/20 transition text-sm",
"type": "date",
}
),
)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
self.user = self.request.user if self.request else None
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
if self.user and self.user.hospital:
# Filter templates by hospital (or global)
from django.db.models import Q
self.fields["template"].queryset = QIProject.objects.filter(
Q(hospital=self.user.hospital) | Q(hospital__isnull=True),
status="template", # We'll add this status or use metadata
).order_by("name")
# Filter project lead by hospital
self.fields["project_lead"].queryset = User.objects.filter(
hospital=self.user.hospital, is_active=True
).order_by("first_name", "last_name")
else:
self.fields["template"].queryset = QIProject.objects.none()
self.fields["project_lead"].queryset = User.objects.none()
# Inline formset for task templates (used with QIProject templates)
class TaskTemplateForm(forms.ModelForm):
"""Simplified form for task templates (no project field needed)"""
class Meta:
model = QIProjectTask
fields = ["title", "description"]
widgets = {
"title": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": _("Task title"),
}
),
"description": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": _("Description"),
}
),
}
TaskTemplateFormSet = inlineformset_factory(
QIProject,
QIProjectTask,
form=TaskTemplateForm,
fields=["title", "description"],
extra=1,
can_delete=True,
)