""" 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, PDCAPhase, FOCUSPhase 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 from apps.core.utils import get_assignable_users assignable = get_assignable_users(Hospital.objects.get(pk=hospital_id)) if hospital_id else User.objects.none() self.fields["project_lead"].queryset = assignable self.fields["team_members"].queryset = assignable 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", "pdca_phase", "focus_phase"] 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, } ), "pdca_phase": 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" } ), "focus_phase": 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" } ), } def __init__(self, *args, **kwargs): self.project = kwargs.pop("project", None) self.current_phase = kwargs.pop("current_phase", None) self.phase_type = kwargs.pop("phase_type", None) self.allow_any_phase = kwargs.pop("allow_any_phase", False) super().__init__(*args, **kwargs) # Make order field not required (has default value of 0) self.fields["order"].required = False if self.allow_any_phase: # Show both PDCA and FOCUS dropdowns if self.project: self.fields["pdca_phase"].queryset = self.project.pdca_phases.all().order_by("order") self.fields["focus_phase"].queryset = self.project.focus_phases.all().order_by("order") if self.current_phase: if isinstance(self.current_phase, PDCAPhase): self.fields["pdca_phase"].initial = self.current_phase.pk elif isinstance(self.current_phase, FOCUSPhase): self.fields["focus_phase"].initial = self.current_phase.pk else: self.fields["pdca_phase"].queryset = PDCAPhase.objects.none() self.fields["focus_phase"].queryset = FOCUSPhase.objects.none() elif self.phase_type == "focus": self.fields["pdca_phase"].queryset = PDCAPhase.objects.none() self.fields["pdca_phase"].widget = forms.HiddenInput() if self.project: self.fields["focus_phase"].queryset = self.project.focus_phases.all().order_by("order") if self.current_phase: self.fields["focus_phase"].initial = self.current_phase.pk else: self.fields["focus_phase"].queryset = FOCUSPhase.objects.none() self.fields["focus_phase"].widget = forms.HiddenInput() else: if self.project: self.fields["pdca_phase"].queryset = self.project.pdca_phases.all().order_by("order") if self.current_phase: self.fields["pdca_phase"].initial = self.current_phase.pk else: self.fields["pdca_phase"].queryset = PDCAPhase.objects.none() self.fields["pdca_phase"].widget = forms.HiddenInput() self.fields["focus_phase"].queryset = FOCUSPhase.objects.none() self.fields["focus_phase"].widget = forms.HiddenInput() # Filter assigned_to choices based on project hospital if self.project and self.project.hospital: from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(self.project.hospital) 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 from apps.core.utils import get_assignable_users self.fields["project_lead"].queryset = get_assignable_users(self.user.hospital) 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, )