Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions apps/employee-portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@base-ui/react": "^1.6.0",
"@phosphor-icons/react": "^2.1.10",
"@probo/helpers": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/react-lazy": "1.0.0",
"@probo/relay": "1.0.0",
"@probo/routes": "1.0.0",
Expand Down
6 changes: 6 additions & 0 deletions apps/employee-portal/src/_locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
"pendingCount_one": "{{count}} document to approve",
"pendingCount_other": "{{count}} documents to approve",
"action": "Start approving"
},
"devices": {
"title": "Devices",
"description": "Manage your work devices.",
"connected": "Device connected",
"action": "Register a device"
}
}
},
Expand Down
89 changes: 37 additions & 52 deletions apps/employee-portal/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import { Heading } from "@probo/ui/src/v2/typography/Heading";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { useParams } from "react-router";

import type { HomePageQuery } from "#/__generated__/core/HomePageQuery.graphql";
import { NotFoundError } from "#/lib/relay/errors";
import { DashboardCard } from "#/pages/_components/DashboardCard";
import { ApprovalDashboardCard } from "#/pages/_components/ApprovalDashboardCard";
import { DeviceCard } from "#/pages/_components/DeviceCard";
import { GetStartedCard } from "#/pages/_components/GetStartedCard";
import { SignatureDashboardCard } from "#/pages/_components/SignatureDashboardCard";
import { useViewerFirstName } from "#/pages/iam/_lib/ViewerIdentityContext";

