diff --git a/packages/collectives-next/pages/secretary/statistics/index.jsx b/packages/collectives-next/pages/secretary/statistics/index.jsx
new file mode 100644
index 0000000000..724defcef2
--- /dev/null
+++ b/packages/collectives-next/pages/secretary/statistics/index.jsx
@@ -0,0 +1,31 @@
+import ListLayout from "next-common/components/layout/ListLayout";
+import SecretaryCollectivesStatistics from "next-common/components/secretary/statistics/collectives";
+import CollectivesProvider from "next-common/context/collectives/collectives";
+import { withCommonProps } from "next-common/lib";
+import { backendApi } from "next-common/services/nextApi";
+import { secretaryMembersApiUri } from "next-common/services/url";
+
+export default function SecretaryStatisticsPage() {
+ const category = "Secretary Statistics";
+ const seoInfo = { title: category, desc: category };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export const getServerSideProps = withCommonProps(async () => {
+ const { result: secretaryMembers } = await backendApi.fetch(
+ secretaryMembersApiUri,
+ );
+
+ return {
+ props: {
+ secretaryMembers: secretaryMembers ?? null,
+ },
+ };
+});
diff --git a/packages/next-common/components/fellowship/statistics/expenditure/claimants/index.jsx b/packages/next-common/components/fellowship/statistics/expenditure/claimants/index.jsx
index 05c9604464..ba77d7456f 100644
--- a/packages/next-common/components/fellowship/statistics/expenditure/claimants/index.jsx
+++ b/packages/next-common/components/fellowship/statistics/expenditure/claimants/index.jsx
@@ -32,16 +32,19 @@ function paginateData(data, page, pageSize) {
return data.slice(start, end);
}
-function StatisticsClaimantsTable({ members = [] }) {
+function StatisticsClaimantsTable({
+ members = [],
+ membersApi = fellowshipStatisticsMembersApi,
+}) {
const [total, setTotal] = useState(0);
const [processedData, setProcessedData] = useState([]);
const [rowData, setRowData] = useState([]);
const [tableLoading, setTableLoading] = useState(false);
+ const pageSize = defaultPageSize;
const { page, component: pageComponent } = usePaginationComponent(
total,
- defaultPageSize,
+ pageSize,
);
- const membersApi = fellowshipStatisticsMembersApi;
const columns = [
useStatisticsClaimantsRankColumn(),
@@ -64,7 +67,7 @@ function StatisticsClaimantsTable({ members = [] }) {
setTableLoading(false);
return [];
}
- }, []);
+ }, [membersApi]);
useEffect(() => {
if (originalMembers && members) {
@@ -99,12 +102,15 @@ function StatisticsClaimantsTable({ members = [] }) {
);
}
-export default function StatisticsClaimants({ members = [] }) {
+export default function StatisticsClaimants({
+ members = [],
+ membersApi = fellowshipStatisticsMembersApi,
+}) {
return (
Top Claimants
-
+
);
diff --git a/packages/next-common/components/fellowship/statistics/expenditure/rank/doughnutChart/index.jsx b/packages/next-common/components/fellowship/statistics/expenditure/rank/doughnutChart/index.jsx
index c1290f421b..0ce28c67ea 100644
--- a/packages/next-common/components/fellowship/statistics/expenditure/rank/doughnutChart/index.jsx
+++ b/packages/next-common/components/fellowship/statistics/expenditure/rank/doughnutChart/index.jsx
@@ -33,9 +33,9 @@ function handleLabelDataArr(members, ranksData) {
const totalSalary = getTotalSalary(ranksData);
const ranksDataObj = transformRanksDataToObject(ranksData);
const dataArr = rankArr.map((rank, index) => {
- const count = ranksDataObj[index] || 0;
- const percent = ranksDataObj[index]
- ? new BigNumber(ranksDataObj[index]).div(totalSalary)
+ const count = ranksDataObj[rank] || 0;
+ const percent = ranksDataObj[rank]
+ ? new BigNumber(ranksDataObj[rank]).div(totalSalary)
: "";
return {
label: `Rank ${rank}`,
@@ -71,11 +71,11 @@ function RankChart({ labelDataArr, data }) {
}
export default function RankDoughnutChart({ members = [] }) {
+ const ranksApi = fellowshipStatisticsRanksApi;
+
const [labelDataArr, setLabelDataArr] = useState([]);
const [contentLoading, setContentLoading] = useState(false);
- const ranksApi = fellowshipStatisticsRanksApi;
-
const { value: ranksData } = useAsync(async () => {
setContentLoading(true);
if (!ranksApi) {
diff --git a/packages/next-common/components/secretary/statistics/assetBreakdown.jsx b/packages/next-common/components/secretary/statistics/assetBreakdown.jsx
new file mode 100644
index 0000000000..3d061f8108
--- /dev/null
+++ b/packages/next-common/components/secretary/statistics/assetBreakdown.jsx
@@ -0,0 +1,34 @@
+import BigNumber from "bignumber.js";
+import { cn } from "next-common/utils";
+import TokenSymbolAsset from "next-common/components/summary/polkadotTreasurySummary/common/tokenSymbolAsset";
+
+export default function AssetBreakdown({
+ usdTotal,
+ rows = [],
+ align = "left",
+}) {
+ if (!rows || rows.length === 0) {
+ return null;
+ }
+
+ return (
+
+
${usdTotal}
+
+ {rows.map((row, i) => (
+ 6 ? 4 : 2)}
+ symbol={row.symbol}
+ />
+ ))}
+
+
+ );
+}
diff --git a/packages/next-common/components/secretary/statistics/breakdown.js b/packages/next-common/components/secretary/statistics/breakdown.js
new file mode 100644
index 0000000000..6aab4ad59f
--- /dev/null
+++ b/packages/next-common/components/secretary/statistics/breakdown.js
@@ -0,0 +1,46 @@
+import BigNumber from "bignumber.js";
+
+export function getCyclesTotal(cycles) {
+ if (!cycles || cycles.length === 0) {
+ return new BigNumber(0);
+ }
+ return cycles.reduce((total, item) => {
+ const registeredPaid = new BigNumber(item.registeredPaid || 0);
+ const unRegisteredPaid = new BigNumber(item.unRegisteredPaid || 0);
+ return total.plus(registeredPaid).plus(unRegisteredPaid);
+ }, new BigNumber(0));
+}
+
+export function getReferendaTotal(paymentReferenda) {
+ if (!paymentReferenda || paymentReferenda.length === 0) {
+ return new BigNumber(0);
+ }
+ return paymentReferenda.reduce((total, item) => {
+ return total.plus(new BigNumber(item.value || 0));
+ }, new BigNumber(0));
+}
+
+export function getReferendaUsd(paymentReferenda) {
+ if (!paymentReferenda || paymentReferenda.length === 0) {
+ return new BigNumber(0);
+ }
+ return paymentReferenda.reduce((total, ref) => {
+ const value = new BigNumber(ref.value || 0);
+ const amount = value.div(Math.pow(10, ref.decimals || 10));
+ return total.plus(amount.times(ref.price || 0));
+ }, new BigNumber(0));
+}
+
+export function getReferendaTotalByAddress(paymentReferenda, address) {
+ const refs = (paymentReferenda || []).filter(
+ (r) => r.beneficiary === address,
+ );
+ return getReferendaTotal(refs);
+}
+
+export function getReferendaUsdByAddress(paymentReferenda, address) {
+ const refs = (paymentReferenda || []).filter(
+ (r) => r.beneficiary === address,
+ );
+ return getReferendaUsd(refs);
+}
diff --git a/packages/next-common/components/secretary/statistics/claimants/index.jsx b/packages/next-common/components/secretary/statistics/claimants/index.jsx
new file mode 100644
index 0000000000..8dc0a530b4
--- /dev/null
+++ b/packages/next-common/components/secretary/statistics/claimants/index.jsx
@@ -0,0 +1,221 @@
+import { useMemo } from "react";
+import DataList from "next-common/components/dataList";
+import { backendApi } from "next-common/services/nextApi";
+import { fellowshipStatisticsMembersApi } from "next-common/services/url";
+import { defaultPageSize } from "next-common/utils/constants";
+import { useAsync } from "react-use";
+import { useStatisticsClaimantColumn } from "../../../fellowship/statistics/expenditure/claimants/columns/claimant";
+import { useStatisticsClaimantsCyclesColumn } from "../../../fellowship/statistics/expenditure/claimants/columns/cycles";
+import { useStatisticsClaimantsRankColumn } from "../../../fellowship/statistics/expenditure/claimants/columns/rank";
+import { useState, useEffect } from "react";
+import usePaginationComponent from "next-common/components/pagination/usePaginationComponent";
+import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard";
+import { StatisticsTitle } from "next-common/components/statistics/styled.js";
+import { isNil } from "lodash-es";
+import BigNumber from "bignumber.js";
+import PaymentReferendaTooltip from "next-common/components/secretary/statistics/paymentReferendaTooltip";
+import AssetBreakdown from "next-common/components/secretary/statistics/assetBreakdown";
+import {
+ getReferendaTotalByAddress,
+ getReferendaUsdByAddress,
+} from "next-common/components/secretary/statistics/breakdown";
+import ValueDisplay from "next-common/components/valueDisplay";
+import { getSalaryAsset } from "next-common/utils/consts/getSalaryAsset";
+import { formatNum, toPrecision } from "next-common/utils";
+
+function handleClaimantsData(originalMembers, members) {
+ const membersRank = members.reduce((acc, member) => {
+ acc[member.address] = member.rank;
+ return acc;
+ }, {});
+
+ return originalMembers.map((item) => ({
+ ...item,
+ rank: !isNil(membersRank[item.who]) ? membersRank[item.who] : null,
+ }));
+}
+
+function paginateData(data, page, pageSize) {
+ const start = (page - 1) * pageSize;
+ const end = page * pageSize;
+ return data.slice(start, end);
+}
+
+function ReferendaCell({ paymentReferenda = [] }) {
+ if (!paymentReferenda.length) {
+ return -;
+ }
+
+ return (
+
+
+ {paymentReferenda.length}
+
+
+ );
+}
+
+function useSecretaryClaimantsReferendaColumn(paymentReferenda) {
+ const referendaByAddress = useMemo(() => {
+ const map = {};
+ for (const ref of paymentReferenda || []) {
+ if (!map[ref.beneficiary]) {
+ map[ref.beneficiary] = [];
+ }
+ map[ref.beneficiary].push(ref);
+ }
+ return map;
+ }, [paymentReferenda]);
+
+ return {
+ name: "Referenda",
+ width: 160,
+ cellRender(data) {
+ const refs = referendaByAddress[data.who] || [];
+ return ;
+ },
+ };
+}
+
+function useSecretaryClaimantsPaidColumn(paymentReferenda) {
+ const { decimals, symbol } = getSalaryAsset();
+
+ return {
+ name: "Total Paid",
+ className: "text-right",
+ cellRender(data, idx) {
+ const salary = BigInt(data.salary || 0);
+ const address = data.who;
+ const referendaTotal = getReferendaTotalByAddress(
+ paymentReferenda,
+ address,
+ );
+
+ if (referendaTotal.isZero()) {
+ return (
+
+ );
+ }
+
+ const referendaUsd = getReferendaUsdByAddress(paymentReferenda, address);
+ const cyclesUsd = new BigNumber(salary.toString()).div(
+ Math.pow(10, decimals),
+ );
+ const usdTotal = cyclesUsd.plus(referendaUsd).toFixed(2);
+
+ const rows = [
+ { value: salary.toString(), decimals, symbol },
+ {
+ value: referendaTotal.toString(),
+ decimals: 10,
+ symbol: "DOT",
+ },
+ ];
+
+ return (
+
+ );
+ },
+ };
+}
+
+function SecretaryClaimantsTable({
+ members = [],
+ membersApi = fellowshipStatisticsMembersApi,
+ paymentReferenda = [],
+}) {
+ const [total, setTotal] = useState(0);
+ const [processedData, setProcessedData] = useState([]);
+ const [rowData, setRowData] = useState([]);
+ const [tableLoading, setTableLoading] = useState(false);
+ const pageSize = defaultPageSize;
+ const { page, component: pageComponent } = usePaginationComponent(
+ total,
+ pageSize,
+ );
+
+ const columns = [
+ useStatisticsClaimantsRankColumn(),
+ useStatisticsClaimantColumn(),
+ useStatisticsClaimantsCyclesColumn(),
+ useSecretaryClaimantsReferendaColumn(paymentReferenda),
+ useSecretaryClaimantsPaidColumn(paymentReferenda),
+ ];
+
+ const { value: originalMembers } = useAsync(async () => {
+ setTableLoading(true);
+ if (!membersApi) {
+ return [];
+ }
+ try {
+ const resp = await backendApi.fetch(membersApi, {
+ page,
+ pageSize: defaultPageSize,
+ });
+ return resp?.result || [];
+ } catch {
+ setTableLoading(false);
+ return [];
+ }
+ }, [membersApi]);
+
+ useEffect(() => {
+ if (originalMembers && members) {
+ const processed = handleClaimantsData(originalMembers, members);
+ setProcessedData(processed);
+ setTotal(processed.length);
+ setTableLoading(false);
+ }
+ }, [originalMembers, members]);
+
+ useEffect(() => {
+ if (processedData.length > 0) {
+ const paginatedData = paginateData(processedData, page, defaultPageSize);
+ const rows = paginatedData.map((item, idx) =>
+ columns.map((col) => col.cellRender(item, idx)),
+ );
+ setRowData(rows);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [processedData, page]);
+
+ return (
+
+
+ {pageComponent}
+
+ );
+}
+
+export default function SecretaryStatisticsClaimants({
+ members = [],
+ membersApi = fellowshipStatisticsMembersApi,
+ paymentReferenda = [],
+}) {
+ return (
+
+
+ Top Beneficiary
+
+
+
+ );
+}
diff --git a/packages/next-common/components/secretary/statistics/collectives.jsx b/packages/next-common/components/secretary/statistics/collectives.jsx
new file mode 100644
index 0000000000..143243e628
--- /dev/null
+++ b/packages/next-common/components/secretary/statistics/collectives.jsx
@@ -0,0 +1,40 @@
+import { useAsync } from "react-use";
+import { backendApi } from "next-common/services/nextApi";
+import {
+ secretaryStatisticsCyclesApi,
+ secretaryStatisticsMembersApi,
+} from "next-common/services/url";
+import { TitleContainer } from "next-common/components/styled/containers/titleContainer";
+import SecretaryStatisticsSummary from "next-common/components/secretary/statistics/summary";
+import StatisticsCycles from "next-common/components/fellowship/statistics/expenditure/cycles";
+import SecretaryStatisticsClaimants from "next-common/components/secretary/statistics/claimants";
+import { useFellowshipCollectiveMembers } from "next-common/hooks/fellowship/core/useFellowshipCollectiveMembers";
+
+export default function SecretaryCollectivesStatistics() {
+ const { members } = useFellowshipCollectiveMembers();
+
+ const { value: data, loading } = useAsync(async () => {
+ const resp = await backendApi.fetch(secretaryStatisticsCyclesApi);
+ return resp?.result;
+ }, []);
+
+ const cycles = data?.cycles || [];
+ const paymentReferenda = data?.paymentReferenda || [];
+
+ return (
+
+ Salary
+
+
+
+
+ );
+}
diff --git a/packages/next-common/components/secretary/statistics/paymentReferendaTooltip.jsx b/packages/next-common/components/secretary/statistics/paymentReferendaTooltip.jsx
new file mode 100644
index 0000000000..88648ebaee
--- /dev/null
+++ b/packages/next-common/components/secretary/statistics/paymentReferendaTooltip.jsx
@@ -0,0 +1,33 @@
+import Tooltip from "next-common/components/tooltip";
+import Link from "next-common/components/link";
+import { toPrecision } from "next-common/utils";
+
+export default function PaymentReferendaTooltip({
+ paymentReferenda = [],
+ children,
+}) {
+ if (!paymentReferenda || paymentReferenda.length === 0) {
+ return children;
+ }
+
+ const content = (
+
+ {paymentReferenda.map((ref) => (
+
+
+ #{ref.referendumIndex}
+ {" · "}
+ {ref.title}
+
+ {" · "}
+ {toPrecision(ref.value, ref.decimals)} {ref.symbol}
+
+ ))}
+
+ );
+
+ return {children};
+}
diff --git a/packages/next-common/components/secretary/statistics/summary/index.jsx b/packages/next-common/components/secretary/statistics/summary/index.jsx
new file mode 100644
index 0000000000..9fa49dd077
--- /dev/null
+++ b/packages/next-common/components/secretary/statistics/summary/index.jsx
@@ -0,0 +1,75 @@
+import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard";
+import SummaryItem from "next-common/components/summary/layout/item";
+import SummaryLayout from "next-common/components/summary/layout/layout";
+import { formatNum } from "next-common/utils";
+import { getSalaryAsset } from "next-common/utils/consts/getSalaryAsset";
+import { LoadingContent } from "next-common/components/fellowship/statistics/common";
+import PaymentReferendaTooltip from "next-common/components/secretary/statistics/paymentReferendaTooltip";
+import AssetBreakdown from "next-common/components/secretary/statistics/assetBreakdown";
+import {
+ getCyclesTotal,
+ getReferendaTotal,
+ getReferendaUsd,
+} from "next-common/components/secretary/statistics/breakdown";
+
+function TotalSpent({ cycles, paymentReferenda }) {
+ const { decimals: salaryDecimals } = getSalaryAsset();
+ const cyclesTotal = getCyclesTotal(cycles);
+ const referendaTotal = getReferendaTotal(paymentReferenda);
+ const referendaUsd = getReferendaUsd(paymentReferenda);
+
+ const cyclesUsd = cyclesTotal.div(Math.pow(10, salaryDecimals));
+ const usdTotal = formatNum(cyclesUsd.plus(referendaUsd).toFixed(2));
+
+ const rows = [
+ { value: cyclesTotal.toString(), decimals: salaryDecimals, symbol: "USDT" },
+ ];
+
+ if (referendaTotal.gt(0)) {
+ rows.push({
+ value: referendaTotal.toString(),
+ decimals: 10,
+ symbol: "DOT",
+ });
+ }
+
+ return (
+
+
+
+ );
+}
+
+function SpentCycles({ count }) {
+ return {count};
+}
+
+export default function SecretaryStatisticsSummary({
+ cycles = [],
+ paymentReferenda = [],
+ loading,
+}) {
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {paymentReferenda?.length || 0}
+
+
+
+
+
+ );
+}
diff --git a/packages/next-common/components/summary/polkadotTreasurySummary/common/tokenSymbolAsset.jsx b/packages/next-common/components/summary/polkadotTreasurySummary/common/tokenSymbolAsset.jsx
index c8ab88aa41..e1d75a6839 100644
--- a/packages/next-common/components/summary/polkadotTreasurySummary/common/tokenSymbolAsset.jsx
+++ b/packages/next-common/components/summary/polkadotTreasurySummary/common/tokenSymbolAsset.jsx
@@ -3,6 +3,7 @@ import ValueDisplay from "next-common/components/valueDisplay";
import { cn } from "next-common/utils";
import { useNativeTokenIcon } from "next-common/components/assethubMigrationAssets/known";
import { useMemo } from "react";
+import { useChainSettings } from "next-common/context/chain";
export default function TokenSymbolAsset({
amount,
@@ -10,12 +11,14 @@ export default function TokenSymbolAsset({
valueClassName,
type = "",
}) {
-
+ const { symbol: nativeSymbol } = useChainSettings();
const NativeAssetIcon = useNativeTokenIcon();
const TokenIcon = useMemo(() => {
- return type === "native" ? NativeAssetIcon : AssetIcon;
- }, [type, NativeAssetIcon]);
+ return type === "native" || symbol === nativeSymbol
+ ? NativeAssetIcon
+ : AssetIcon;
+ }, [type, symbol, nativeSymbol, NativeAssetIcon]);
return (
diff --git a/packages/next-common/services/url.js b/packages/next-common/services/url.js
index 1ae6328bb0..4050f1a03d 100644
--- a/packages/next-common/services/url.js
+++ b/packages/next-common/services/url.js
@@ -118,6 +118,11 @@ export const secretarySalaryCycleUnregisteredPaymentsApi = (index) =>
export const secretarySalaryCycleFeedsApi = (index) =>
`secretary/salary/cycles/${index}/feeds`;
+export const secretaryStatisticsCyclesApi =
+ "secretary/statistics/salary/cycles";
+export const secretaryStatisticsMembersApi =
+ "secretary/statistics/salary/members";
+
// ambassador
export const ambassadorParamsApi = "ambassador/params";
export const ambassadorMembersApiUri = "ambassador/members";
diff --git a/packages/next-common/utils/consts/menu/secretary.js b/packages/next-common/utils/consts/menu/secretary.js
index af5f433c4d..9f10181362 100644
--- a/packages/next-common/utils/consts/menu/secretary.js
+++ b/packages/next-common/utils/consts/menu/secretary.js
@@ -38,6 +38,19 @@ function getSecretarySalaryMenu() {
};
}
+function getSecretaryStatisticsMenu() {
+ const chainSettings = getChainSettings(process.env.NEXT_PUBLIC_CHAIN);
+ if (!chainSettings.modules.secretary) {
+ return null;
+ }
+
+ return {
+ value: "secretary-statistics",
+ name: Names.statistics,
+ pathname: "/secretary/statistics",
+ };
+}
+
export function getSecretaryMenu() {
const chainSettings = getChainSettings(process.env.NEXT_PUBLIC_CHAIN);
if (!chainSettings.modules.secretary) {
@@ -48,8 +61,10 @@ export function getSecretaryMenu() {
name: Names.secretary,
icon: ,
pathname: "/secretary",
- items: [getSecretaryMembersMenu(), getSecretarySalaryMenu()].filter(
- Boolean,
- ),
+ items: [
+ getSecretaryMembersMenu(),
+ getSecretarySalaryMenu(),
+ getSecretaryStatisticsMenu(),
+ ].filter(Boolean),
};
}