From 4ffef0ef57625126d5ab78991c525ace05f5542d Mon Sep 17 00:00:00 2001
From: Sandeep Chauhan <sandeepsajan0@gmail.com>
Date: Tue, 30 May 2023 14:38:47 +0530
Subject: [PATCH] Updated PAF approval flow (#3400)

Fixes #3387
---
 .../activity/adapters/django_messages.py      |   1 -
 hypha/apply/activity/adapters/emails.py       |  31 +++-
 hypha/apply/activity/adapters/utils.py        |  21 +++
 .../migrations/0074_alter_event_type.py       |  18 +++
 hypha/apply/activity/options.py               |   1 +
 .../messages/email/assign_paf_approvers.html  |  14 ++
 .../dashboard/contracting_dashboard.html      |   7 +
 .../templates/dashboard/dashboard.html        |   7 +
 .../dashboard/finance_dashboard.html          |   7 +
 .../dashboard/reviewer_dashboard.html         |   7 +
 hypha/apply/dashboard/views.py                | 116 ++++++++++++-
 hypha/apply/projects/forms/__init__.py        |   2 +
 hypha/apply/projects/forms/project.py         |  76 +++++++--
 .../0075_alter_pafapprovals_user.py           |  21 +++
 hypha/apply/projects/models/project.py        |   2 +-
 hypha/apply/projects/permissions.py           | 112 ++++++++++++-
 hypha/apply/projects/tables.py                |  16 ++
 .../includes/supporting_documents.html        |  60 ++++++-
 .../application_projects/project_detail.html  |   4 +-
 .../projects/templatetags/approval_tools.py   |  18 +--
 .../projects/templatetags/project_tags.py     |  31 +++-
 hypha/apply/projects/tests/test_views.py      |   8 +
 hypha/apply/projects/views/project.py         | 152 +++++++++++++++---
 tailwind.config.js                            |   1 +
 24 files changed, 662 insertions(+), 71 deletions(-)
 create mode 100644 hypha/apply/activity/migrations/0074_alter_event_type.py
 create mode 100644 hypha/apply/activity/templates/messages/email/assign_paf_approvers.html
 create mode 100644 hypha/apply/projects/migrations/0075_alter_pafapprovals_user.py

diff --git a/hypha/apply/activity/adapters/django_messages.py b/hypha/apply/activity/adapters/django_messages.py
index c8f4a74d7..2499ed6d3 100644
--- a/hypha/apply/activity/adapters/django_messages.py
+++ b/hypha/apply/activity/adapters/django_messages.py
@@ -14,7 +14,6 @@ class DjangoMessagesAdapter(AdapterBase):
         MESSAGES.BATCH_REVIEWERS_UPDATED: 'batch_reviewers_updated',
         MESSAGES.BATCH_TRANSITION: 'batch_transition',
         MESSAGES.BATCH_DETERMINATION_OUTCOME: 'batch_determinations',
-        MESSAGES.UPLOAD_DOCUMENT: _('Successfully uploaded document'),
         MESSAGES.REMOVE_DOCUMENT: _('Successfully removed document'),
         MESSAGES.SKIPPED_REPORT: 'handle_skipped_report',
         MESSAGES.REPORT_FREQUENCY_CHANGED: 'handle_report_frequency',
diff --git a/hypha/apply/activity/adapters/emails.py b/hypha/apply/activity/adapters/emails.py
index d297c06d0..0fd4aad46 100644
--- a/hypha/apply/activity/adapters/emails.py
+++ b/hypha/apply/activity/adapters/emails.py
@@ -24,6 +24,7 @@ from ..tasks import send_mail
 from .base import AdapterBase
 from .utils import (
     get_compliance_email,
+    get_users_for_groups,
     is_ready_for_review,
     is_reviewer_update,
     is_transition,
@@ -57,6 +58,7 @@ class EmailAdapter(AdapterBase):
         MESSAGES.SENT_TO_COMPLIANCE: 'messages/email/sent_to_compliance.html',
         MESSAGES.SEND_FOR_APPROVAL: 'messages/email/paf_for_approval.html',
         MESSAGES.REQUEST_PROJECT_CHANGE: 'messages/email/project_request_change.html',
+        MESSAGES.ASSIGN_PAF_APPROVER: 'messages/email/assign_paf_approvers.html',
         MESSAGES.APPROVE_PAF: 'messages/email/paf_for_approval.html',
         MESSAGES.UPDATE_INVOICE: 'handle_invoice_updated',
         MESSAGES.UPDATE_INVOICE_STATUS: 'handle_invoice_status_updated',
@@ -84,7 +86,7 @@ class EmailAdapter(AdapterBase):
                 subject = _(
                     'Reminder: Application ready to review: {source.title}'
                 ).format(source=source)
-            elif message_type in [MESSAGES.SENT_TO_COMPLIANCE]:
+            elif message_type in [MESSAGES.SENT_TO_COMPLIANCE, MESSAGES.APPROVE_PAF, MESSAGES.SEND_FOR_APPROVAL]:
                 subject = _('Project is waiting for approval: {source.title}').format(source=source)
             elif message_type == MESSAGES.UPLOAD_CONTRACT:
                 subject = _('Contract uploaded for the project: {source.title}').format(source=source)
@@ -100,6 +102,8 @@ class EmailAdapter(AdapterBase):
                     subject = _('Project status has changed to {source.status}: {source.title}').format(source=source)
             elif message_type == MESSAGES.REQUEST_PROJECT_CHANGE:
                 subject = _("Project has been rejected, please update and resubmit")
+            elif message_type == MESSAGES.ASSIGN_PAF_APPROVER:
+                subject = _("Project documents are ready to be assigned for approval: {source.title}".format(source=source))
             else:
                 try:
                     subject = source.page.specific.subject or _(
@@ -264,9 +268,30 @@ class EmailAdapter(AdapterBase):
             project_settings = ProjectSettings.for_request(request)
             if project_settings.paf_approval_sequential:
                 next_paf_approval = source.paf_approvals.filter(approved=False).first()
-                if next_paf_approval:
+                if next_paf_approval and next_paf_approval.user:
                     return [next_paf_approval.user.email]
-            return source.paf_approvals.filter(approved=False).values_list('user__email', flat=True)
+            return list(filter(lambda approver: approver is not None, source.paf_approvals.filter(approved=False).values_list('user__email', flat=True)))
+
+        if message_type == MESSAGES.ASSIGN_PAF_APPROVER:
+            from hypha.apply.projects.models.project import ProjectSettings
+            # notify PAFReviewerRole's groups' users to assign approvers
+            request = kwargs.get('request')
+            project_settings = ProjectSettings.for_request(request)
+            if project_settings.paf_approval_sequential:
+                next_paf_approval = source.paf_approvals.filter(approved=False).first()
+                if next_paf_approval and not next_paf_approval.user:
+                    assigners = get_users_for_groups(list(next_paf_approval.paf_reviewer_role.user_roles.all()), exact_match=True)
+                    return [assigner.email for assigner in assigners]
+
+            assigners_emails = []
+            if user == source.lead:
+                for approval in source.paf_approvals.filter(approved=False, user__isnull=True):
+                    assigners_emails.extend([assigner.email for assigner in get_users_for_groups(list(approval.paf_reviewer_role.user_roles.all()), exact_match=True)])
+            else:
+                assigners_emails.extend([assigner.email for assigner in
+                                         get_users_for_groups(list(user.groups.all()),
+                                                              exact_match=True)])
+            return set(assigners_emails)
 
         if message_type == MESSAGES.REQUEST_PROJECT_CHANGE:
             return [source.lead.email]
diff --git a/hypha/apply/activity/adapters/utils.py b/hypha/apply/activity/adapters/utils.py
index 70c544024..786bd538a 100644
--- a/hypha/apply/activity/adapters/utils.py
+++ b/hypha/apply/activity/adapters/utils.py
@@ -1,5 +1,6 @@
 from collections import defaultdict
 
+from django.db.models import Count
 from django.utils.translation import gettext as _
 
 from hypha.apply.activity.options import MESSAGES
@@ -81,3 +82,23 @@ def get_compliance_email(target_user_gps=None):
                 staff_users_email.append(user.email)
             target_user_emails.extend(staff_users_email)
     return target_user_emails
+
+
+def get_users_for_groups(groups, user_queryset=None, exact_match=False):
+    """
+    It will return the user queryset with the mentioned groups,
+
+    **NOTE: exact_match and user_queryset are not working together(for now). Set either one of them.
+    """
+    if groups:
+        if not user_queryset:
+            if exact_match:
+                user_queryset = User.objects.annotate(group_count=Count('groups')).filter(group_count=len(groups), groups__name=groups.pop().name)
+            else:
+                user_queryset = User.objects.filter(groups__name=groups.pop().name)
+        else:
+            user_queryset = user_queryset.filter(groups__name=groups.pop().name)
+        return get_users_for_groups(groups, user_queryset=user_queryset)
+    else:
+        return user_queryset
+
diff --git a/hypha/apply/activity/migrations/0074_alter_event_type.py b/hypha/apply/activity/migrations/0074_alter_event_type.py
new file mode 100644
index 000000000..ecca83337
--- /dev/null
+++ b/hypha/apply/activity/migrations/0074_alter_event_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.18 on 2023-05-10 12:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0073_add_approve_invoice'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='type',
+            field=models.CharField(choices=[('UPDATE_LEAD', 'updated lead'), ('BATCH_UPDATE_LEAD', 'batch updated lead'), ('EDIT_SUBMISSION', 'edited submission'), ('APPLICANT_EDIT', 'edited applicant'), ('NEW_SUBMISSION', 'submitted new submission'), ('DRAFT_SUBMISSION', 'submitted new draft submission'), ('SCREENING', 'screened'), ('TRANSITION', 'transitioned'), ('BATCH_TRANSITION', 'batch transitioned'), ('DETERMINATION_OUTCOME', 'sent determination outcome'), ('BATCH_DETERMINATION_OUTCOME', 'sent batch determination outcome'), ('INVITED_TO_PROPOSAL', 'invited to proposal'), ('REVIEWERS_UPDATED', 'updated reviewers'), ('BATCH_REVIEWERS_UPDATED', 'batch updated reviewers'), ('PARTNERS_UPDATED', 'updated partners'), ('PARTNERS_UPDATED_PARTNER', 'partners updated partner'), ('READY_FOR_REVIEW', 'marked ready for review'), ('BATCH_READY_FOR_REVIEW', 'marked batch ready for review'), ('NEW_REVIEW', 'added new review'), ('COMMENT', 'added comment'), ('PROPOSAL_SUBMITTED', 'submitted proposal'), ('OPENED_SEALED', 'opened sealed submission'), ('REVIEW_OPINION', 'reviewed opinion'), ('DELETE_SUBMISSION', 'deleted submission'), ('DELETE_REVIEW', 'deleted review'), ('CREATED_PROJECT', 'created project'), ('UPDATED_VENDOR', 'updated contracting information'), ('UPDATE_PROJECT_LEAD', 'updated project lead'), ('EDIT_REVIEW', 'edited review'), ('SEND_FOR_APPROVAL', 'sent for approval'), ('APPROVE_PROJECT', 'approved project'), ('ASSIGN_PAF_APPROVER', 'assign paf approver'), ('APPROVE_PAF', 'approved paf'), ('PROJECT_TRANSITION', 'transitioned project'), ('REQUEST_PROJECT_CHANGE', 'requested project change'), ('SUBMIT_CONTRACT_DOCUMENTS', 'submitted contract documents'), ('UPLOAD_DOCUMENT', 'uploaded document to project'), ('REMOVE_DOCUMENT', 'removed document from project'), ('UPLOAD_CONTRACT', 'uploaded contract to project'), ('APPROVE_CONTRACT', 'approved contract'), ('CREATE_INVOICE', 'created invoice for project'), ('UPDATE_INVOICE_STATUS', 'updated invoice status'), ('APPROVE_INVOICE', 'approve invoice'), ('DELETE_INVOICE', 'deleted invoice'), ('SENT_TO_COMPLIANCE', 'sent project to compliance'), ('UPDATE_INVOICE', 'updated invoice'), ('SUBMIT_REPORT', 'submitted report'), ('SKIPPED_REPORT', 'skipped report'), ('REPORT_FREQUENCY_CHANGED', 'changed report frequency'), ('DISABLED_REPORTING', 'disabled reporting'), ('REPORT_NOTIFY', 'notified report'), ('CREATE_REMINDER', 'created reminder'), ('DELETE_REMINDER', 'deleted reminder'), ('REVIEW_REMINDER', 'reminder to review'), ('BATCH_DELETE_SUBMISSION', 'batch deleted submissions'), ('BATCH_ARCHIVE_SUBMISSION', 'batch archive submissions'), ('STAFF_ACCOUNT_CREATED', 'created new account'), ('STAFF_ACCOUNT_EDITED', 'edited account'), ('ARCHIVE_SUBMISSION', 'archived submission'), ('UNARCHIVE_SUBMISSION', 'unarchived submission')], max_length=50, verbose_name='verb'),
+        ),
+    ]
diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py
index a43ae5f85..7ba8bba05 100644
--- a/hypha/apply/activity/options.py
+++ b/hypha/apply/activity/options.py
@@ -35,6 +35,7 @@ class MESSAGES(TextChoices):
     EDIT_REVIEW = 'EDIT_REVIEW', _('edited review')
     SEND_FOR_APPROVAL = 'SEND_FOR_APPROVAL', _('sent for approval')
     APPROVE_PROJECT = 'APPROVE_PROJECT', _('approved project')
+    ASSIGN_PAF_APPROVER = 'ASSIGN_PAF_APPROVER', _('assign paf approver')
     APPROVE_PAF = 'APPROVE_PAF', _('approved paf')
     PROJECT_TRANSITION = 'PROJECT_TRANSITION', _('transitioned project')
     REQUEST_PROJECT_CHANGE = 'REQUEST_PROJECT_CHANGE', _('requested project change')
diff --git a/hypha/apply/activity/templates/messages/email/assign_paf_approvers.html b/hypha/apply/activity/templates/messages/email/assign_paf_approvers.html
new file mode 100644
index 000000000..e87757f02
--- /dev/null
+++ b/hypha/apply/activity/templates/messages/email/assign_paf_approvers.html
@@ -0,0 +1,14 @@
+{% extends "messages/email/base.html" %}
+
+{% load i18n %}
+{% block salutation %}{% endblock %}
+
+{% block content %}
+{% trans "Project documents are ready to be assigned for approval." %}
+
+{% trans "Title" %}: {{ source.title }}
+{% trans "Link" %}: {{ request.scheme }}://{{ request.get_host }}{% url 'apply:projects:approval' pk=source.pk %}
+{% trans "Original Submission" %}: {{ request.scheme }}://{{ request.get_host }}{% url 'apply:submissions:simplified' pk=source.submission.pk %}
+
+{% blocktrans with lead=source.lead email=source.lead.email %}Please contact {{ lead }} - {{ email }} if you have any questions.{% endblocktrans %}
+{% endblock %}
diff --git a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html
index 6c693b18e..d78251451 100644
--- a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html
@@ -22,6 +22,13 @@
         {% include "dashboard/includes/paf_waiting_for_approval.html" with paf_waiting_for_approval=paf_waiting_for_approval %}
     {% endif %}
 
+    {% if paf_waiting_for_assignment.count %}
+     <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space">
+        <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4>
+        {% render_table paf_waiting_for_assignment.table %}
+    </div>
+    {% endif %}
+
     {% if projects_in_contracting.count %}
         {% include "dashboard/includes/projects_in_contracting.html" with projects_in_contracting=projects_in_contracting %}
     {% endif %}
diff --git a/hypha/apply/dashboard/templates/dashboard/dashboard.html b/hypha/apply/dashboard/templates/dashboard/dashboard.html
index 1cda540e0..beb08b907 100644
--- a/hypha/apply/dashboard/templates/dashboard/dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/dashboard.html
@@ -76,6 +76,13 @@
         {% include "dashboard/includes/paf_waiting_for_approval.html" with paf_waiting_for_approval=paf_waiting_for_approval %}
     {% endif %}
 
+    {% if paf_waiting_for_assignment.count %}
+     <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space">
+        <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4>
+        {% render_table paf_waiting_for_assignment.table %}
+    </div>
+    {% endif %}
+
     {% if projects.table.data %}
     <div id="active-projects" class="wrapper wrapper--bottom-space">
         {% trans "Your projects" as project_heading %}
diff --git a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html
index e417cd9e6..a95b0f681 100644
--- a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html
@@ -83,6 +83,13 @@
     {% if paf_waiting_for_approval.count %}
         {% include "dashboard/includes/paf_waiting_for_approval.html" with paf_waiting_for_approval=paf_waiting_for_approval %}
     {% endif %}
+
+    {% if paf_waiting_for_assignment.count %}
+        <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space">
+            <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4>
+            {% render_table paf_waiting_for_assignment.table %}
+        </div>
+    {% endif %}
     </div>
 {% endblock %}
 
diff --git a/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html b/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html
index f327f26f0..83011ccd6 100644
--- a/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html
+++ b/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html
@@ -70,6 +70,13 @@
     </div>
     {% endif %}
 
+    {% if paf_waiting_for_assignment.count %}
+     <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space">
+        <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4>
+        {% render_table paf_waiting_for_assignment.table %}
+    </div>
+    {% endif %}
+
     {% if my_inactive_submissions.data %}
         <div class="wrapper wrapper--bottom-space">
             <h4 class="heading heading--normal">{% trans "Submission history" %}</h4>
diff --git a/hypha/apply/dashboard/views.py b/hypha/apply/dashboard/views.py
index 4232657af..258bcb7d8 100644
--- a/hypha/apply/dashboard/views.py
+++ b/hypha/apply/dashboard/views.py
@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.db.models import Count
 from django.http import HttpResponseRedirect
 from django.shortcuts import render
 from django.urls import reverse, reverse_lazy
@@ -22,7 +23,11 @@ from hypha.apply.projects.filters import ProjectListFilter
 from hypha.apply.projects.models import Invoice, PAFApprovals, Project, ProjectSettings
 from hypha.apply.projects.models.project import WAITING_FOR_APPROVAL
 from hypha.apply.projects.permissions import has_permission
-from hypha.apply.projects.tables import InvoiceDashboardTable, ProjectsDashboardTable
+from hypha.apply.projects.tables import (
+    InvoiceDashboardTable,
+    ProjectsAssigneeDashboardTable,
+    ProjectsDashboardTable,
+)
 from hypha.apply.utils.views import ViewDispatcher
 
 
@@ -75,6 +80,7 @@ class AdminDashboardView(MyFlaggedMixin, TemplateView):
             'paf_waiting_for_approval': self.paf_waiting_for_approval(),
             'rounds': self.rounds(),
             'my_flagged': self.my_flagged(submissions),
+            'paf_waiting_for_assignment': self.paf_waiting_for_approver_assignment(),
         })
 
         return context
