Skip to content

ISSUE-062: Equipment Operation Authorization System #75

@codewizdave

Description

@codewizdave

ISSUE-062: Equipment Operation Authorization System

Description

The system currently tracks certifications but doesn't enforce whether an employee is authorized to operate specific equipment (chariots/forklifts). Users need to define and enforce rules that prevent equipment operation if all required conditions are not met.

Current State

  • Certifications are tracked (CACES, medical visits, training)
  • No link between certifications and equipment authorization
  • No validation of authorization before equipment operation
  • No visual indicator of who can operate what equipment
  • No reporting on unauthorized operation attempts

Expected Behavior

Equipment Model

Add to: src/employee/models.py

class Equipment(BaseModel):
    """Represent equipment that requires authorization."""

    id = AutoField()
    name = CharField(max_length=100)  # e.g., "Chariot Élévateur 1"
    type = CharField(max_length=50)  # e.g., "Forklift", "Crane", "Platform"
    serial_number = CharField(max_length=100, null=True)
    location = CharField(max_length=100, null=True)
    status = CharField(max_length=20, default="active")  # active, maintenance, retired
    notes = TextField(null=True)

    def can_be_operated_by(self, employee: Employee) -> tuple[bool, list[str]]:
        """
        Check if employee is authorized to operate this equipment.

        Returns:
            Tuple of (is_authorized, list_of_reasons)
        """
        requirements = EquipmentRequirement.select().where(EquipmentRequirement.equipment == self)
        reasons = []

        for requirement in requirements:
            if not requirement.is_satisfied_by(employee):
                reasons.append(requirement.get_failure_message())

        return len(reasons) == 0, reasons


class EquipmentRequirement(BaseModel):
    """Requirements for operating equipment."""

    id = AutoField()
    equipment = ForeignKeyField(Equipment, backref="requirements")
    requirement_type = CharField(max_length=50)  # caces, medical, training, contract
    caces_type = CharField(max_length=50, null=True)  # Required if type=caces
    training_name = CharField(max_length=100, null=True)  # Required if type=training
    min_days_valid = IntegerField(default=30)  # Certificate must be valid this many more days
    required = BooleanField(default=True)  # If True, must have this certification

    def is_satisfied_by(self, employee: Employee) -> bool:
        """Check if employee satisfies this requirement."""
        if self.requirement_type == "caces":
            return self._check_caces(employee)
        elif self.requirement_type == "medical":
            return self._check_medical(employee)
        elif self.requirement_type == "training":
            return self._check_training(employee)
        elif self.requirement_type == "contract":
            return self._check_contract(employee)
        return False

    def _check_caces(self, employee: Employee) -> bool:
        """Check if employee has valid CACES certification."""
        caces = CACES.select().where(
            (CACES.employee == employee) &
            (CACES.caces_type == self.caces_type)
        ).first()

        if not caces:
            return not self.required

        # Check if still valid
        if caces.expiration_date:
            days_until = (caces.expiration_date - date.today()).days
            return days_until >= self.min_days_valid

        return True

    def _check_medical(self, employee: Employee) -> bool:
        """Check if employee has valid medical visit."""
        visit = MedicalVisit.select().where(
            MedicalVisit.employee == employee
        ).order_by(MedicalVisit.visit_date.desc()).first()

        if not visit:
            return not self.required

        # Check next due date
        if visit.next_due_date:
            days_until = (visit.next_due_date - date.today()).days
            return days_until >= self.min_days_valid

        return True

    def _check_training(self, employee: Employee) -> bool:
        """Check if employee has required training."""
        training = Training.select().where(
            (Training.employee == employee) &
            (Training.training_name == self.training_name)
        ).first()

        if not training:
            return not self.required

        # Check if expired
        if training.expiration_date:
            days_until = (training.expiration_date - date.today()).days
            return days_until >= self.min_days_valid

        return True

    def _check_contract(self, employee: Employee) -> bool:
        """Check if employee has valid contract."""
        if not employee.current_contract:
            return not self.required

        # Check if contract is active
        if employee.current_contract.end_date:
            days_until = (employee.current_contract.end_date - date.today()).days
            return days_until >= self.min_days_valid

        return True

    def get_failure_message(self) -> str:
        """Get human-readable failure message."""
        if self.requirement_type == "caces":
            return f"Requires valid {self.caces_type} certification (min {self.min_days_valid} days valid)"
        elif self.requirement_type == "medical":
            return f"Requires valid medical visit (min {self.min_days_valid} days valid)"
        elif self.requirement_type == "training":
            return f"Requires {self.training_name} training (min {self.min_days_valid} days valid)"
        elif self.requirement_type == "contract":
            return f"Requires valid employment contract (min {self.min_days_valid} days valid)"
        return "Authorization requirement not met"


