diff --git a/submit-api/src/submit_api/models/proponent.py b/submit-api/src/submit_api/models/proponent.py
index 69231568..9b37dbeb 100644
--- a/submit-api/src/submit_api/models/proponent.py
+++ b/submit-api/src/submit_api/models/proponent.py
@@ -8,11 +8,13 @@
from .account import Account
from .account_project import AccountProject
+from .account_user import AccountUser
from .base_model import BaseModel
from .invitations import Invitations
from .project import Project
from ..enums.invitation_status import InvitationStatus
from ..enums.proponent_status import ProponentStatus
+from ..enums.role import RoleEnum
class Proponent(BaseModel):
@@ -50,7 +52,13 @@ def get_all_proponents(cls, include_deleted=False, approved_conditions_only=None
return query.order_by(cls.name).all()
@classmethod
- def get_proponent_by_id(cls, proponent_id, include_invitations=False, include_projects=False):
+ def get_proponent_by_id(
+ cls,
+ proponent_id,
+ include_invitations=False,
+ include_projects=False,
+ include_administrators=False,
+ ):
"""Get proponent by id.
Args:
@@ -70,7 +78,7 @@ def get_proponent_by_id(cls, proponent_id, include_invitations=False, include_pr
"name": proponent.name,
"status": proponent.status.value if proponent.status else None
}
- if not include_invitations and not include_projects:
+ if not include_invitations and not include_projects and not include_administrators:
return proponent_dict
accounts_ids = Account.query.with_entities(Account.id).filter_by(proponent_id=proponent_id).all()
@@ -93,4 +101,38 @@ def get_proponent_by_id(cls, proponent_id, include_invitations=False, include_pr
"project_id": account_project.project_id,
} for account_project in account_projects]
+ if include_administrators and accounts_ids:
+ proponent_dict["administrators"] = cls._build_administrators(
+ AccountUser.query.filter(
+ AccountUser.account_id.in_(accounts_ids)
+ ).all()
+ )
+
return proponent_dict
+
+ @classmethod
+ def _build_administrators(cls, account_users):
+ """Build administrators list from account users."""
+ administrators = []
+ for user in account_users:
+ user_role = getattr(user, "role", None)
+ if not user_role or not user_role.active:
+ continue
+
+ if user_role.role.role_name != RoleEnum.ACCOUNT_PRIMARY_ADMIN.value:
+ continue
+
+ if not user.user_id:
+ continue
+
+ administrators.append({
+ "id": user.id,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "full_name": user.full_name,
+ "position": user.position,
+ "company_name": user.company_name,
+ "work_contact_number": user.work_contact_number,
+ "work_email_address": user.work_email_address,
+ })
+ return administrators
diff --git a/submit-api/src/submit_api/resources/migration_ops.py b/submit-api/src/submit_api/resources/migration_ops.py
index 50a96383..9121a46f 100644
--- a/submit-api/src/submit_api/resources/migration_ops.py
+++ b/submit-api/src/submit_api/resources/migration_ops.py
@@ -65,8 +65,8 @@ def post(): # pylint: disable=too-many-locals,too-many-statements
# Query all foreign key constraints that reference users.auth_guid
current_app.logger.info("Querying foreign key constraints on users.auth_guid...")
fk_query = text("""
- SELECT
- tc.table_name,
+ SELECT
+ tc.table_name,
tc.constraint_name,
kcu.column_name
FROM information_schema.table_constraints AS tc
diff --git a/submit-api/src/submit_api/resources/proponent/proponent.py b/submit-api/src/submit_api/resources/proponent/proponent.py
index a589264f..5b9f88c4 100644
--- a/submit-api/src/submit_api/resources/proponent/proponent.py
+++ b/submit-api/src/submit_api/resources/proponent/proponent.py
@@ -83,7 +83,13 @@ def get(proponent_id):
"""Get a proponent by id."""
include_invitations = request.args.get("include-invitations", "false").lower() == "true"
include_projects = request.args.get("include-projects", "false").lower() == "true"
- proponent = ProponentService.get_proponent(proponent_id, include_invitations, include_projects)
+ include_administrators = request.args.get("include-administrators", "false").lower() == "true"
+ proponent = ProponentService.get_proponent(
+ proponent_id,
+ include_invitations,
+ include_projects,
+ include_administrators,
+ )
if not proponent:
raise ResourceNotFoundError(f"Proponent with id {proponent_id} not found")
return proponent, HTTPStatus.OK
diff --git a/submit-api/src/submit_api/services/proponent_service.py b/submit-api/src/submit_api/services/proponent_service.py
index 5f399bd2..34260d52 100644
--- a/submit-api/src/submit_api/services/proponent_service.py
+++ b/submit-api/src/submit_api/services/proponent_service.py
@@ -6,12 +6,19 @@ class ProponentService:
"""Proponent management service."""
@classmethod
- def get_proponent(cls, proponent_id, include_invitations=False, include_projects=False):
+ def get_proponent(
+ cls,
+ proponent_id,
+ include_invitations=False,
+ include_projects=False,
+ include_administrators=False,
+ ):
"""Get proponent by id."""
return Proponent.get_proponent_by_id(
proponent_id,
include_invitations=include_invitations,
- include_projects=include_projects
+ include_projects=include_projects,
+ include_administrators=include_administrators,
)
@classmethod
diff --git a/submit-web/src/components/App/Proponents/Contacts/ContactCard.tsx b/submit-web/src/components/App/Proponents/Contacts/ContactCard.tsx
new file mode 100644
index 00000000..a15b1dee
--- /dev/null
+++ b/submit-web/src/components/App/Proponents/Contacts/ContactCard.tsx
@@ -0,0 +1,56 @@
+import { Box, Link, Typography } from "@mui/material";
+import { BCDesignTokens } from "epic.theme";
+import { ProponentAdministrator } from "@/models/Proponent";
+
+type ContactCardProps = {
+ administrator: ProponentAdministrator;
+ entityName: string;
+ index: number;
+};
+
+export const ContactCard = ({
+ administrator,
+ entityName,
+ index,
+}: ContactCardProps) => {
+
+ return (
+
+
+ {`${entityName} Account Administrator ${index + 1}`}
+
+
+
+ {administrator.full_name}
+
+ {administrator.company_name && (
+ {administrator.company_name}
+ )}
+ {administrator.position}
+ {administrator.work_contact_number}
+
+
+ {administrator.work_email_address}
+
+
+
+ );
+};
+
diff --git a/submit-web/src/components/App/Proponents/Contacts/ContactsSection.tsx b/submit-web/src/components/App/Proponents/Contacts/ContactsSection.tsx
new file mode 100644
index 00000000..93338ee2
--- /dev/null
+++ b/submit-web/src/components/App/Proponents/Contacts/ContactsSection.tsx
@@ -0,0 +1,51 @@
+import { Grid, Typography } from "@mui/material";
+import { BCDesignTokens } from "epic.theme";
+import { ProponentAdministrator } from "@/models/Proponent";
+import { ContactCard } from "./ContactCard";
+
+type ContactsSectionProps = {
+ entityName?: string;
+ administrators?: ProponentAdministrator[];
+};
+
+export const ContactsSection = ({
+ entityName,
+ administrators = [],
+}: ContactsSectionProps) => {
+ if (!entityName || administrators.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+ Contacts
+
+
+ {administrators.map((administrator, index) => (
+
+
+
+ ))}
+
+ >
+ );
+};
+
diff --git a/submit-web/src/components/App/Proponents/Contacts/index.ts b/submit-web/src/components/App/Proponents/Contacts/index.ts
new file mode 100644
index 00000000..d544dca1
--- /dev/null
+++ b/submit-web/src/components/App/Proponents/Contacts/index.ts
@@ -0,0 +1,2 @@
+export * from "./ContactsSection";
+
diff --git a/submit-web/src/hooks/api/useProponents.ts b/submit-web/src/hooks/api/useProponents.ts
index d5152fc3..65c40b95 100644
--- a/submit-web/src/hooks/api/useProponents.ts
+++ b/submit-web/src/hooks/api/useProponents.ts
@@ -35,6 +35,7 @@ export const useGetAllProponents = () => {
type GetProponentOptions = {
includeProjects?: boolean;
includeInvitations?: boolean;
+ includeAdministrators?: boolean;
};
const getProponent = (
@@ -48,6 +49,11 @@ const getProponent = (
if (options.includeInvitations) {
params["include-invitations"] = String(Boolean(options.includeInvitations));
}
+ if (options.includeAdministrators) {
+ params["include-administrators"] = String(
+ Boolean(options.includeAdministrators),
+ );
+ }
return submitRequest({
url: `proponents/${proponentId}`,
params,
diff --git a/submit-web/src/models/Proponent.ts b/submit-web/src/models/Proponent.ts
index 514643c6..64c9f44a 100644
--- a/submit-web/src/models/Proponent.ts
+++ b/submit-web/src/models/Proponent.ts
@@ -1,6 +1,17 @@
import { Invitation } from "./Invitation";
import { AccountProject, Project } from "./Project";
+export type ProponentAdministrator = {
+ id: number;
+ first_name: string;
+ last_name: string;
+ full_name: string;
+ company_name: string | null;
+ position: string;
+ work_contact_number: string;
+ work_email_address: string;
+};
+
export type ProponentStatus = "ELIGIBLE" | "INELIGIBLE" | "INVITE_GENERATED" | "PENDING_ONBOARDING" | "ONBOARDED";
export type Proponent = {
@@ -11,4 +22,5 @@ export type Proponent = {
invitations?: Invitation[];
projects?: Project[];
account_projects?: AccountProject[];
+ administrators?: ProponentAdministrator[];
};
diff --git a/submit-web/src/routes/staff/_staffLayout/proponents/$proponentId.tsx b/submit-web/src/routes/staff/_staffLayout/proponents/$proponentId.tsx
index 613a28ed..5789e91a 100644
--- a/submit-web/src/routes/staff/_staffLayout/proponents/$proponentId.tsx
+++ b/submit-web/src/routes/staff/_staffLayout/proponents/$proponentId.tsx
@@ -1,4 +1,5 @@
import { ProjectsTable } from "@/components/App/Proponents/ProjectsTable/ProjectsTable";
+import { ContactsSection } from "@/components/App/Proponents/Contacts";
import { RegistrationUrl } from "@/components/App/Proponents/RegistrationUrl/RegistrationUrl";
import { ProponentStatusChip } from "@/components/App/ProponentStatusChip";
import { ContentBox } from "@/components/Shared/Layouts/ContentBox";
@@ -10,13 +11,12 @@ import { getProponentOptions } from "@/hooks/api/useProponents";
import { InvitationStatus } from "@/models/Invitation";
import { HTTP_STATUS } from "@/utils/constants";
import { InfoOutlined } from "@mui/icons-material";
-import { Grid, IconButton, Tooltip, Typography } from "@mui/material";
+import { Box, Grid, IconButton, Tooltip, Typography } from "@mui/material";
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, notFound, useParams } from "@tanstack/react-router";
import { isAxiosError } from "axios";
import { BCDesignTokens } from "epic.theme";
import { useEffect, useState } from "react";
-import { Box } from "@mui/material";
export const Route = createFileRoute(
"/staff/_staffLayout/proponents/$proponentId",
@@ -27,6 +27,7 @@ export const Route = createFileRoute(
getProponentOptions(Number(proponentId), {
includeProjects: true,
includeInvitations: true,
+ includeAdministrators: true,
}),
),
onError: (error) => {
@@ -72,6 +73,7 @@ function ProponentPage() {
getProponentOptions(proponentId, {
includeProjects: true,
includeInvitations: true,
+ includeAdministrators: true,
}),
);
@@ -91,7 +93,7 @@ function ProponentPage() {
return (
-
+
}
@@ -168,6 +170,10 @@ function ProponentPage() {
/>
>
)}
+