@@ -117,6 +123,34 @@ class AdminDashboardView(MyFlaggedMixin, TemplateView):
             'url': reverse('apply:projects:all'),
         }
 
+    def paf_waiting_for_approver_assignment(self):
+        project_settings = ProjectSettings.for_request(self.request)
+
+        paf_approvals = PAFApprovals.objects.annotate(
+            roles_count=Count('paf_reviewer_role__user_roles')
+        ).filter(roles_count=len(list(self.request.user.groups.all())), approved=False, user__isnull=True)
+
+        for role in self.request.user.groups.all():
+            paf_approvals = paf_approvals.filter(paf_reviewer_role__user_roles__id=role.id)
+
+        paf_approvals_ids = paf_approvals.values_list('id', flat=True)
+        projects = Project.objects.filter(paf_approvals__id__in=paf_approvals_ids).for_table()
+
+        if project_settings.paf_approval_sequential:
+            all_projects = list(projects)
+            for project in all_projects:
+                matched_paf_approval = paf_approvals.filter(project=project).order_by(
+                    'paf_reviewer_role__sort_order').first()
+                if project.paf_approvals.filter(
+                    paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order,
+                    approved=False).exists():
+                    projects = projects.exclude(id=project.id)
+
+        return {
+            'count': projects.count(),
+            'table': ProjectsAssigneeDashboardTable(projects),
+        }
+
     def paf_waiting_for_approval(self):
         if not self.request.user.is_apply_staff or not PAFApprovals.objects.filter(
             project__status=WAITING_FOR_APPROVAL,
@@ -203,7 +237,8 @@ class FinanceDashboardView(MyFlaggedMixin, TemplateView):
             'active_invoices': self.active_invoices(),
             'invoices_for_approval': self.invoices_for_approval(),
             'invoices_to_convert': self.invoices_to_convert(),
-            'paf_waiting_for_approval': self.paf_waiting_for_approval()
+            'paf_waiting_for_approval': self.paf_waiting_for_approval(),
+            'paf_waiting_for_assignment': self.paf_waiting_for_approver_assignment(),
         })
 
         return context