class EquipmentOperationLog(BaseModel):
    """Log equipment operation attempts."""

    id = AutoField()
    equipment = ForeignKeyField(Equipment, backref="operation_logs")
    employee = ForeignKeyField(Employee, backref="operation_logs")
    timestamp = DateTimeField(default=datetime.now)
    authorized = BooleanField()
    reasons = TextField(null=True)  # JSON array of reasons if not authorized
    operation_type = CharField(max_length=50)  # start, stop, check

Equipment Management UI

Create: src/ui_ctk/views/equipment_view.py

class EquipmentView(ctk.CTkFrame):
    """View for managing equipment and authorizations."""

    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.selected_equipment = None
        self.create_widgets()
        self.load_equipment()

    def create_widgets(self):
        """Create widgets."""
        # Equipment list
        self.equipment_list = self.create_equipment_list()
        self.equipment_list.pack(side="left", fill="both", expand=True)

        # Equipment details
        self.details_panel = self.create_details_panel()
        self.details_panel.pack(side="right", fill="both", expand=True)

    def create_details_panel(self):
        """Create equipment details panel."""
        panel = ctk.CTkFrame(self)

        # Equipment info
        info_frame = ctk.CTkFrame(panel)
        info_frame.pack(fill="x", padx=10, pady=10)

        # Authorization status
        auth_frame = ctk.CTkFrame(panel)
        auth_frame.pack(fill="x", padx=10, pady=10)

        ctk.CTkLabel(
            auth_frame,
            text="Authorization Requirements",
            font=("Arial", 14, "bold")
        ).pack(pady=10)

        # Requirements list
        self.requirements_list = ctk.CTkScrollableFrame(auth_frame)
        self.requirements_list.pack(fill="both", expand=True, padx=10, pady=10)

        # Buttons
        btn_frame = ctk.CTkFrame(panel)
        btn_frame.pack(fill="x", padx=10, pady=10)

        ctk.CTkButton(
            btn_frame,
            text="Add Requirement",
            command=self.add_requirement
        ).pack(side="left", padx=5)

        ctk.CTkButton(
            btn_frame,
            text="Check Authorization",
            command=self.check_authorization
        ).pack(side="left", padx=5)

        # Employee selector
        employee_frame = ctk.CTkFrame(panel)
        employee_frame.pack(fill="x", padx=10, pady=10)

        ctk.CTkLabel(
            employee_frame,
            text="Check Employee Authorization:"
        ).pack(side="left", padx=5)

        self.employee_selector = ctk.CTkComboBox(
            employee_frame,
            values=[e.full_name for e in Employee.select()]
        )
        self.employee_selector.pack(side="left", padx=5)

        return panel

    def check_authorization(self):
        """Check if selected employee can operate equipment."""
        if not self.selected_equipment:
            return

        employee_name = self.employee_selector.get()
        employee = Employee.select().where(Employee.full_name == employee_name).first()

        if not employee:
            return

        is_authorized, reasons = self.selected_equipment.can_be_operated_by(employee)

        # Show result dialog
        AuthorizationResultDialog(self, employee, self.selected_equipment, is_authorized, reasons)


class AuthorizationResultDialog(ctk.CTkToplevel):
    """Dialog showing authorization check result."""

    def __init__(self, parent, employee, equipment, is_authorized, reasons):
        super().__init__(parent)

        self.title("Authorization Check Result")
        self.geometry("600x400")
        self.transient(parent)
        self.grab_set()

        # Center dialog
        self.update_idletasks()
        x = parent.winfo_x() + (parent.winfo_width() - self.winfo_width()) // 2
        y = parent.winfo_y() + (parent.winfo_height() - self.winfo_height()) // 2
        self.geometry(f"+{x}+{y}")

        self.create_widgets(employee, equipment, is_authorized, reasons)

    def create_widgets(self, employee, equipment, is_authorized, reasons):
        """Create dialog widgets."""
        main = ctk.CTkFrame(self)
        main.pack(fill="both", expand=True, padx=20, pady=20)

        # Result
        if is_authorized:
            result_label = ctk.CTkLabel(
                main,
                text=f"✓ AUTHORIZED",
                font=("Arial", 24, "bold"),
                text_color="green"
            )
            result_label.pack(pady=20)

            info = ctk.CTkLabel(
                main,
                text=f"{employee.full_name} is authorized to operate {equipment.name}",
                font=("Arial", 14)
            )
            info.pack(pady=10)
        else:
            result_label = ctk.CTkLabel(
                main,
                text=f"✗ NOT AUTHORIZED",
                font=("Arial", 24, "bold"),
                text_color="red"
            )
            result_label.pack(pady=20)

            info = ctk.CTkLabel(
                main,
                text=f"{employee.full_name} is NOT authorized to operate {equipment.name}",
                font=("Arial", 14)
            )
            info.pack(pady=10)

            # Reasons
            if reasons:
                reasons_frame = ctk.CTkFrame(main)
                reasons_frame.pack(fill="both", expand=True, pady=20)

                ctk.CTkLabel(
                    reasons_frame,
                    text="Reasons:",
                    font=("Arial", 12, "bold")
                ).pack(anchor="w", padx=10, pady=10)

                for reason in reasons:
                    reason_label = ctk.CTkLabel(
                        reasons_frame,
                        text=f"• {reason}",
                        text_color="red"
                    )
                    reason_label.pack(anchor="w", padx=20, pady=5)

        # Log the check
        EquipmentOperationLog.create(
            equipment=equipment,
            employee=employee,
            authorized=is_authorized,
            reasons=json.dumps(reasons) if reasons else None,
            operation_type="check"
        )

        # Close button
        ctk.CTkButton(
            main,
            text="Close",
            command=self.destroy
        ).pack(pady=20)