export const homePageQuery = graphql`
Expand All @@ -37,38 +38,38 @@ export const homePageQuery = graphql`
organizationId: $organizationId
first: 1
filter: { signed: false }
) @required(action: THROW) {
) {
totalCount
edges @required(action: THROW) {
node @required(action: THROW) {
id
}
}
}
completedSignatures: signableDocuments(
organizationId: $organizationId
filter: { signed: true }
) @required(action: THROW) {
) {
totalCount
}
pendingApprovals: approvableDocuments(
organizationId: $organizationId
first: 1
filter: { approvalStates: [PENDING] }
) @required(action: THROW) {
) {
totalCount
edges @required(action: THROW) {
node @required(action: THROW) {
id
}
}
}
approvedDocuments: approvableDocuments(
organizationId: $organizationId
filter: { approvalStates: [APPROVED] }
) @required(action: THROW) {
) {
totalCount
}
...GetStartedCard_viewer @arguments(organizationId: $organizationId)
...SignatureDashboardCard_viewer @arguments(organizationId: $organizationId)
...ApprovalDashboardCard_viewer @arguments(organizationId: $organizationId)
...DeviceCard_viewer @arguments(organizationId: $organizationId)
}
organization: node(id: $organizationId) {
__typename
... on Organization {
...DeviceCard_organization
}
}
}
`;
Expand All @@ -79,26 +80,20 @@ interface HomePageProps {

export function HomePage({ queryRef }: HomePageProps) {
const { t } = useTranslation();
const { organizationId } = useParams();
const firstName = useViewerFirstName();
const { viewer } = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef);
const { viewer, organization } = usePreloadedQuery<HomePageQuery>(
homePageQuery,
queryRef,
);

if (organizationId == null) {
throw new NotFoundError("organizationId is required");
if (organization == null || organization.__typename !== "Organization") {
throw new NotFoundError("invalid type for organization node");
}

const pendingSignatureCount = viewer.pendingSignatures.totalCount;
const signedCount = viewer.completedSignatures.totalCount;
const pendingApprovalCount = viewer.pendingApprovals.totalCount;
const approvedCount = viewer.approvedDocuments.totalCount;

const firstPendingSignatureId = viewer.pendingSignatures.edges[0]?.node.id ?? null;
const firstPendingApprovalId = viewer.pendingApprovals.edges[0]?.node.id ?? null;

const showGetStarted
= (pendingSignatureCount > 0 || pendingApprovalCount > 0)
&& signedCount === 0
&& approvedCount === 0;
= (viewer.pendingSignatures.totalCount > 0 || viewer.pendingApprovals.totalCount > 0)
&& viewer.completedSignatures.totalCount === 0
&& viewer.approvedDocuments.totalCount === 0;

const welcome = firstName === ""
? t("homePage.welcomeFallback")
Expand All @@ -117,30 +112,20 @@ export function HomePage({ queryRef }: HomePageProps) {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{showGetStarted && (
<div className="md:row-span-2">
<GetStartedCard
organizationId={organizationId}
pendingSignatureCount={pendingSignatureCount}
pendingApprovalCount={pendingApprovalCount}
firstPendingSignatureId={firstPendingSignatureId}
firstPendingApprovalId={firstPendingApprovalId}
/>
<GetStartedCard viewerKey={viewer} />
</div>
)}
<DashboardCard
kind="signatures"
organizationId={organizationId}
pendingCount={pendingSignatureCount}
completedCount={signedCount}
firstPendingId={firstPendingSignatureId}
wash={!showGetStarted && pendingSignatureCount > 0}
<SignatureDashboardCard
viewerKey={viewer}
wash={!showGetStarted && viewer.pendingSignatures.totalCount > 0}
/>
<ApprovalDashboardCard
viewerKey={viewer}
wash={!showGetStarted && viewer.pendingApprovals.totalCount > 0}
/>
<DashboardCard
kind="approvals"
organizationId={organizationId}
pendingCount={pendingApprovalCount}
completedCount={approvedCount}
firstPendingId={firstPendingApprovalId}
wash={!showGetStarted && pendingApprovalCount > 0}
<DeviceCard
Comment thread
codenem marked this conversation as resolved.
viewerKey={viewer}
organizationKey={organization}
/>
</div>
</main>
Expand Down
1 change: 1 addition & 0 deletions apps/employee-portal/src/pages/HomePageSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function HomePageSkeleton() {
<CardSkeleton size={5} className="h-75 md:row-span-2 md:h-full" />
<CardSkeleton size={5} className="h-75" />
<CardSkeleton size={5} className="h-75" />
<CardSkeleton size={5} className="h-75" />
</div>
</main>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import { graphql, useFragment } from "react-relay";

import type { ApprovalDashboardCard_viewer$key } from "#/__generated__/core/ApprovalDashboardCard_viewer.graphql";

import { DashboardCard } from "./DashboardCard";

const approvalDashboardCardFragment = graphql`
fragment ApprovalDashboardCard_viewer on Viewer
@argumentDefinitions(organizationId: { type: "ID!" })
@throwOnFieldError {
pendingApprovals: approvableDocuments(
organizationId: $organizationId
first: 1
filter: { approvalStates: [PENDING] }
) {
totalCount
edges {
node {
id
}
}
}
approvedDocuments: approvableDocuments(
organizationId: $organizationId
filter: { approvalStates: [APPROVED] }
) {
totalCount
}
}
`;

export interface ApprovalDashboardCardProps {
viewerKey: ApprovalDashboardCard_viewer$key;
wash?: boolean;
}

export function ApprovalDashboardCard({
viewerKey,
wash = false,
}: ApprovalDashboardCardProps) {
const viewer = useFragment(approvalDashboardCardFragment, viewerKey);

return (
<DashboardCard
kind="approvals"
pendingCount={viewer.pendingApprovals.totalCount}
completedCount={viewer.approvedDocuments.totalCount}
firstPendingId={viewer.pendingApprovals.edges[0]?.node.id ?? null}
wash={wash}
/>
);
}
10 changes: 8 additions & 2 deletions apps/employee-portal/src/pages/_components/DashboardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import { Heading } from "@probo/ui/src/v2/typography/Heading";
import { Text } from "@probo/ui/src/v2/typography/Text";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router";

import { NotFoundError } from "#/lib/relay/errors";

import { dashboardCard } from "./variants";

export interface DashboardCardProps {
kind: "signatures" | "approvals";
organizationId: string;
pendingCount: number;
completedCount: number;
firstPendingId: string | null;
Expand All @@ -46,15 +48,19 @@ export interface DashboardCardProps {

export function DashboardCard({
kind,
organizationId,
pendingCount,
completedCount,
firstPendingId,
wash = false,
}: DashboardCardProps) {
const { t } = useTranslation();
const { organizationId } = useParams();
const slots = dashboardCard({ wash });

if (organizationId == null) {
throw new NotFoundError("organizationId is required");
}

const listPath = kind === "signatures"
? `/${organizationId}/signatures`
: `/${organizationId}/approvals`;
Expand Down
Loading
Loading