@@ -219,6 +254,31 @@ class FinanceDashboardView(MyFlaggedMixin, TemplateView):
             'table': InvoiceDashboardTable(invoices),
         }
 
+    def paf_waiting_for_approver_assignment(self):
+        project_settings = ProjectSettings.for_request(self.request)
+
+        paf_approvals = PAFApprovals.objects.annotate(
+                roles_count=Count('paf_reviewer_role__user_roles')
+            ).filter(roles_count=len(list(self.request.user.groups.all())), approved=False, user__isnull=True)
+
+        for role in self.request.user.groups.all():
+            paf_approvals = paf_approvals.filter(paf_reviewer_role__user_roles__id=role.id)
+
+        paf_approvals_ids = paf_approvals.values_list('id', flat=True)
+        projects = Project.objects.filter(paf_approvals__id__in=paf_approvals_ids).for_table()
+
+        if project_settings.paf_approval_sequential:
+            all_projects = list(projects)
+            for project in all_projects:
+                matched_paf_approval = paf_approvals.filter(project=project).order_by('paf_reviewer_role__sort_order').first()
+                if project.paf_approvals.filter(paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, approved=False).exists():
+                    projects = projects.exclude(id=project.id)
+
+        return {
+            'count': projects.count(),
+            'table': ProjectsAssigneeDashboardTable(projects),
+        }
+
     def invoices_for_approval(self):
         if self.request.user.is_finance_level_2:
             invoices = Invoice.objects.approved_by_finance_1()
@@ -327,6 +387,7 @@ class ReviewerDashboardView(MyFlaggedMixin, MySubmissionContextMixin, TemplateVi
             'awaiting_reviews': self.awaiting_reviews(submissions),
             'my_reviewed': self.my_reviewed(submissions),
             'my_flagged': self.my_flagged(submissions),
+            'paf_waiting_for_assignment': self.paf_waiting_for_approver_assignment(),
         })
 
         return context
