From 6bc97cc58d3584e3afe18d4067bc31a4e05c874e Mon Sep 17 00:00:00 2001 From: "Luca Cannarozzo (@cannarocks)" Date: Mon, 14 Jul 2025 21:43:40 +0200 Subject: [PATCH 1/2] feat(TargetReach): add TargetReachInfo component and Stoplight for visual feedback --- .../components/campaignForm/Section.tsx | 4 +- .../components/campaignForm/index.tsx | 6 ++ .../campaignForm/targetReach/Stoplight.tsx | 27 ++++++ .../campaignForm/targetReach/index.tsx | 95 +++++++++++++++++++ src/services/tryberApi/index.ts | 23 ++++- 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 src/pages/campaigns/components/campaignForm/targetReach/Stoplight.tsx create mode 100644 src/pages/campaigns/components/campaignForm/targetReach/index.tsx diff --git a/src/pages/campaigns/components/campaignForm/Section.tsx b/src/pages/campaigns/components/campaignForm/Section.tsx index 2344a22a..5493a692 100644 --- a/src/pages/campaigns/components/campaignForm/Section.tsx +++ b/src/pages/campaigns/components/campaignForm/Section.tsx @@ -8,11 +8,13 @@ const Section = ({ subtitle, id, children, + style, }: { title: string; subtitle: string; id: string; children: React.ReactNode; + style?: React.CSSProperties; }) => { const { setCurrentSection, pushSection } = useCampaignFormContext(); @@ -27,7 +29,7 @@ const Section = ({ if (inView) setCurrentSection(id); }, [inView, id, setCurrentSection]); return ( -
+
{title} diff --git a/src/pages/campaigns/components/campaignForm/index.tsx b/src/pages/campaigns/components/campaignForm/index.tsx index d370a382..27319f94 100644 --- a/src/pages/campaigns/components/campaignForm/index.tsx +++ b/src/pages/campaigns/components/campaignForm/index.tsx @@ -47,6 +47,7 @@ import ImportPages from "./ImportPages"; import { Section } from "./Section"; import { Stepper } from "./Stepper"; import { SurveyButton } from "./SurveyButton"; +import { TargetReachInfo } from "./targetReach"; export interface FormProps { dossier?: GetDossiersByCampaignApiResponse; @@ -268,7 +269,12 @@ const CampaignFormContent = ({ dossier, isEdit, duplicate }: FormProps) => { title="Target Details" subtitle="Define the target details here." id="who" + style={{ position: "relative" }} > +
{ + return ; +}; diff --git a/src/pages/campaigns/components/campaignForm/targetReach/index.tsx b/src/pages/campaigns/components/campaignForm/targetReach/index.tsx new file mode 100644 index 00000000..57fdf74d --- /dev/null +++ b/src/pages/campaigns/components/campaignForm/targetReach/index.tsx @@ -0,0 +1,95 @@ +import { Button, Text, Title } from "@appquality/appquality-design-system"; +import { useMemo, useState } from "react"; +import { ArrowClockwise } from "react-bootstrap-icons"; +import { useGetDossiersByCampaignAvailableTestersQuery } from "src/services/tryberApi"; +import { styled } from "styled-components"; +import { formatDate, formatTime } from "../formatDate"; +import { Stoplight } from "./Stoplight"; + +const StyledContainer = styled.div` + position: absolute; + top: 0; + right: 0; + display: grid; + grid-template-columns: 1fr 4fr; + align-items: center; + align-content: center; +`; + +const TitleContainer = styled.div` + display: flex; + align-items: center; +`; + +function prettyPrintNumber(num: number): string { + if (num >= 1_000_000_000) + return (num / 1_000_000_000).toFixed(1).replace(/\.0$/, "") + "B"; + if (num >= 1_000_000) + return (num / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; + if (num >= 1_000) return (num / 1_000).toFixed(1).replace(/\.0$/, "") + "k"; + return num.toString(); +} + +export const TargetReachInfo = ({ + campaignId, + cap, +}: { + campaignId?: number; + cap?: number; +}) => { + const [disableCache, setDisableCache] = useState<"0" | "1" | undefined>(); + const { data, isLoading, isFetching, isError, refetch } = + useGetDossiersByCampaignAvailableTestersQuery({ + campaign: campaignId?.toString() || "", + refresh: disableCache, + }); + + const stoplightColor = useMemo(() => { + if (!cap) return "grey"; + if (!data?.count) return "grey"; + if (data.count > cap * 8) return "green"; + if (data.count > cap * 4) return "yellow"; + return "red"; + }, [data?.count, cap]); + + const prettyCount = useMemo(() => { + if (!data?.count) return "0"; + return prettyPrintNumber(data.count); + }, [data?.count]); + + if (isLoading || isFetching) + return ( + Loading... + ); + + if (isError || !data) return null; + + return ( + +
+ +
+
+ + Potential reach: {prettyCount} testers + + + + Updated on: {formatDate(data.lastUpdate)} at{" "} + {formatTime(data.lastUpdate)}.{" "} + + +
+
+ ); +}; diff --git a/src/services/tryberApi/index.ts b/src/services/tryberApi/index.ts index 1328abf8..3334d5ac 100644 --- a/src/services/tryberApi/index.ts +++ b/src/services/tryberApi/index.ts @@ -462,6 +462,15 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.dossierCreationData, }), }), + getDossiersByCampaignAvailableTesters: build.query< + GetDossiersByCampaignAvailableTestersApiResponse, + GetDossiersByCampaignAvailableTestersApiArg + >({ + query: (queryArg) => ({ + url: `/dossiers/${queryArg.campaign}/availableTesters`, + params: { refresh: queryArg.refresh }, + }), + }), postDossiersByCampaignManual: build.mutation< PostDossiersByCampaignManualApiResponse, PostDossiersByCampaignManualApiArg @@ -1390,10 +1399,10 @@ export type GetCampaignsByCampaignBugsAndBugIdApiArg = { }; export type GetCampaignsByCampaignBugsAndBugIdAiReviewApiResponse = /** status 200 OK */ { - ai_status: string; + ai_notes?: string; ai_reason: string; + ai_status: string; score_percentage: number; - ai_notes?: string; }; export type GetCampaignsByCampaignBugsAndBugIdAiReviewApiArg = { /** A campaign id */ @@ -1958,6 +1967,15 @@ export type PutDossiersByCampaignApiArg = { campaign: string; dossierCreationData: DossierCreationData; }; +export type GetDossiersByCampaignAvailableTestersApiResponse = + /** status 200 OK */ { + count: number; + lastUpdate: string; + }; +export type GetDossiersByCampaignAvailableTestersApiArg = { + campaign: string; + refresh?: "1" | "0"; +}; export type PostDossiersByCampaignManualApiResponse = /** status 200 OK */ {}; export type PostDossiersByCampaignManualApiArg = { /** A campaign id */ @@ -3293,6 +3311,7 @@ export const { usePostDossiersMutation, useGetDossiersByCampaignQuery, usePutDossiersByCampaignMutation, + useGetDossiersByCampaignAvailableTestersQuery, usePostDossiersByCampaignManualMutation, usePutDossiersByCampaignPhasesMutation, usePostDossiersByCampaignPreviewMutation, From 5b79e556109559909adc9df1a355aaf58be565c0 Mon Sep 17 00:00:00 2001 From: "Luca Cannarozzo (@cannarocks)" Date: Tue, 15 Jul 2025 09:29:33 +0200 Subject: [PATCH 2/2] fix(TargetReachInfo): improve stoplight color logic by consolidating conditions --- .../campaigns/components/campaignForm/targetReach/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/campaigns/components/campaignForm/targetReach/index.tsx b/src/pages/campaigns/components/campaignForm/targetReach/index.tsx index 57fdf74d..a32f3d8a 100644 --- a/src/pages/campaigns/components/campaignForm/targetReach/index.tsx +++ b/src/pages/campaigns/components/campaignForm/targetReach/index.tsx @@ -45,8 +45,8 @@ export const TargetReachInfo = ({ }); const stoplightColor = useMemo(() => { - if (!cap) return "grey"; - if (!data?.count) return "grey"; + if (!cap || !data?.count) return "grey"; + if (data.count > cap * 8) return "green"; if (data.count > cap * 4) return "yellow"; return "red";