Equipment Authorization Report

Add report showing:

  1. Equipment and authorized operators
  2. Operators near expiration (won't be authorized soon)
  3. Unauthorized operation attempts
  4. Equipment with no authorized operators
# src/reports/equipment_authorization_report.py
def generate_authorization_report():
    """Generate equipment authorization report."""
    report = {
        "equipment": [],
        "unauthorized_attempts": [],
        "expiring_soon": []
    }

    for equipment in Equipment.select():
        authorized_operators = []
        all_operators = []

        for employee in Employee.select():
            is_authorized, reasons = equipment.can_be_operated_by(employee)
            operator_info = {
                "employee": employee,
                "authorized": is_authorized,
                "reasons": reasons
            }
            all_operators.append(operator_info)
            if is_authorized:
                authorized_operators.append(employee)

        report["equipment"].append({
            "equipment": equipment,
            "authorized_count": len(authorized_operators),
            "authorized_operators": authorized_operators,
            "all_operators": all_operators
        })

    return report

Affected Files

  • src/employee/models.py - Add Equipment, EquipmentRequirement, EquipmentOperationLog models
  • src/ui_ctk/views/equipment_view.py - New equipment management view
  • src/ui_ctk/dialogs/authorization_result_dialog.py - New authorization result dialog
  • src/reports/equipment_authorization_report.py - New authorization report
  • Database migration for new tables

Implementation Plan

Phase 1: Data Model (1 day)

  1. Create Equipment model
  2. Create EquipmentRequirement model
  3. Create EquipmentOperationLog model
  4. Create database migration
  5. Add authorization check methods

Phase 2: Equipment Management UI (2 days)

  1. Create equipment view with list
  2. Add equipment creation/editing
  3. Add requirement management
  4. Add authorization check dialog
  5. Integrate with main window

Phase 3: Authorization Logic (1 day)

  1. Implement authorization check algorithm
  2. Add validation for requirements
  3. Handle edge cases (expired, missing certifications)
  4. Add operation logging

Phase 4: Reporting (1 day)

  1. Create authorization report
  2. Add equipment authorization summary
  3. Export to Excel
  4. Add scheduling for regular reports

Dependencies

  • None (new functionality)

Related Issues

  • ISSUE-060: Hierarchical Document Storage (certification tracking)
  • ISSUE-061: Configurable Alert System (uses same validation logic)
  • ISSUE-064: Contract History Tracking (contract validation)

Acceptance Criteria

  • Equipment model implemented
  • EquipmentRequirement model implemented
  • Authorization check logic works correctly
  • Can define requirements per equipment
  • Multiple requirement types supported (CACES, medical, training, contract)
  • Configurable minimum validity period
  • Authorization check dialog shows clear results
  • Operation attempts logged
  • Authorization report generated
  • Excel export of authorization report
  • All tests pass

Estimated Effort

Total: 5-6 days

  • Data model: 1 day
  • Equipment management UI: 2 days
  • Authorization logic: 1 day
  • Reporting: 1 day
  • Testing and integration: 0.5-1 day

Notes

This is a critical safety feature. Ensuring only properly certified employees can operate equipment prevents accidents and ensures compliance with safety regulations.

Example Use Cases

Example 1: Forklift Authorization

Equipment: "Chariot Élévateur 1"
Requirements:

  • CACES 1A or 1B (min 30 days valid)
  • Valid medical visit (min 30 days valid)
  • Active contract

Result: Only employees meeting ALL requirements are authorized.

Example 2: Crane Authorization

Equipment: "Pont Roulant 5T"
Requirements:

  • CACES 3 (min 60 days valid)
  • Specific training "Pont Roulant" (min 30 days valid)
  • Valid medical visit (min 60 days valid)

Result: More stringent requirements for heavy equipment.

Future Enhancements

  • Equipment authorization schedule (who is authorized when)
  • Temporary authorization (with supervisor approval)
  • Authorization expiration warnings
  • Equipment operation time tracking
  • Integration with access control systems
  • Mobile authorization verification (QR codes)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions