399 lines
16 KiB
Python
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,
|
|
)
|