@@ -343,6 +404,31 @@ class ReviewerDashboardView(MyFlaggedMixin, MySubmissionContextMixin, TemplateVi
             'table': ReviewerSubmissionsTable(submissions[:limit], prefix='my-review-'),
         }
 
+    def paf_waiting_for_approver_assignment(self):
+        project_settings = ProjectSettings.for_request(self.request)
+
+        paf_approvals = PAFApprovals.objects.annotate(
+                roles_count=Count('paf_reviewer_role__user_roles')
+            ).filter(roles_count=len(list(self.request.user.groups.all())), approved=False, user__isnull=True)
+
+        for role in self.request.user.groups.all():
+            paf_approvals = paf_approvals.filter(paf_reviewer_role__user_roles__id=role.id)
+
+        paf_approvals_ids = paf_approvals.values_list('id', flat=True)
+        projects = Project.objects.filter(paf_approvals__id__in=paf_approvals_ids).for_table()
+
+        if project_settings.paf_approval_sequential:
+            all_projects = list(projects)
+            for project in all_projects:
+                matched_paf_approval = paf_approvals.filter(project=project).order_by('paf_reviewer_role__sort_order').first()
+                if project.paf_approvals.filter(paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, approved=False).exists():
+                    projects = projects.exclude(id=project.id)
+
+        return {
+            'count': projects.count(),
+            'table': ProjectsAssigneeDashboardTable(projects),
+        }
+
     def my_reviewed(self, submissions):
         """Staff reviewer's reviewed submissions for 'Previous reviews' block"""
         submissions = submissions.reviewed_by(self.request.user).order_by('-submit_time')
@@ -389,6 +475,7 @@ class ContractingDashboardView(MyFlaggedMixin, TemplateView):
         context.update({
             'paf_waiting_for_approval': self.paf_waiting_for_approval(),
             'projects_in_contracting': self.projects_in_contracting(),
+            'paf_waiting_for_assignment': self.paf_waiting_for_approver_assignment(),
         })
 
         return context
@@ -446,6 +533,31 @@ class ContractingDashboardView(MyFlaggedMixin, TemplateView):
             }
         }
 
