Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions submit-api/src/submit_api/models/proponent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand 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
4 changes: 2 additions & 2 deletions submit-api/src/submit_api/resources/migration_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 9 additions & 2 deletions submit-api/src/submit_api/services/proponent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Box
sx={{
border: 1,
borderColor: BCDesignTokens.surfaceColorBorderDefault,
p: BCDesignTokens.layoutPaddingMedium,
flex: 1,
minHeight: 0,
}}
>
<Typography
variant="subtitle1"
sx={{
fontWeight: 700,
mb: BCDesignTokens.layoutMarginSmall,
}}
>
{`${entityName} Account Administrator ${index + 1}`}
</Typography>

<Typography variant="body1" sx={{ fontWeight: 700 }}>
{administrator.full_name}
</Typography>
{administrator.company_name && (
<Typography variant="body1">{administrator.company_name}</Typography>
)}
<Typography variant="body1">{administrator.position}</Typography>
<Typography variant="body1">{administrator.work_contact_number}</Typography>
<Typography variant="body1">
<Link
href={`mailto:${administrator.work_email_address}`}
underline="hover"
>
{administrator.work_email_address}
</Link>
</Typography>
</Box>
);
};

Original file line number Diff line number Diff line change
@@ -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 (
<>
<Typography
variant="h5"
sx={{
fontWeight: 700,
mt: BCDesignTokens.layoutMarginXxxlarge,
mb: BCDesignTokens.layoutMarginMedium,
}}
>
Contacts
</Typography>
<Grid container spacing={2}>
{administrators.map((administrator, index) => (
<Grid
item
xs={12}
md={6}
key={administrator.id ?? index}
sx={{ display: "flex" }}
>
<ContactCard
administrator={administrator}
entityName={entityName}
index={index}
/>
</Grid>
))}
</Grid>
</>
);
};

2 changes: 2 additions & 0 deletions submit-web/src/components/App/Proponents/Contacts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ContactsSection";

6 changes: 6 additions & 0 deletions submit-web/src/hooks/api/useProponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const useGetAllProponents = () => {
type GetProponentOptions = {
includeProjects?: boolean;
includeInvitations?: boolean;
includeAdministrators?: boolean;
};

const getProponent = (
Expand All @@ -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<Proponent>({
url: `proponents/${proponentId}`,
params,
Expand Down
12 changes: 12 additions & 0 deletions submit-web/src/models/Proponent.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -11,4 +22,5 @@ export type Proponent = {
invitations?: Invitation[];
projects?: Project[];
account_projects?: AccountProject[];
administrators?: ProponentAdministrator[];
};
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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",
Expand All @@ -27,6 +27,7 @@ export const Route = createFileRoute(
getProponentOptions(Number(proponentId), {
includeProjects: true,
includeInvitations: true,
includeAdministrators: true,
}),
),
onError: (error) => {
Expand Down Expand Up @@ -72,6 +73,7 @@ function ProponentPage() {
getProponentOptions(proponentId, {
includeProjects: true,
includeInvitations: true,
includeAdministrators: true,
}),
);

Expand All @@ -91,7 +93,7 @@ function ProponentPage() {

return (
<PageGrid>
<Grid item xs={12}>
<Grid item xs={12} sx={{ alignSelf: "flex-start" }}>
<ContentBox
mainLabel={proponent?.name}
statusChip={<ProponentStatusChip status={proponent?.status} />}
Expand Down Expand Up @@ -168,6 +170,10 @@ function ProponentPage() {
/>
</>
)}
<ContactsSection
entityName={proponent?.name}
administrators={proponent?.administrators}
/>
</ContentBox>
</Grid>
</PageGrid>
Expand Down