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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"date-fns": "^4.1.0",
"email-validator": "^2.0.4",
"i18next": "^25.3.2",
"need4deed-sdk": "^0.0.85",
"need4deed-sdk": "^0.0.90",
"next": "15.3.8",
"react": "^19.0.0",
"react-day-picker": "^9.13.0",
Expand Down
9 changes: 9 additions & 0 deletions public/locales/de/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,15 @@
},
"agents": {
"agents": "RAC-Verzeichnis",
"table": {
"title": "Name",
"type": "Typ",
"volunteerSearch": "Freiwilligensuche",
"district": "Bezirk",
"activeVolunteers": "Aktive Freiwillige",
"numberOfOpportunities": "Anzahl der Möglichkeiten",
"email": "E-Mail"
},
"tabs": {
"tab1": "Liste",
"tab2": "Karten",
Expand Down
9 changes: 9 additions & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,15 @@
},
"agents": {
"agents": "RAC Directory",
"table": {
"title": "Name",
"type": "Type",
"volunteerSearch": "Volunteer Search",
"district": "District",
"activeVolunteers": "Active Volunteers",
"numberOfOpportunities": "Number of Opportunities",
"email": "Email"
},
"tabs": {
"tab1": "List",
"tab2": "Cards",
Expand Down
10 changes: 6 additions & 4 deletions src/components/Dashboard/Agents/AgentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { MapPinIcon } from "@phosphor-icons/react";
import { AgentTrustLevel } from "@/components/Dashboard/Profile/types/agent";
import { ApiAgentGetList } from "need4deed-sdk";
import type { ApiAgentGetList, OptionItem } from "need4deed-sdk";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { IconDiv } from "@/components/styled/container";
Expand All @@ -14,19 +14,21 @@ import { TrustLevelDropdown } from "@/components/Dashboard/Profile/sections/Prof
import { Heading4, Paragraph } from "@/components/styled/text";
import { useUpdateAgentStatus } from "@/hooks";
import { getNormalizedAgent } from "./helpers";
import { createAgentTypeMap, createServiceTypeMap, createVolunteerSearchMap } from "./icon";
import { createAgentTypeMap, createServiceTypeMap, createVolunteerSearchMap } from "./constants";
import { StatusBadge } from "../common/StatusBadge";
import { Card, CardDetailsInfo, CardHeader, CardHeaderInfo, DistrictContainer, DistrictDiv } from "./styles";

interface Props {
agent: ApiAgentGetList;
districtsList?: OptionItem[];
}

export const AgentCard = ({ agent }: Props) => {
export const AgentCard = ({ agent, districtsList }: Props) => {
const { t, i18n } = useTranslation();
const router = useRouter();

const { id, title, district, volunteerSearch, serviceType, type, trustLevel } = getNormalizedAgent(agent);
const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null;

const { mutate: patchAgent } = useUpdateAgentStatus(agent.id);

Expand Down Expand Up @@ -82,7 +84,7 @@ export const AgentCard = ({ agent }: Props) => {
<IconDiv size="var(--dashboard-agents-card-detail-icon-size)">
<MapPinIcon weight="fill" />
</IconDiv>
<Paragraph>{district?.title?.[i18n.language as "en" | "de"]}</Paragraph>
<Paragraph>{districtTitle}</Paragraph>
</DistrictDiv>
</DistrictContainer>
</Card>
Expand Down
7 changes: 4 additions & 3 deletions src/components/Dashboard/Agents/AgentCardList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiAgentGetList } from "need4deed-sdk";
import type { ApiAgentGetList, OptionItem } from "need4deed-sdk";
import { PaginatedGrid } from "@/components/core/paginatedGrid";
import { AgentCard } from "./AgentCard";
import { AgentCardListContainer } from "./styles";
Expand All @@ -10,10 +10,11 @@ type Props = {
rows: number;
currentPage: number;
setCurrentPage: (page: number) => void;
districtsList?: OptionItem[];
};

export function AgentCardList({ agents, count, columns, rows, currentPage, setCurrentPage }: Props) {
const items = agents.map((agent) => <AgentCard key={agent.id} agent={agent} />);
export function AgentCardList({ agents, count, columns, rows, currentPage, setCurrentPage, districtsList }: Props) {
const items = agents.map((agent) => <AgentCard key={agent.id} agent={agent} districtsList={districtsList} />);

return (
<AgentCardListContainer data-testid="agent-card-list">
Expand Down
40 changes: 31 additions & 9 deletions src/components/Dashboard/Agents/AgentListController.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { ApiAgentGetList, ApiOptionLists, SortOrder } from "need4deed-sdk";
import { type ApiAgentGetList, type ApiOptionLists, SortOrder } from "need4deed-sdk";
import { AgentCardList } from "./AgentCardList";
import { useEffect } from "react";
import { DashboardListLoading } from "@/components/Dashboard/common/DashboardListLoading";
import { useGetQuery, usePageParam } from "@/hooks";
import { apiPathAgent, cacheTTL } from "@/config/constants";
import { apiPathAgent, cacheTTL, CARD_COLUMNS, CARD_LIMIT, CARD_ROWS, TABLE_LIMIT } from "@/config/constants";
import { serializeAgentFilters } from "./helpers";
import { AgentCardsFilter } from "./Filters/types";

const columns = 3;
const rows = 3;
const limit = columns * rows;
import { ViewMode } from "../common/types";
import { AgentTableList } from "./AgentTableList";

type Props = {
setNumOfAgents: (num: number) => void;
Expand All @@ -18,10 +16,20 @@ type Props = {
filter: AgentCardsFilter;
apiFilterOptions?: ApiOptionLists;
volunteerId?: string;
viewMode: ViewMode;
};

export const AgentListController = ({ setNumOfAgents, sortOrder, isFiltersOpen, filter, apiFilterOptions }: Props) => {
export const AgentListController = ({
setNumOfAgents,
sortOrder,
isFiltersOpen,
filter,
apiFilterOptions,
viewMode,
}: Props) => {
const { currentPage, setCurrentPage } = usePageParam();
const isListView = viewMode === ViewMode.LIST;
const limit = isListView ? TABLE_LIMIT : CARD_LIMIT;

const serializedFilter = new URLSearchParams(
serializeAgentFilters(filter, undefined, false, {
Expand Down Expand Up @@ -51,14 +59,28 @@ export const AgentListController = ({ setNumOfAgents, sortOrder, isFiltersOpen,

if (isLoading) return <DashboardListLoading />;

if (isListView) {
return (
<AgentTableList
agents={agents}
count={count}
itemsPerPage={limit}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
districtsList={apiFilterOptions?.district ?? undefined}
/>
);
}

return (
<AgentCardList
agents={agents}
count={count}
columns={columns - (isFiltersOpen ? 1 : 0)}
rows={rows + (isFiltersOpen ? 1 : 0)}
columns={CARD_COLUMNS - (isFiltersOpen ? 1 : 0)}
rows={CARD_ROWS + (isFiltersOpen ? 1 : 0)}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
districtsList={apiFilterOptions?.district}
/>
);
};
55 changes: 55 additions & 0 deletions src/components/Dashboard/Agents/AgentTableList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import type { ApiAgentGetList, OptionItem } from "need4deed-sdk";
import { EntityTableList } from "../common/EntityTableList";
import { useTranslation } from "react-i18next";
import { createAgentTableColumns } from "./agentsTableColumns";
import { useMemo } from "react";
import { AgentTableRow } from "./AgentTableRow";
import { createAgentTypeMap, createVolunteerSearchMap } from "./constants";

interface TableListProps {
agents: ApiAgentGetList[];
count: number;
itemsPerPage: number;
currentPage: number;
setCurrentPage: (page: number) => void;
districtsList?: OptionItem[];
}

export function AgentTableList({
agents,
count,
itemsPerPage,
currentPage,
setCurrentPage,
districtsList,
}: TableListProps) {
const { t } = useTranslation();

const columns = useMemo(() => createAgentTableColumns(t), [t]);
const typeLabels = useMemo(() => createAgentTypeMap(t), [t]);
const searchLabels = useMemo(() => createVolunteerSearchMap(t), [t]);

return (
<EntityTableList
columns={columns}
data={agents}
renderRow={(agent, isLast) => (
<AgentTableRow
key={agent.id}
agent={agent}
isLast={isLast}
typeLabels={typeLabels}
searchLabels={searchLabels}
districtsList={districtsList}
/>
)}
count={count}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
testIdPrefix="agents"
/>
);
}
63 changes: 63 additions & 0 deletions src/components/Dashboard/Agents/AgentTableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client";

import type { ApiAgentGetList, OptionItem } from "need4deed-sdk";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { AGENT_COL_WIDTHS } from "./agentsTableColumns";
import { TableCell, TableRow } from "@/components/core/common/Table";
import styled from "styled-components";
import { WrapAnywhereCell } from "../common/EntityTableList/styles";

interface TableRowProps {
agent: ApiAgentGetList;
isLast: boolean;
typeLabels: Record<string, string>;
searchLabels: Record<string, string>;
districtsList?: OptionItem[];
}

export function AgentTableRow({ agent, isLast, typeLabels, searchLabels, districtsList }: TableRowProps) {
const { i18n } = useTranslation();
const router = useRouter();

// TODO: remove cast once codebase migrates to need4deed-sdk@0.0.91+
const { id, title, type, volunteerSearch, district, activeVolunteers, email } = agent as ApiAgentGetList & {
email?: string;
};
const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null;

const handleGoToProfile = () => {
if (!id) return;
router.push(`/${i18n.language}/dashboard/agents/${id}`);
};

return (
<ClickableRow $isLast={isLast} onClick={handleGoToProfile} data-testid={`agent-row-${id}`}>
<TableCell data-testid={`agent-title-${id}`}>{title}</TableCell>
<TableCell $width={AGENT_COL_WIDTHS.type} data-testid={`agent-type-${id}`}>
{typeLabels[type] || type}
</TableCell>
<TableCell $width={AGENT_COL_WIDTHS.volunteerSearch} data-testid={`agent-search-${id}`}>
{searchLabels[volunteerSearch] || volunteerSearch}
</TableCell>
<TableCell $width={AGENT_COL_WIDTHS.district} data-testid={`agent-district-${id}`}>
{districtTitle || "—"}
</TableCell>
<TableCell $width={AGENT_COL_WIDTHS.activeVolunteers} data-testid={`agent-active-volunteers-${id}`}>
{activeVolunteers}
</TableCell>
<TableCell $width={AGENT_COL_WIDTHS.numOpportunities} data-testid={`agent-opportunities-${id}`}>
{"—"}
</TableCell>
<WrapAnywhereCell data-testid={`agent-email-${id}`}>{email || "—"}</WrapAnywhereCell>
</ClickableRow>
);
}

const ClickableRow = styled(TableRow)`
cursor: pointer;

&:hover {
background: var(--color-pink-50);
}
`;
4 changes: 4 additions & 0 deletions src/components/Dashboard/Agents/Agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { createFilterFromOption, getClearFilter } from "../common/CardsFilter/he
import { deserializeAgentFilters, serializeAgentFilters } from "./helpers";
import Filters from "../common/CardsFilter/Filters";
import FiltersContent from "./Filters/FiltersContent";
import { ViewMode } from "../common/types";

export const Agents = () => {
const { t } = useTranslation();
Expand All @@ -30,6 +31,8 @@ export const Agents = () => {
const pathname = usePathname();
const router = useRouter();
const tabs = [t("dashboard.agents.tabs.tab1"), t("dashboard.agents.tabs.tab2"), t("dashboard.agents.tabs.tab3")];
const VIEW_MODE_BY_TAB = [ViewMode.LIST, ViewMode.CARDS, ViewMode.MAP] as const;
const viewMode = VIEW_MODE_BY_TAB[selectedTabIndex] ?? ViewMode.LIST;

const handleSearchInputChange = (searchInput: string) => {
handleFilterUpdate((prev) => ({ ...prev, search: searchInput }));
Expand Down Expand Up @@ -91,6 +94,7 @@ export const Agents = () => {
isFiltersOpen={isFiltersOpen}
filter={cardsFilter}
apiFilterOptions={apiFilterOptions}
viewMode={viewMode}
/>
<Filters
isFiltersOpen={isFiltersOpen}
Expand Down
34 changes: 34 additions & 0 deletions src/components/Dashboard/Agents/agentsTableColumns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TFunction } from "i18next";
import { Column } from "../common/EntityTableList";
import { COLUMN_WIDTH } from "../common/EntityTableList/columnWidths";

export const AGENT_COL_WIDTHS = {
type: COLUMN_WIDTH.MD,
volunteerSearch: COLUMN_WIDTH.LG,
district: COLUMN_WIDTH.LG,
activeVolunteers: COLUMN_WIDTH.XS,
numOpportunities: COLUMN_WIDTH.SM,
email: COLUMN_WIDTH.LG,
};

export const createAgentTableColumns = (t: TFunction): Column[] => [
{ key: "title", label: t("dashboard.agents.table.title") },
{ key: "type", label: t("dashboard.agents.table.type"), width: AGENT_COL_WIDTHS.type },
{
key: "volunteerSearch",
label: t("dashboard.agents.table.volunteerSearch"),
width: AGENT_COL_WIDTHS.volunteerSearch,
},
{ key: "district", label: t("dashboard.agents.table.district"), width: AGENT_COL_WIDTHS.district },
{
key: "activeVolunteers",
label: t("dashboard.agents.table.activeVolunteers"),
width: AGENT_COL_WIDTHS.activeVolunteers,
},
{
key: "numOpportunities",
label: t("dashboard.agents.table.numberOfOpportunities"),
width: AGENT_COL_WIDTHS.numOpportunities,
},
{ key: "email", label: t("dashboard.agents.table.email") },
];
34 changes: 34 additions & 0 deletions src/components/Dashboard/Agents/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AgentServiceType, AgentType, AgentVolunteerSearchType } from "need4deed-sdk";
import { TFunction } from "i18next";

export const createAgentTypeMap = (t: TFunction): Record<AgentType, string> => ({
[AgentType.AE]: t("dashboard.agents.filters.type.ae"),
[AgentType.ASOG]: t("dashboard.agents.filters.type.asog"),
[AgentType.COUNSELING_CENTER]: t("dashboard.agents.filters.type.counseling-center"),
[AgentType.GU1]: t("dashboard.agents.filters.type.gu1"),
[AgentType.GU2]: t("dashboard.agents.filters.type.gu2"),
[AgentType.GU2_PLUS]: t("dashboard.agents.filters.type.gu2+"),
[AgentType.GU3]: t("dashboard.agents.filters.type.gu3"),
[AgentType.MULTIPLE_SOCIAL_SUPPORT]: t("dashboard.agents.filters.type.multiple-social-support"),
[AgentType.NU]: t("dashboard.agents.filters.type.nu"),
[AgentType.TANDEM]: t("dashboard.agents.filters.type.tandem"),
});

export const createVolunteerSearchMap = (t: TFunction): Record<AgentVolunteerSearchType, string> => ({
[AgentVolunteerSearchType.SEARCHING]: t("dashboard.agentProfile.status.volunteerSearch.searching"),
[AgentVolunteerSearchType.NOT_NEEDED]: t("dashboard.agentProfile.status.volunteerSearch.notNeeded"),
[AgentVolunteerSearchType.VOLUNTEERS_FOUND]: t("dashboard.agentProfile.status.volunteerSearch.filled"),
});

export const createServiceTypeMap = (t: TFunction): Record<AgentServiceType, string> => ({
[AgentServiceType.CHILDCARE]: t("dashboard.agentProfile.status.serviceType.childcare"),
[AgentServiceType.CONSULTATION]: t("dashboard.agentProfile.status.serviceType.consultation"),
[AgentServiceType.JOB_COACHING]: t("dashboard.agentProfile.status.serviceType.jobCoaching"),
[AgentServiceType.REFUGEE_ACCOMMODATION]: t("dashboard.agentProfile.status.serviceType.refugeeAccommodation"),
[AgentServiceType.SPORT]: t("dashboard.agentProfile.status.serviceType.sport"),
[AgentServiceType.TANDEM]: t("dashboard.agentProfile.status.serviceType.tandem"),
[AgentServiceType.TUTORING]: t("dashboard.agentProfile.status.serviceType.tutoring"),
[AgentServiceType.VOLUNTARY_SUPPORT]: t("dashboard.agentProfile.status.serviceType.volunteerSupport"),
[AgentServiceType.WELFARE]: t("dashboard.agentProfile.status.serviceType.welfare"),
[AgentServiceType.YOUTH]: t("dashboard.agentProfile.status.serviceType.youth"),
});
2 changes: 1 addition & 1 deletion src/components/Dashboard/Agents/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function getNormalizedAgent(agent: AgentListItem): Omit<
district: agent.district,
volunteerSearch: agent.volunteerSearch ?? AgentVolunteerSearchType.NOT_NEEDED,
trustLevel: agent.trustLevel ? agent.trustLevel : AgentTrustType.UNKNOWN,
serviceType: agent.serviceType,
serviceType: agent.serviceType ?? undefined,
};
}

Expand Down
Loading
Loading