Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
atrincas committed Dec 5, 2024
1 parent b1383f4 commit c9e94b9
Show file tree
Hide file tree
Showing 20 changed files with 234 additions and 120 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@ts-rest/react-query": "3.51.0",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"country-iso-3-to-2": "^1.1.1",
"d3": "7.9.0",
"framer-motion": "11.11.9",
"jotai": "2.10.1",
Expand Down
1 change: 1 addition & 0 deletions client/src/app/auth/api/[...nextauth]/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cookies } from "next/headers";

import { UserWithAccessToken } from "@shared/dtos/users/user.dto";
import { LogInSchema } from "@shared/schemas/auth/login.schema";
import type {
Expand Down
10 changes: 0 additions & 10 deletions client/src/app/projects/[id]/store.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { COST_TYPE_SELECTOR } from "@shared/entities/projects.entity";
import { atom } from "jotai";
import { parseAsStringLiteral, useQueryState } from "nuqs";

import { CASH_FLOW_VIEWS } from "@/containers/projects/custom-project/annual-project-cash-flow/header/tabs";

export const projectsUIState = atom<{
projectSummaryOpen: boolean;
Expand All @@ -13,10 +10,3 @@ export const showCostDetailsAtom = atom<boolean>(false);
export const costDetailsFilterAtom = atom<COST_TYPE_SELECTOR>(
COST_TYPE_SELECTOR.TOTAL,
);

export function useProjectCashFlowTab() {
return useQueryState(
"cashflowTab",
parseAsStringLiteral(CASH_FLOW_VIEWS).withDefault("chart"),
);
}
10 changes: 10 additions & 0 deletions client/src/app/projects/[id]/url-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { parseAsStringLiteral, useQueryState } from "nuqs";

import { CASH_FLOW_VIEWS } from "@/containers/projects/custom-project/annual-project-cash-flow/header/tabs";

export function useProjectCashFlowTab() {
return useQueryState(
"cashflowTab",
parseAsStringLiteral(CASH_FLOW_VIEWS).withDefault("chart"),
);
}
10 changes: 8 additions & 2 deletions client/src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import { cn } from "@/lib/utils";

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
/**
* When true, adds a horizontal scrollbar to the scroll area
*/
hasHorizontalScroll?: boolean;
}
>(({ className, children, hasHorizontalScroll, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
Expand All @@ -19,6 +24,7 @@ const ScrollArea = React.forwardRef<
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
{hasHorizontalScroll && <ScrollBar orientation="horizontal" />}
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChartNoAxesColumnIcon, Table2Icon } from "lucide-react";

import { useProjectCashFlowTab } from "@/app/projects/[id]/store";
import { useProjectCashFlowTab } from "@/app/projects/[id]/url-store";

import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC } from "react";

import { useProjectCashFlowTab } from "@/app/projects/[id]/store";
import { useProjectCashFlowTab } from "@/app/projects/[id]/url-store";

import CashflowChart from "@/containers/projects/custom-project/annual-project-cash-flow/chart";
import Header from "@/containers/projects/custom-project/annual-project-cash-flow/header";
Expand All @@ -12,7 +12,7 @@ const AnnualProjectCashFlow: FC = () => {
const [tab] = useProjectCashFlowTab();

return (
<Card variant="secondary" className="flex flex-col p-0">
<Card variant="secondary" className="flex flex-col overflow-hidden p-0">
<Header />
{tab === "table" ? <CashFlowTable /> : <CashflowChart />}
</Card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ export const chartData = YEARS.map((y) => ({
}));
export const tableData = mockData.data.yearlyBreakdown;

export const columns = [
export const columns = (years: string[]) => [
columnHelper.accessor("costName", {
header: () => <span>Project</span>,
}),
...YEARS.map((year) =>
...years.map((year) =>
columnHelper.accessor(`costValues.${year}`, {
header: () => <span>{year}</span>,
cell: (info) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
tableData,
columns,
} from "@/containers/projects/custom-project/annual-project-cash-flow/table/columns";
import { getBreakdownYears } from "@/containers/projects/custom-project/annual-project-cash-flow/utils";
import mockData from "@/containers/projects/custom-project/mock-data";

import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
TableBody,
TableCell,
Expand All @@ -33,7 +35,7 @@ const CashFlowTable: FC = () => {
});
const table = useReactTable({
data: tableData,
columns: columns,
columns: columns(getBreakdownYears(mockData.data.yearlyBreakdown)),
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
state: {
Expand All @@ -44,7 +46,7 @@ const CashFlowTable: FC = () => {

return (
<div className="relative flex h-full flex-1 flex-col overflow-hidden">
<ScrollArea className="scroll-x-auto w-full">
<ScrollArea hasHorizontalScroll>
<div className="table h-full w-full table-fixed">
<table className="w-full caption-bottom text-sm">
<TableHeader className="sticky top-0">
Expand Down Expand Up @@ -106,7 +108,6 @@ const CashFlowTable: FC = () => {
)}
</TableBody>
</table>
<ScrollBar orientation="horizontal" className="" />
</div>
</ScrollArea>
<TablePagination
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { YearlyBreakdown } from "@shared/dtos/custom-projects/custom-project-output.dto";

function getBreakdownYears(data: YearlyBreakdown[]): string[] {
if (data.length === 0) return [];

return Object.keys(data[0].costValues)
.map((y) => y)
.sort((a, b) => Number(a) - Number(b));
}

// TODO: This functionality will be used when backend API response is updated
// function parseYearlyBreakdownForChart(
// data: YearlyBreakdown[],
// years: string[],
// ) {
// if (data.length === 0) return [];

// let estimatedRenevueValues = {};
// let annualNetCashFlowValues = {};

// data.forEach((d) => {
// switch (d.costName) {
// case "estimatedRenevue":
// estimatedRenevueValues = d.costValues;
// break;
// case "annualNetCashFlow":
// annualNetCashFlowValues = d.costValues;
// break;
// default:
// break;
// }
// });

// return years.map((y) => ({
// year: y,
// estimatedRevenue: estimatedRenevueValues[y],
// annualNetCashFlow: annualNetCashFlowValues[y],
// }));
// }

export { getBreakdownYears };
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const PARAMETERS = [
{
key: FILTER_KEYS[3],
label: "Cost type",
className: "",
className: "flex-1",
options: [
{
label: COST_TYPE_SELECTOR.TOTAL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { FC, useState } from "react";

import { ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons";
import {
useReactTable,
getCoreRowModel,
flexRender,
SortingState,
getSortedRowModel,
} from "@tanstack/react-table";
import { ChevronsUpDownIcon } from "lucide-react";

import { cn } from "@/lib/utils";

Expand All @@ -24,17 +22,13 @@ import {
} from "@/components/ui/table";

export interface CostItem {
name: string;
costName: string;
label: string;
value: number;
value: string;
}

interface CostDetailTableProps {
data: {
costName: string;
label: string;
value: string;
}[];
data: CostItem[];
}
const CostDetailTable: FC<CostDetailTableProps> = ({ data }) => {
const [sorting, setSorting] = useState<SortingState>([]);
Expand All @@ -61,35 +55,9 @@ const CostDetailTable: FC<CostDetailTableProps> = ({ data }) => {
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<div
className={cn({
"flex items-center justify-between": true,
"cursor-pointer select-none":
header.column.getCanSort(),
})}
onClick={header.column.getToggleSortingHandler()}
title={
header.column.getCanSort()
? header.column.getNextSortingOrder() === "asc"
? "Sort ascending"
: header.column.getNextSortingOrder() === "desc"
? "Sort descending"
: "Clear sort"
: undefined
}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{{
asc: <ChevronUpIcon className="h-4 w-4" />,
desc: <ChevronDownIcon className="h-4 w-4" />,
}[header.column.getIsSorted() as string] ?? (
<ChevronsUpDownIcon className="h-4 w-4" />
)}
</div>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
Expand All @@ -106,27 +74,21 @@ const CostDetailTable: FC<CostDetailTableProps> = ({ data }) => {
"divide-background border-b-background": true,
"bg-big-stone-950 hover:bg-big-stone-950":
row.original.costName === "capitalExpenditure" ||
row.original.costName === "operatingExpenditure" ||
row.original.costName === "operationalExpenditure" ||
row.original.costName === "totalCost",
})}
>
{row.getVisibleCells().map((cell) => {
console.log(cell);
return (
<TableCell
key={cell.id}
className={cn({
"px-4": cell.column.id === "value",
"text-big-stone-200": cell.column.id === "label",
})}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
);
})}
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cn({
"px-4": cell.column.id === "value",
"text-big-stone-200": cell.column.id === "label",
})}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { CustomProjectCostDetails } from "@shared/dtos/custom-projects/custom-project-output.dto";

import { formatCurrency } from "@/lib/format";

import { CostItem } from "@/containers/projects/custom-project/cost-details/table";

const capitalExpenditurePattern =
/^(capitalExpenditure|feasibilityAnalysis|conservationPlanningAndAdmin|dataCollectionAndFieldCost|communityRepresentation|blueCarbonProjectPlanning|establishingCarbonRights|validation|implementationLabor)$/;
const operationalExpenditurePattern =
/^(operationalExpenditure|monitoring|maintenance|communityBenefitSharingFund|carbonStandardFees|baselineReassessment|mrv|longTermProjectOperatingCost)$/;
const currencySettings = { maximumFractionDigits: 0 };
const customProjectCostDetailsLabelMap: Record<
keyof CustomProjectCostDetails,
string
> = {
capitalExpenditure: "Capital expenditure",
operationalExpenditure: "Operating expenditure",
totalCost: "Total cost",
feasibilityAnalysis: "Feasibility analysis",
conservationPlanningAndAdmin: "Conservation planning and admin",
dataCollectionAndFieldCost: "Data collection and field costs",
communityRepresentation: "Community representation",
blueCarbonProjectPlanning: "Blue carbon project planning",
establishingCarbonRights: "Establishing carbon rights",
validation: "Validation",
implementationLabor: "Implementation labor",
monitoring: "Monitoring",
maintenance: "Maintenance",
communityBenefitSharingFund: "Community benefit sharing fund",
carbonStandardFees: "Carbon standard fees",
baselineReassessment: "Baseline reassessment",
mrv: "MRV",
longTermProjectOperatingCost: "Long-term project operating",
} as const;

function parseCostDetailsForTable(data: CustomProjectCostDetails): CostItem[] {
const capitalItems: CostItem[] = [];
const operationalItems: CostItem[] = [];

Object.entries(data).forEach(([key, value]) => {
if (key === "totalCost") {
return;
}

const costItem: CostItem = {
costName: key,
label:
customProjectCostDetailsLabelMap[key as keyof CustomProjectCostDetails],
value: formatCurrency(value, currencySettings),
};

if (capitalExpenditurePattern.test(key)) {
capitalItems.push(costItem);
} else if (operationalExpenditurePattern.test(key)) {
operationalItems.push(costItem);
}
});

const totalCostItem: CostItem = {
costName: "totalCost",
label: "Total cost",
value: formatCurrency(data.totalCost, currencySettings),
};

// Array should be in this order
return [...capitalItems, ...operationalItems, totalCostItem];
}

export { parseCostDetailsForTable };
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { FC } from "react";

import ReactCountryFlag from "react-country-flag";

import getCountryISO2 from "country-iso-3-to-2";

import Metric from "@/components/ui/metric";

interface SubValue {
Expand Down Expand Up @@ -43,7 +45,7 @@ const DetailItem: FC<DetailItemProps> = ({
{countryCode && (
<ReactCountryFlag
className="h-[16px] w-[22px] rounded"
countryCode={countryCode}
countryCode={getCountryISO2(countryCode)}
svg
/>
)}
Expand Down
Loading

0 comments on commit c9e94b9

Please sign in to comment.