diff --git a/frontends/api/src/mitxonline/hooks/contracts/queries.ts b/frontends/api/src/mitxonline/hooks/contracts/queries.ts index af5bd481bf..9bb492bde9 100644 --- a/frontends/api/src/mitxonline/hooks/contracts/queries.ts +++ b/frontends/api/src/mitxonline/hooks/contracts/queries.ts @@ -1,7 +1,9 @@ import { queryOptions } from "@tanstack/react-query" import type { B2bApiB2bContractsRetrieveRequest, + B2bApiB2bManagerOrganizationsContractsCodesRetrieveRequest, ContractPage, + ManagerContractDetail, } from "@mitodl/mitxonline-api-axios/v2" import { b2bApi } from "../../clients" @@ -14,6 +16,9 @@ const contractKeys = { "detail", opts, ], + managerContractCodes: ( + opts: B2bApiB2bManagerOrganizationsContractsCodesRetrieveRequest, + ) => [...contractKeys.root, "manager", "codes", opts], } const contractQueries = { @@ -31,6 +36,17 @@ const contractQueries = { return b2bApi.b2bContractsRetrieve(opts).then((res) => res.data) }, }), + managerContractCodes: ( + opts: B2bApiB2bManagerOrganizationsContractsCodesRetrieveRequest, + ) => + queryOptions({ + queryKey: contractKeys.managerContractCodes(opts), + queryFn: async (): Promise => { + return b2bApi + .b2bManagerOrganizationsContractsCodesRetrieve(opts) + .then((res) => res.data) + }, + }), } export { contractQueries, contractKeys } diff --git a/frontends/main/src/app-pages/ContractAdminPage/ContractAdminPage.tsx b/frontends/main/src/app-pages/ContractAdminPage/ContractAdminPage.tsx new file mode 100644 index 0000000000..761dff6bc0 --- /dev/null +++ b/frontends/main/src/app-pages/ContractAdminPage/ContractAdminPage.tsx @@ -0,0 +1,130 @@ +"use client" +import React from "react" +import { useQuery } from "@tanstack/react-query" +import { + Breadcrumbs, + Container, + Skeleton, + styled, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "ol-components" +import { contractQueries } from "api/mitxonline-hooks/contracts" +import * as urls from "@/common/urls" + +type EnrollmentCodeRow = { + id?: number | string + code?: string + is_redeemed?: boolean + redeemed_by?: string | null + redeemed_on?: string | null +} + +type ContractAdminPageProps = { + orgId: number + contractId: number +} + +const PageRoot = styled(Container)(({ theme }) => ({ + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(6), +})) + +const TableWrapper = styled("div")(({ theme }) => ({ + marginTop: theme.spacing(3), + border: `1px solid ${theme.custom.colors.lightGray2}`, + borderRadius: "4px", + overflow: "hidden", +})) + +const formatDate = (value?: string | null) => { + if (!value) { + return "—" + } + const date = new Date(value) + if (Number.isNaN(date.getTime())) { + return value + } + return date.toLocaleDateString() +} + +const formatBoolean = (value?: boolean) => (value ? "Yes" : "No") + +const ContractAdminPage: React.FC = ({ + orgId, + contractId, +}) => { + const codesQuery = useQuery( + contractQueries.managerContractCodes({ + id: contractId, + parent_lookup_organization: orgId, + }), + ) + + return ( + + + + {codesQuery.isLoading + ? "Loading contract..." + : "Contract Admin"} + + {/* {codesData?.description ? ( + {codesData.description} + ) : null} */} + + + Enrollment Codes + + + {codesQuery.isLoading ? ( + + ) : codesQuery.isError ? ( + + Unable to load enrollment codes for this contract. + + ) : codesQuery.data && (codesQuery.data as unknown as EnrollmentCodeRow[]).length === 0 ? ( + + No enrollment codes found for this contract. + + ) : ( + + + + + + Code + Redeemed + Redeemed By + Redeemed On + + + + {codesQuery.data && (codesQuery.data as unknown as EnrollmentCodeRow[]).map((code, index) => ( + + {code.code ?? "—"} + {formatBoolean(code.is_redeemed)} + {code.redeemed_by ?? "—"} + {formatDate(code.redeemed_on)} + + ))} + +
+
+
+ )} +
+ ) +} + +export default ContractAdminPage +export type { ContractAdminPageProps } diff --git a/frontends/main/src/app/contractadmin/[orgId]/[contractId]/page.tsx b/frontends/main/src/app/contractadmin/[orgId]/[contractId]/page.tsx new file mode 100644 index 0000000000..80b5641492 --- /dev/null +++ b/frontends/main/src/app/contractadmin/[orgId]/[contractId]/page.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { Metadata } from "next" +import { standardizeMetadata } from "@/common/metadata" +import invariant from "tiny-invariant" +import ContractAdminPage from "@/app-pages/ContractAdminPage/ContractAdminPage" + +export const metadata: Metadata = standardizeMetadata({ + title: "Contract Admin", +}) + +const Page: React.FC< + PageProps<"/contractadmin/[orgId]/[contractId]"> +> = async ({ params }) => { + const resolved = await params + invariant(resolved?.orgId, "orgId is required") + invariant(resolved?.contractId, "contractId is required") + + const orgId = Number(resolved.orgId) + const contractId = Number(resolved.contractId) + invariant( + Number.isFinite(orgId) && !Number.isNaN(orgId), + "orgId must be numeric", + ) + invariant( + Number.isFinite(contractId) && !Number.isNaN(contractId), + "contractId must be numeric", + ) + + return +} + +export default Page diff --git a/frontends/main/src/common/urls.ts b/frontends/main/src/common/urls.ts index 815eec1462..70fd663c27 100644 --- a/frontends/main/src/common/urls.ts +++ b/frontends/main/src/common/urls.ts @@ -209,6 +209,13 @@ export const B2B_ATTACH_VIEW = "/enrollmentcode/[code]" export const b2bAttachView = (code: string) => generatePath(B2B_ATTACH_VIEW, { code: code }) +export const CONTRACT_ADMIN_VIEW = "/contractadmin/[orgId]/[contractId]" +export const contractAdminView = (orgId: number, contractId: number) => + generatePath(CONTRACT_ADMIN_VIEW, { + orgId: String(orgId), + contractId: String(contractId), + }) + export const FACEBOOK_SHARE_BASE_URL = "https://www.facebook.com/sharer/sharer.php" export const TWITTER_SHARE_BASE_URL = "https://x.com/share" diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index ec662ecd58..bd6836d683 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -70,6 +70,19 @@ export type { SkeletonProps } from "@mui/material/Skeleton" export { default as Stack } from "@mui/material/Stack" export type { StackProps } from "@mui/material/Stack" +export { default as Table } from "@mui/material/Table" +export type { TableProps } from "@mui/material/Table" +export { default as TableBody } from "@mui/material/TableBody" +export type { TableBodyProps } from "@mui/material/TableBody" +export { default as TableCell } from "@mui/material/TableCell" +export type { TableCellProps } from "@mui/material/TableCell" +export { default as TableContainer } from "@mui/material/TableContainer" +export type { TableContainerProps } from "@mui/material/TableContainer" +export { default as TableHead } from "@mui/material/TableHead" +export type { TableHeadProps } from "@mui/material/TableHead" +export { default as TableRow } from "@mui/material/TableRow" +export type { TableRowProps } from "@mui/material/TableRow" + export { default as Tab } from "@mui/material/Tab" export type { TabProps } from "@mui/material/Tab"