+    def paf_waiting_for_approver_assignment(self):
+        project_settings = ProjectSettings.for_request(self.request)
+
+        paf_approvals = PAFApprovals.objects.annotate(
+                roles_count=Count('paf_reviewer_role__user_roles')
+            ).filter(roles_count=len(list(self.request.user.groups.all())), approved=False, user__isnull=True)
+
+        for role in self.request.user.groups.all():
+            paf_approvals = paf_approvals.filter(paf_reviewer_role__user_roles__id=role.id)
+
+        paf_approvals_ids = paf_approvals.values_list('id', flat=True)
+        projects = Project.objects.filter(paf_approvals__id__in=paf_approvals_ids).for_table()
+
+        if project_settings.paf_approval_sequential:
+            all_projects = list(projects)
+            for project in all_projects:
+                matched_paf_approval = paf_approvals.filter(project=project).order_by('paf_reviewer_role__sort_order').first()
+                if project.paf_approvals.filter(paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, approved=False).exists():
+                    projects = projects.exclude(id=project.id)
+
+        return {
+            'count': projects.count(),
+            'table': ProjectsAssigneeDashboardTable(projects),
+        }
+
     def projects_in_contracting(self):
         if not self.request.user.is_contracting:
             return {
diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py
index 08f68acaa..aab0d7c6d 100644
--- a/hypha/apply/projects/forms/__init__.py
+++ b/hypha/apply/projects/forms/__init__.py
@@ -7,6 +7,7 @@ from .payment import (
 from .project import (
     ApproveContractForm,
     ApproversForm,
+    AssignApproversForm,
     ChangePAFStatusForm,
     ChangeProjectStatusForm,
     CreateProjectForm,
@@ -37,6 +38,7 @@ __all__ = [
     'SubmitContractDocumentsForm',
     'ApproveContractForm',
     'ApproversForm',
+    'AssignApproversForm',
     'ChangePAFStatusForm',
     'ChangeProjectStatusForm',
     'CreateProjectForm',
diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py
index 2a3db2099..2a564d523 100644
--- a/hypha/apply/projects/forms/project.py
+++ b/hypha/apply/projects/forms/project.py
@@ -1,6 +1,6 @@
 from django import forms
 from django.contrib.auth import get_user_model
-from django.db.models import Q
+from django.db.models import Count, Q
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 from django_file_form.forms import FileFormMixin
@@ -33,6 +33,17 @@ def filter_request_choices(choices):
     return [(k, v) for k, v in PROJECT_STATUS_CHOICES if k in choices]
 
 
+def get_latest_project_paf_approval_via_roles(project, roles):
+    # exact match the roles with paf approval's reviewer roles
+    paf_approvals = project.paf_approvals.annotate(
+        roles_count=Count('paf_reviewer_role__user_roles')
+    ).filter(roles_count=len(list(roles)), approved=False)
+
+    for role in roles:
+        paf_approvals = paf_approvals.filter(paf_reviewer_role__user_roles__id=role.id)
+    return paf_approvals.first()
+
+
 class ApproveContractForm(forms.Form):
     id = forms.IntegerField(widget=forms.HiddenInput())
 
@@ -224,47 +235,39 @@ class ApproversForm(forms.ModelForm):
         widgets = {'id': forms.HiddenInput()}
 
     def __init__(self, user=None, *args, **kwargs):
+        from hypha.apply.activity.adapters.utils import get_users_for_groups
         super().__init__(*args, **kwargs)
 
         for paf_reviewer_role in PAFReviewersRole.objects.all():
-            users = User.objects.all()
-            for group in paf_reviewer_role.user_roles.all():
-                users = users.filter(groups__name=group)
+            users = get_users_for_groups(list(paf_reviewer_role.user_roles.all()), exact_match=True)
             approval = PAFApprovals.objects.filter(project=self.instance, paf_reviewer_role=paf_reviewer_role)
             if approval:
                 initial_user = approval.first().user
             self.fields[slugify(paf_reviewer_role.label)] = forms.ModelChoiceField(
                 queryset=users,
+                required=False,
+                blank=True,
                 label=paf_reviewer_role.label,
                 initial=initial_user if approval else None,
                 disabled=approval.first().approved if approval.first() else False,
                 # using approval.first() as condition for existing projects
             )
 
-    def clean(self):
-        cleaned_data = super().clean()
-
-        paf_reviewer_roles = PAFReviewersRole.objects.all()
-        if paf_reviewer_roles:
-            for paf_reviewer_role in paf_reviewer_roles:
-                if not cleaned_data[slugify(paf_reviewer_role.label)]:
-                    self.add_error(slugify(paf_reviewer_role.label))
-        return cleaned_data
-
     def save(self, commit=True):
         # add users as PAFApprovals
         for paf_reviewer_role in PAFReviewersRole.objects.all():
+            assigned_user = self.cleaned_data[slugify(paf_reviewer_role.label)]
             paf_approvals = PAFApprovals.objects.filter(project=self.instance, paf_reviewer_role=paf_reviewer_role)
             if not paf_approvals.exists():
                 PAFApprovals.objects.create(
                     project=self.instance,
                     paf_reviewer_role=paf_reviewer_role,
-                    user=self.cleaned_data[slugify(paf_reviewer_role.label)],
+                    user=assigned_user if assigned_user else None,
                     approved=False,
                 )
             elif not paf_approvals.first().approved:
                 paf_approval = paf_approvals.first()
-                paf_approval.user = self.cleaned_data[slugify(paf_reviewer_role.label)]
+                paf_approval.user = assigned_user if assigned_user else None
                 paf_approval.save()
         return super().save(commit=True)
 
@@ -274,10 +277,51 @@ class SetPendingForm(ApproversForm):
         if self.instance.status != DRAFT:
             raise forms.ValidationError(_('A Project can only be sent for Approval when Drafted.'))
 
+        # :todo: we should have a check form contains enough data to create PAF Approvals
         cleaned_data = super().clean()
         return cleaned_data
 
 
+class AssignApproversForm(forms.ModelForm):
+    class Meta:
+        fields = ['id']
+        model = Project
+        widgets = {'id': forms.HiddenInput()}
+
+    def __init__(self, user=None, *args, **kwargs):
+        from hypha.apply.activity.adapters.utils import get_users_for_groups
+        super().__init__(*args, **kwargs)
+        self.user = user
+
+        paf_approval = get_latest_project_paf_approval_via_roles(project=self.instance, roles=user.groups.all())
+
+        if paf_approval:
+            current_paf_reviewer_role = paf_approval.paf_reviewer_role
+
+            users = get_users_for_groups(list(current_paf_reviewer_role.user_roles.all()), exact_match=True)
+
+            self.fields[slugify(current_paf_reviewer_role.label)] = forms.ModelChoiceField(
+                queryset=users,
+                required=False,
+                blank=True,
+                label=current_paf_reviewer_role.label,
+                initial=paf_approval.user,
+                disabled=paf_approval.approved,
+            )
+
+    def save(self, commit=True):
+        paf_approval = get_latest_project_paf_approval_via_roles(project=self.instance, roles=self.user.groups.all())
+
+        current_paf_reviewer_role = paf_approval.paf_reviewer_role
+        assigned_user = self.cleaned_data[slugify(current_paf_reviewer_role.label)]
+
+        if not paf_approval.approved:
+            paf_approval.user = assigned_user if assigned_user else None
+            paf_approval.save()
+
+        return super().save(commit=True)
+
+
 class SubmitContractDocumentsForm(forms.ModelForm):
     class Meta:
         fields = ['id']
diff --git a/hypha/apply/projects/migrations/0075_alter_pafapprovals_user.py b/hypha/apply/projects/migrations/0075_alter_pafapprovals_user.py
new file mode 100644
index 000000000..32369c66b
--- /dev/null
+++ b/hypha/apply/projects/migrations/0075_alter_pafapprovals_user.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2.18 on 2023-05-04 12:21
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('application_projects', '0074_update_projects_status_committed_to_draft'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='pafapprovals',
+            name='user',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paf_approvals', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py
index cbb7867a9..cf00f475b 100644
--- a/hypha/apply/projects/models/project.py
+++ b/hypha/apply/projects/models/project.py
@@ -499,7 +499,7 @@ class ProjectSettings(BaseSiteSetting, ClusterableModel):
 class PAFApprovals(models.Model):
     project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="paf_approvals")
     paf_reviewer_role = models.ForeignKey("PAFReviewersRole", on_delete=models.CASCADE, related_name="paf_approvals")
-    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="paf_approvals")
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="paf_approvals")
     approved = models.BooleanField(default=False)
     created_at = models.DateTimeField(auto_now_add=True)
     updated_at = models.DateTimeField()
diff --git a/hypha/apply/projects/permissions.py b/hypha/apply/projects/permissions.py
index d7548b885..5c460f016 100644
--- a/hypha/apply/projects/permissions.py
+++ b/hypha/apply/projects/permissions.py
@@ -1,5 +1,7 @@
 from django.core.exceptions import PermissionDenied
 
+from hypha.apply.activity.adapters.utils import get_users_for_groups
+
 from .models.project import (
     CLOSING,
     COMPLETE,
@@ -66,6 +68,100 @@ def can_submit_contract_documents(user, project, **kwargs):
     return False, 'Forbidden Error'
 
 
+def can_update_paf_approvers(user, project, **kwargs):
+    if not user.is_authenticated:
+        return False, 'Login Required'
+
+    if project.status != WAITING_FOR_APPROVAL:
+        return False, 'PAF Approvers can be updated only in Waiting for approval state'
+    if user == project.lead:
+        return True, 'Lead can update approvers in approval state'
+    if not project.paf_approvals.exists():
+        return False, 'No user can update approvers without paf approval, except lead(lead can add paf approvals)'
+
+    request = kwargs.get('request')
+    project_settings = ProjectSettings.for_request(request)
+    if project_settings.paf_approval_sequential:
+        next_paf_approval = project.paf_approvals.filter(approved=False).first()
+        if next_paf_approval:
+            if next_paf_approval.user and user in get_users_for_groups(list(next_paf_approval.paf_reviewer_role.user_roles.all()), exact_match=True):
+                return True, 'PAF Reviewer-roles users can update next approval approvers if any approvers assigned'
+        return False, 'Forbidden Error'
+    else:
+        approvers_ids = []
+        for approval in project.paf_approvals.filter(approved=False, user__isnull=False):
+            approvers_ids.extend(assigner.id for assigner in
+                                 get_users_for_groups(list(approval.paf_reviewer_role.user_roles.all()),
+                                                      exact_match=True))
+        if user.id in approvers_ids:
+            return True, 'PAF Reviewer-roles users can update approvers'
+    return False, 'Forbidden Error'
+
+
+def can_update_assigned_paf_approvers(user, project, **kwargs):
+    """
+    Only for Approvers teams members(with PAFReviewerRoles' user_roles' users)
+    UpdateAssignApproversView will be used by only approvers teams members.
+    """
+    if not user.is_authenticated:
+        return False, 'Login Required'
+    if project.status != WAITING_FOR_APPROVAL:
+        return False, 'PAF approvers can be assigned only in Waiting for Approval state'
+    if not project.paf_approvals.exists():
+        return False, 'No user can assign approvers with paf_approvals'
+
+    request = kwargs.get('request')
+    project_settings = ProjectSettings.for_request(request)
+    if project_settings.paf_approval_sequential:
+        next_paf_approval = project.paf_approvals.filter(approved=False).first()
+        if next_paf_approval:
+            if user in get_users_for_groups(list(next_paf_approval.paf_reviewer_role.user_roles.all()),
+                                                exact_match=True):
+                return True, 'PAF Reviewer-roles users can assign approvers'
+            return False, 'Forbidden Error'
+        return False, 'Forbidden Error'
+    else:
+        assigners_ids = []
+        for approval in project.paf_approvals.filter(approved=False):
+            assigners_ids.extend(assigner.id for assigner in
+                                 get_users_for_groups(list(approval.paf_reviewer_role.user_roles.all()),
+                                                      exact_match=True))
+        if user.id in assigners_ids:
+            return True, 'PAF Reviewer-roles users can assign approvers'
+    return False, 'Forbidden Error'
+
+
+def can_assign_paf_approvers(user, project, **kwargs):
+    if not user.is_authenticated:
+        return False, 'Login Required'
+
+    if project.status != WAITING_FOR_APPROVAL:
+        return False, 'PAF approvers can be assigned only in Waiting for Approval state'
+    if not project.paf_approvals.exists():
+        return False, 'No user can assign approvers with paf_approvals'
+
+    request = kwargs.get('request')
+    project_settings = ProjectSettings.for_request(request)
+    if project_settings.paf_approval_sequential:
+        next_paf_approval = project.paf_approvals.filter(approved=False).first()
+        if next_paf_approval:
+            if next_paf_approval.user:
+                return False, 'User already assigned'
+            else:
+                if user in get_users_for_groups(list(next_paf_approval.paf_reviewer_role.user_roles.all()), exact_match=True):
+                    return True, 'PAF Reviewer-roles users can assign approvers'
+            return False, 'Forbidden Error'
+        return False, 'Forbidden Error'
+    else:
+        assigners_ids = []
+        for approval in project.paf_approvals.filter(approved=False, user__isnull=True):
+            assigners_ids.extend(assigner.id for assigner in get_users_for_groups(list(approval.paf_reviewer_role.user_roles.all()), exact_match=True))
+
+        if user.id in assigners_ids:
+            return True, 'PAF Reviewer-roles users can assign approvers'
+    return False, 'Forbidden Error'
+
+
 def can_update_paf_status(user, project, **kwargs):
     if not user.is_authenticated:
         return False, 'Login Required'
@@ -80,7 +176,8 @@ def can_update_paf_status(user, project, **kwargs):
     if request:
         project_settings = ProjectSettings.for_request(request)
         if project_settings.paf_approval_sequential:
-            if user.id == project.paf_approvals.filter(approved=False).first().user.id:
+            approver = project.paf_approvals.filter(approved=False).first().user
+            if approver and user.id == approver.id:
                 return True, 'Next Approver can approve PAF(For Sequential Approvals)'
             return False, 'Only Next can approve PAF(For Sequential Approvals)'
         if user.id in project.paf_approvals.filter(approved=False).values_list('user', flat=True):
@@ -170,9 +267,13 @@ def can_access_project(user, project):
     if user.is_applicant and user == project.user:
         return True, 'Applicant(project user) can view project in all statuses'
 
-    if project.status in [DRAFT, WAITING_FOR_APPROVAL] and project.paf_approvals.exists() and \
-        user.id in project.paf_approvals.all().values_list('user', flat=True):
-        return True, 'PAF Approvers can access the project in Draft and Approval state'
+    if project.status in [DRAFT, WAITING_FOR_APPROVAL, CONTRACTING] and project.paf_approvals.exists():
+        paf_reviewer_roles_users_ids = []
+        for approval in project.paf_approvals.all():
+            paf_reviewer_roles_users_ids.extend([role_user.id for role_user in get_users_for_groups(
+                list(approval.paf_reviewer_role.user_roles.all()), exact_match=True)])
+        if user.id in paf_reviewer_roles_users_ids:
+            return True, 'PAF Approvers can access the project in Draft, Approval state and after approval state'
 
     return False, 'Forbidden Error'
 
@@ -181,6 +282,9 @@ permissions_map = {
     'contract_approve': can_approve_contract,
     'contract_upload': can_upload_contract,
     'paf_status_update': can_update_paf_status,
+    'paf_approvers_update': can_update_paf_approvers,
+    'paf_approvers_assign': can_assign_paf_approvers,
+    'update_paf_assigned_approvers': can_update_assigned_paf_approvers,  # Permission for UpdateAssignApproversView
     'project_status_update': can_update_project_status,
     'project_reports_update': can_update_project_reports,
     'report_update': can_update_report,
diff --git a/hypha/apply/projects/tables.py b/hypha/apply/projects/tables.py
index 823e5ef2f..ac4f24b44 100644
--- a/hypha/apply/projects/tables.py
+++ b/hypha/apply/projects/tables.py
@@ -106,6 +106,22 @@ class ProjectsDashboardTable(BaseProjectsTable):
         attrs = {'class': 'projects-table'}
 
 
+class ProjectsAssigneeDashboardTable(BaseProjectsTable):
+    class Meta:
+        fields = [
+            'title',
+            'fund',
+            'lead',
+            'reporting',
+            'last_payment_request',
+            'end_date',
+        ]
+        model = Project
+        orderable = False
+        exclude = ['status']
+        attrs = {'class': 'projects-table'}
+
+
 class ProjectsListTable(BaseProjectsTable):
     class Meta:
         fields = [
diff --git a/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html b/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html
index e696b7bd9..8baa8996e 100644
--- a/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html
+++ b/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html
@@ -10,6 +10,7 @@
             <svg class="icon icon--caret-down" x-show="collapsed" aria-hidden=true><use xlink:href="#caret-down"></use></svg>
             {% endif %}
         </div>
+        <div>
         {% user_can_send_for_approval object user as can_send_to_approve %}
         {% if can_send_to_approve %}
             <a data-fancybox
@@ -23,24 +24,43 @@
                 {% endif %}
             </a>
         {% endif %}
-        {% user_can_update_paf_approvers object user as can_update_paf_approvers %}
+        {% user_can_update_paf_approvers object user request as can_update_paf_approvers %}
+        {% user_can_assign_approvers_to_project object user request as can_assign_paf_approvers %}
         {% if can_update_paf_approvers %}
+            {% if user == project.lead %}
             <a data-fancybox
             data-src="#update-paf-approvers"
-            class="button button--project-action button--project-action--white"
+            class="button button--project-action button--project-action--white ml-2"
             href="#">
                 {% trans "View/Update Approvers" %}
             </a>
+            {% else %}
+            <a data-fancybox
+            data-src="#change-assigned-paf-approvers"
+            class="button button--project-action button--project-action--white ml-2"
+            href="#">
+                {% trans "Change approver" %}
+            </a>
+            {% endif %}
+        {% endif %}
+        {% if can_assign_paf_approvers %}
+            <a data-fancybox
+            data-src="#assign-paf-approvers"
+            class="button button--project-action ml-2"
+            href="#">
+                {% trans "Assign approver" %}
+            </a>
         {% endif %}
         {% user_can_update_paf_status object user request=request as can_update_paf_status %}
         {% if object.can_make_approval and can_update_paf_status %}
             <a data-fancybox
             data-src="#update-paf-status"
-            class="button button--project-action"
+            class="button button--project-action ml-2"
             href="#">
                 {% trans "Update Status" %}
             </a>
         {% endif %}
+        </div>
     </div>
     <ul class="docs-block__inner" id="project-documents-elements" {% if collapsible_header %} x-show="!collapsed" role="region"
        aria-labelledby="project-documents-section" {% endif %}>
@@ -227,7 +247,8 @@
     <h4 class="modal__project-header-bar">{% trans "Submit for Approval" %}</h4>
 
     {% if remaining_document_categories %}
-        <h5>{% trans "Are you sure you're ready to submit?" %}</h5>
+        <h5>{% trans "Are you sure you're ready to submit the project documents to be approved in" %}
+            {% if project_settings.paf_approval_sequential %}{% trans "sequential order?" %}{% else %}{% trans "parallel order?" %}{% endif %}</h5>
 
         <p>{% trans "This project is missing the following documents" %}:</p>
 
@@ -238,12 +259,19 @@
         </ul>
         {% trans "Submit anyway" as submit %}
     {% else %}
+        <h5>{% trans "Are you ready to submit the project documents to be approved in" %}
+            {% if project_settings.paf_approval_sequential %}{% trans "sequential order?" %}{% else %}{% trans "parallel order?" %}{% endif %}</h5>
         {% trans "Submit" as submit %}
     {% endif %}
     {% if project_settings.paf_reviewers_roles.all %}
-        <p> <strong> {% trans "Please select approvers for PAF in " %} {% if project_settings.paf_approval_sequential %} {% trans "sequential order" %} {% else %}{% trans "parallel order" %}{% endif %}</strong></p>
-        <p>{% trans "(please note that in "%}{% if project_settings.paf_approval_sequential %}{%trans "sequential order, approvers will approve PAF one after the other)"%}{% else %}{% trans "parallel order, approvers can approve PAF anytime)" %}{% endif %}</p>
-        <br>
+
+        <div class="flex items-center text-sm">
+            <p class="flex-shrink text-slate-500 pr-2 mb-0">Optional</p>
+            <p class="flex-grow h-px bg-mid-grey mb-0"></p>
+        </div>
+
+        <p>{% trans "Please note that in "%}{% if project_settings.paf_approval_sequential %}{%trans "sequential order, approvers will approve PAF one after the other"%}{% else %}{% trans "parallel order, approvers can approve PAF anytime" %}{% endif %}</p>
+
         {% include 'funds/includes/delegated_form_base.html' with form=request_approval_form value=submit %}
     {% else %}
         <p>{% trans "No PAF Reviewer Roles created yet, please create these in " %}
@@ -268,12 +296,28 @@
         </p>
     {% endif %}
 </div>
+
+<div class="modal" id="change-assigned-paf-approvers">
+        <h4 class="modal__project-header-bar">{% trans "Change Approver" %}</h4>
+        <p class="text-mid-grey">{% trans "Selected approver will be notified. On unselecting, every listed member here will be notified." %} </p>
+        {% trans "Submit" as submit %}
+        {% include 'funds/includes/delegated_form_base.html' with form=assign_approvers_form value=submit %}
+    </div>
+
+{% endif %}
+
+{% if can_assign_paf_approvers %}
+    <div class="modal" id="assign-paf-approvers">
+        <h4 class="modal__project-header-bar">{% trans "Assign Approver" %}</h4>
+        <p class="text-mid-grey">{% trans "Selected approver will be notified. On unselecting, every listed member here will be notified." %} </p>
+        {% trans "Submit" as submit %}
+        {% include 'funds/includes/delegated_form_base.html' with form=assign_approvers_form value=submit %}
+    </div>
 {% endif %}
 
 {% if can_update_paf_status %}
     <div class="modal" id="update-paf-status">
         <h4 class="modal__project-header-bar">{% trans "Update PAF Status" %}</h4>
-        <p>{% trans "Project's current status" %}: {{ object.get_status_display }}</p>
         {% trans "Update Status" as update %}
         {% include 'funds/includes/delegated_form_base.html' with form=change_paf_status value=update %}
     </div>
diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html
index cf1134f9b..8565b9d40 100644
--- a/hypha/apply/projects/templates/application_projects/project_detail.html
+++ b/hypha/apply/projects/templates/application_projects/project_detail.html
@@ -153,7 +153,7 @@
             {% block sidebar %}
 
             <aside class="sidebar">
-                {% user_next_step_on_project object user as next_step %}
+                {% user_next_step_on_project object user request=request as next_step %}
                 {% if next_step %}
                     {% if mobile %}
                     <a class="js-actions-toggle button button--white button--full-width button--actions">{% trans "Next Step" %}</a>
@@ -187,7 +187,7 @@
                             {% if paf_approval.approved %}
                                 <p class="sidebar__paf-approvals--approved m-0">{% trans "Approved by " %}{{ paf_approval.user }} {% if paf_approval.approved_at %}(<i>{{ paf_approval.approved_at|date }}</i>){% endif %}</p>
                             {% else %}
-                                <p class="sidebar__paf-approvals--pending m-0">{% trans "Pending approval from " %}{{ paf_approval.user }}</p>
+                                <p class="sidebar__paf-approvals--pending m-0">{% trans "Pending approval from " %}{% if paf_approval.user %}{{ paf_approval.user }}{% else %} {{ paf_approval.paf_reviewer_role.label }}{% trans " (nobody assigned yet)" %} {% endif %}</p>
                             {% endif %}
                             <br>
                         {% endfor %}
diff --git a/hypha/apply/projects/templatetags/approval_tools.py b/hypha/apply/projects/templatetags/approval_tools.py
index db10faca1..3374fd36d 100644
--- a/hypha/apply/projects/templatetags/approval_tools.py
+++ b/hypha/apply/projects/templatetags/approval_tools.py
@@ -1,6 +1,5 @@
 from django import template
 
-from ..models.project import WAITING_FOR_APPROVAL
 from ..permissions import has_permission
 
 register = template.Library()
@@ -22,14 +21,15 @@ def user_can_send_for_approval(project, user):
 
 
 @register.simple_tag
-def user_can_update_paf_approvers(project, user):
-    if not project.status == WAITING_FOR_APPROVAL:
-        return False
-    if user.paf_approvals.filter(project=project).exists():
-        return False
-    if project.paf_approvals.exists() and user.is_apply_staff:
-        return True
-    return False
+def user_can_update_paf_approvers(project, user, request):
+    permission, _ = has_permission('paf_approvers_update', user, object=project, raise_exception=False, request=request)
+    return permission
+
+
+@register.simple_tag
+def user_can_assign_approvers_to_project(project, user, request):
+    permission, _ = has_permission('paf_approvers_assign', user, object=project, raise_exception=False, request=request)
+    return permission
 
 
 @register.simple_tag
diff --git a/hypha/apply/projects/templatetags/project_tags.py b/hypha/apply/projects/templatetags/project_tags.py
index 7b20c9ac9..b145c3512 100644
--- a/hypha/apply/projects/templatetags/project_tags.py
+++ b/hypha/apply/projects/templatetags/project_tags.py
@@ -1,4 +1,5 @@
 from django import template
+from django.db.models import Count
 from django.urls import reverse
 
 from hypha.apply.projects.models.project import (
@@ -22,7 +23,8 @@ def project_can_have_report(project):
 
 
 @register.simple_tag
-def user_next_step_on_project(project, user):
+def user_next_step_on_project(project, user, request=None):
+    from hypha.apply.projects.models.project import PAFReviewersRole, ProjectSettings
     if project.status == DRAFT:
         if user.is_apply_staff:
             if not project.user_has_updated_details:
@@ -34,10 +36,33 @@ def user_next_step_on_project(project, user):
             return "Changes requested. Awaiting documents to be resubmitted."
         return "Awaiting approval form to be created."
     elif project.status == WAITING_FOR_APPROVAL:
-        if user.id in project.paf_approvals.values_list('user', flat=True):
-            return "Awaiting project approval from assigned approvers. Please review and update status"
         if user.is_applicant:
             return "Awaiting approval form to be approved."
+
+        if request:
+            project_settings = ProjectSettings.for_request(request=request)
+            if project_settings.paf_approval_sequential:
+                latest_unapproved_approval = project.paf_approvals.filter(approved=False).first()
+                if latest_unapproved_approval:
+                    if latest_unapproved_approval.user:
+                        return f"Awaiting approval. Assigned to {latest_unapproved_approval.user}"
+                    return f"Awaiting {latest_unapproved_approval.paf_reviewer_role.label} to assign an approver"
+            else:
+                matched_roles = PAFReviewersRole.objects.annotate(roles_count=Count('user_roles')).filter(
+                    roles_count=len(user.groups.all()))
+                for group in user.groups.all():
+                    matched_roles = matched_roles.filter(user_roles__id=group.id)
+                if not matched_roles:
+                    return "Awaiting PAF approval form to be approved"
+                else:
+                    matched_unapproved_approval = project.paf_approvals.filter(approved=False, paf_reviewer_role__in=matched_roles)
+                    if not matched_unapproved_approval.exists():
+                        return "Awaiting approval from other approvers teams"
+                    else:
+                        if matched_unapproved_approval.first().user:
+                            return f"Awaiting approval. Assigned to {matched_unapproved_approval.first().user}"
+                        return f"Awaiting {matched_unapproved_approval.first().paf_reviewer_role.label} to assign an approver"
+
         return "Awaiting project approval from assigned approvers"
     elif project.status == CONTRACTING:
         if not project.contracts.exists():
diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py
index d654542ff..3520e21a9 100644
--- a/hypha/apply/projects/tests/test_views.py
+++ b/hypha/apply/projects/tests/test_views.py
@@ -85,6 +85,14 @@ class TestSendForApprovalView(BaseViewTestCase):
     url_name = 'funds:projects:{}'
     user_factory = StaffFactory
 
+    def setUp(self):
+        super().setUp()
+        apply_site = ApplySiteFactory()
+        self.project_setting, _ = ProjectSettings.objects.get_or_create(site_id=apply_site.id)
+        self.project_setting.use_settings = True
+        self.project_setting.save()
+        self.role = PAFReviewerRoleFactory(page=self.project_setting)
+
     def get_kwargs(self, instance):
         return {'pk': instance.id}
 
diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py
index 9399e1120..fba849a5f 100644
--- a/hypha/apply/projects/views/project.py
+++ b/hypha/apply/projects/views/project.py
@@ -52,6 +52,7 @@ from ..filters import InvoiceListFilter, ProjectListFilter, ReportListFilter
 from ..forms import (
     ApproveContractForm,
     ApproversForm,
+    AssignApproversForm,
     ChangePAFStatusForm,
     ChangeProjectStatusForm,
     ProjectApprovalForm,
@@ -104,12 +105,43 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView):
 
         response = super().form_valid(form)
 
-        messenger(
-            MESSAGES.SEND_FOR_APPROVAL,
-            request=self.request,
-            user=self.request.user,
-            source=self.object,
-        )
+        project_settings = ProjectSettings.for_request(self.request)
+
+        paf_approvals = self.object.paf_approvals.filter(approved=False)
+
+        if project_settings.paf_approval_sequential:
+            if paf_approvals:
+                user = paf_approvals.first().user
+                if user:
+                    # notify only if first user/approver is updated
+                    messenger(
+                        MESSAGES.SEND_FOR_APPROVAL,
+                        request=self.request,
+                        user=self.request.user,
+                        source=self.object,
+                    )
+                else:
+                    messenger(
+                        MESSAGES.ASSIGN_PAF_APPROVER,
+                        request=self.request,
+                        user=self.request.user,
+                        source=self.object,
+                    )
+        else:
+            if paf_approvals.filter(user__isnull=False).exists():
+                messenger(
+                    MESSAGES.SEND_FOR_APPROVAL,
+                    request=self.request,
+                    user=self.request.user,
+                    source=self.object,
+                )
+            if paf_approvals.filter(user__isnull=True).exists():
+                messenger(
+                    MESSAGES.ASSIGN_PAF_APPROVER,
+                    request=self.request,
+                    user=self.request.user,
+                    source=self.object,
+                )
 
         project.status = WAITING_FOR_APPROVAL
         project.save(update_fields=['status'])
@@ -509,12 +541,20 @@ class ChangePAFStatusView(DelegatedViewMixin, UpdateView):
             if project_settings.paf_approval_sequential:
                 # notify next approver
                 if self.object.paf_approvals.filter(approved=False).exists():
-                    messenger(
-                        MESSAGES.APPROVE_PAF,
-                        request=self.request,
-                        user=self.request.user,
-                        source=self.object,
-                    )
+                    if self.object.paf_approvals.filter(approved=False).first().user:
+                        messenger(
+                            MESSAGES.APPROVE_PAF,
+                            request=self.request,
+                            user=self.request.user,
+                            source=self.object,
+                        )
+                    else:
+                        messenger(
+                            MESSAGES.ASSIGN_PAF_APPROVER,
+                            request=self.request,
+                            user=self.request.user,
+                            source=self.object,
+                        )
             messages.success(self.request, _("PAF has been approved"),
                              extra_tags=PROJECT_ACTION_MESSAGE_TAG)
 
@@ -591,15 +631,59 @@ class ChangeProjectstatusView(DelegatedViewMixin, UpdateView):
         return response
 
 
+@method_decorator(login_required, name='dispatch')
+class UpdateAssignApproversView(DelegatedViewMixin, UpdateView):
+    context_name = 'assign_approvers_form'
+    form_class = AssignApproversForm
+    model = Project
+
+    def dispatch(self, request, *args, **kwargs):
+        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
+        permission, _ = has_permission('update_paf_assigned_approvers', request.user, self.project,
+                                       raise_exception=True, request=request)
+        return super().dispatch(request, *args, **kwargs)
+
+    def form_valid(self, form):
+        from ..forms.project import get_latest_project_paf_approval_via_roles
+        project = self.kwargs['object']
+
+        response = super().form_valid(form)
+
+        paf_approval = get_latest_project_paf_approval_via_roles(project=project, roles=self.request.user.groups.all())
+
+        if paf_approval.user:
+            messenger(
+                MESSAGES.APPROVE_PAF,
+                request=self.request,
+                user=self.request.user,
+                source=self.object,
+            )
+        else:
+            messenger(
+                MESSAGES.ASSIGN_PAF_APPROVER,
+                request=self.request,
+                user=self.request.user,
+                source=self.object,
+            )
+
+        return response
+
+
 class UpdatePAFApproversView(DelegatedViewMixin, UpdateView):
     context_name = 'update_approvers_form'
     form_class = ApproversForm
     model = Project
 
+    def dispatch(self, request, *args, **kwargs):
+        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
+        permission, _ = has_permission('paf_approvers_update', request.user, self.project, raise_exception=True, request=request)
+        return super().dispatch(request, *args, **kwargs)
+
     def form_valid(self, form):
         project = self.kwargs['object']
 
         project_settings = ProjectSettings.for_request(self.request)
+        old_approvers = None
         if self.object.paf_approvals.exists():
             old_approvers = list(project.paf_approvals.filter(approved=False).values_list('user__id', flat=True))
 
@@ -611,28 +695,51 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView):
             # if approvers exists already
             if project_settings.paf_approval_sequential:
                 user = paf_approvals.first().user
-                if user.id != old_approvers[0]:
+                if user and user.id != old_approvers[0]:
                     # notify only if first user/approver is updated
                     messenger(
                         MESSAGES.APPROVE_PAF,
                         request=self.request,
-                        user=self.object.user,
+                        user=self.request.user,
+                        source=self.object,
+                    )
+                elif not user:
+                    messenger(
+                        MESSAGES.ASSIGN_PAF_APPROVER,
+                        request=self.request,
+                        user=self.request.user,
                         source=self.object,
                     )
             else:
+                if paf_approvals.filter(user__isnull=False).exists():
+                    messenger(
+                        MESSAGES.APPROVE_PAF,
+                        request=self.request,
+                        user=self.request.user,
+                        source=self.object,
+                    )
+                if paf_approvals.filter(user__isnull=True).exists():
+                    messenger(
+                        MESSAGES.ASSIGN_PAF_APPROVER,
+                        request=self.request,
+                        user=self.request.user,
+                        source=self.object,
+                    )
+        elif paf_approvals:
+            if paf_approvals.filter(user__isnull=False).exists():
                 messenger(
                     MESSAGES.APPROVE_PAF,
                     request=self.request,
-                    user=self.object.user,
+                    user=self.request.user,
+                    source=self.object,
+                )
+            if paf_approvals.filter(user__isnull=True).exists():
+                messenger(
+                    MESSAGES.ASSIGN_PAF_APPROVER,
+                    request=self.request,
+                    user=self.request.user,
                     source=self.object,
                 )
-        elif paf_approvals:
-            messenger(
-                MESSAGES.APPROVE_PAF,
-                request=self.request,
-                user=self.object.user,
-                source=self.object,
-            )
 
         messages.success(self.request, _("PAF approvers have been updated"), extra_tags=PROJECT_ACTION_MESSAGE_TAG)
         return response
@@ -665,6 +772,7 @@ class AdminProjectDetailView(
         UpdateLeadView,
         UploadContractView,
         UploadDocumentView,
+        UpdateAssignApproversView,
         ChangePAFStatusView,
         ChangeProjectstatusView,
         ChangeInvoiceStatusView,
diff --git a/tailwind.config.js b/tailwind.config.js
index c022d8117..fd02a9d18 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -6,6 +6,7 @@ module.exports = {
             colors: {
                 'light-blue' : '#0d7db0',
                 'tomato': '#f05e54',
+                'mid-grey': '#cfcfcf',
             },
         },
     },
-- 
GitLab