Skip to content

Commit 4a792ac

Browse files
feat(charges): add a unit charges page (#779)
1 parent 5639851 commit 4a792ac

File tree

11 files changed

+269
-40
lines changed

11 files changed

+269
-40
lines changed

components/CreateDatasetStorageSubscription.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as yup from "yup";
1313

1414
import { useEnqueueError } from "../hooks/useEnqueueStackError";
1515
import { useGetStorageCost } from "../hooks/useGetStorageCost";
16-
import { coinsFormatter } from "../utils/app/coins";
16+
import { formatCoins } from "../utils/app/coins";
1717
import { getErrorMessage } from "../utils/next/orvalError";
1818

1919
export interface CreateDatasetStorageSubscriptionProps {
@@ -77,9 +77,7 @@ export const CreateDatasetStorageSubscription = ({
7777
sx={{ maxWidth: 100 }}
7878
type="number"
7979
/>
80-
{cost && (
81-
<span>Cost: {coinsFormatter.format(cost * values.allowance).slice(1)}C</span>
82-
)}
80+
{cost && <span>Cost: {formatCoins(cost * values.allowance)}</span>}
8381
<Button disabled={isSubmitting || !isValid} onClick={submitForm}>
8482
Create
8583
</Button>

components/finance/ProductCharges.tsx

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
Box,
88
Container,
99
Divider,
10-
MenuItem,
1110
Paper,
12-
Select,
1311
Table,
1412
TableBody,
1513
TableCell,
@@ -19,10 +17,11 @@ import {
1917
} from "@mui/material";
2018
import { filesize } from "filesize";
2119

20+
import { formatCoins } from "../../utils/app/coins";
2221
import { toLocalTimeString } from "../../utils/app/datetime";
2322
import { formatOrdinals } from "../../utils/app/ordinals";
24-
import { getBillingPeriods } from "../../utils/app/products";
2523
import { CenterLoader } from "../CenterLoader";
24+
import { SelectBillingCycle } from "./SelectBillingCycle";
2625

2726
export interface ProductChargesProps {
2827
productId: ProductDetail["id"];
@@ -41,11 +40,6 @@ export const ProductCharges = ({ productId }: ProductChargesProps) => {
4140
return <CenterLoader />;
4241
}
4342

44-
const periods = getBillingPeriods(
45-
productData.product.unit.billing_day,
46-
productData.product.product.created,
47-
);
48-
4943
return (
5044
<Container maxWidth="md">
5145
<Typography variant="h1">Product Ledger</Typography>
@@ -61,18 +55,14 @@ export const ProductCharges = ({ productId }: ProductChargesProps) => {
6155
Billing period (Billed on the {formatOrdinals(productData.product.unit.billing_day)} of the
6256
month)
6357
</Typography>
64-
<Select
65-
id="select-billing-cycle"
66-
size="small"
67-
value={monthDelta}
68-
onChange={(event) => {
69-
setMonthDelta(Number(event.target.value));
70-
}}
71-
>
72-
{periods.map(([i, d1, d2]) => (
73-
<MenuItem key={d1} value={i}>{`${d1}${d2}`}</MenuItem>
74-
))}
75-
</Select>
58+
59+
<SelectBillingCycle
60+
billingDay={productData.product.unit.billing_day}
61+
created={productData.product.product.created}
62+
monthDelta={monthDelta}
63+
onChange={setMonthDelta}
64+
/>
65+
7666
<Typography gutterBottom sx={{ mt: 2 }} variant="h2">
7767
Charges
7868
</Typography>
@@ -103,7 +93,7 @@ export const ProductCharges = ({ productId }: ProductChargesProps) => {
10393
<TableCell>{charge.charge.additional_data?.job_job}</TableCell>
10494
<TableCell>{charge.charge.additional_data?.job_collection}</TableCell>
10595
<TableCell>{charge.closed ? "Yes" : "No"}</TableCell>
106-
<TableCell>C&nbsp;{charge.charge.coins}</TableCell>
96+
<TableCell>{formatCoins(charge.charge.coins)}</TableCell>
10797
<TableCell>{charge.charge.username}</TableCell>
10898
<TableCell>{toLocalTimeString(charge.charge.timestamp, true, true)}</TableCell>
10999
</TableRow>
@@ -142,7 +132,7 @@ export const ProductCharges = ({ productId }: ProductChargesProps) => {
142132
<TableCell>{charge.date}</TableCell>
143133
{/* TODO: assert additional_data to interface from data-manager-client when it's updated */}
144134
<TableCell>{filesize(charge.additional_data?.peak_bytes ?? 0)}</TableCell>
145-
<TableCell>C&nbsp;{charge.coins}</TableCell>
135+
<TableCell>{formatCoins(charge.coins)}</TableCell>
146136
</TableRow>
147137
))
148138
) : (
@@ -159,7 +149,7 @@ export const ProductCharges = ({ productId }: ProductChargesProps) => {
159149
<Box textAlign="right">
160150
<Typography variant="h3">Total Charges</Typography>
161151
<Typography variant="subtitle1">To be paid by the unit owner</Typography>
162-
C&nbsp;{chargesData.coins}
152+
{formatCoins(chargesData.coins)}
163153
</Box>
164154
</Container>
165155
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Dispatch, SetStateAction } from "react";
2+
3+
import { MenuItem, Select } from "@mui/material";
4+
5+
import { getBillingPeriods } from "../../utils/app/products";
6+
7+
export interface SelectBillingCycleProps {
8+
monthDelta: number;
9+
onChange: Dispatch<SetStateAction<number>>;
10+
billingDay: number;
11+
created: string;
12+
}
13+
14+
export const SelectBillingCycle = ({
15+
billingDay,
16+
created,
17+
monthDelta,
18+
onChange,
19+
}: SelectBillingCycleProps) => {
20+
const periods = getBillingPeriods(billingDay, created);
21+
22+
return (
23+
<Select
24+
id="select-billing-cycle"
25+
size="small"
26+
value={monthDelta}
27+
onChange={(event) => {
28+
onChange(Number(event.target.value));
29+
}}
30+
>
31+
{periods.map(([i, d1, d2]) => (
32+
<MenuItem key={d1} value={i}>{`${d1}${d2}`}</MenuItem>
33+
))}
34+
</Select>
35+
);
36+
};

components/finance/UnitCharges.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useState } from "react";
2+
3+
import type { UnitDetail, UnitProductChargeSummary } from "@squonk/account-server-client";
4+
import { useGetUnit, useGetUnitCharges } from "@squonk/account-server-client/unit";
5+
6+
import {
7+
Box,
8+
Container,
9+
Divider,
10+
Paper,
11+
Table,
12+
TableBody,
13+
TableCell,
14+
TableHead,
15+
TableRow,
16+
Typography,
17+
} from "@mui/material";
18+
19+
import { formatCoins } from "../../utils/app/coins";
20+
import { ChargesLinkIconButton } from "../products/ChargesLinkIconButton";
21+
import { SelectBillingCycle } from "./SelectBillingCycle";
22+
23+
export interface UnitChargesProps {
24+
unitId: UnitDetail["id"];
25+
}
26+
27+
const productTypes: Record<UnitProductChargeSummary["product_type"], string> = {
28+
DATA_MANAGER_PROJECT_TIER_SUBSCRIPTION: "Project Subscription",
29+
DATA_MANAGER_STORAGE_SUBSCRIPTION: "Dataset Subscription",
30+
};
31+
32+
export const UnitCharges = ({ unitId }: UnitChargesProps) => {
33+
const [monthDelta, setMonthDelta] = useState(0);
34+
35+
const { data: unit } = useGetUnit(unitId);
36+
const { data: charges } = useGetUnitCharges(unitId, { pbp: monthDelta });
37+
38+
const processingTotal = charges?.summary.charges.find(
39+
(charge) => charge.type === "PROCESSING",
40+
)?.coins;
41+
const storageTotal = charges?.summary.charges.find((charge) => charge.type === "STORAGE")?.coins;
42+
const totalCharges = charges?.summary.charges
43+
.map((charge) => charge.coins)
44+
.reduce((acc, charge) => acc + Number.parseFloat(charge), 0);
45+
46+
return (
47+
<Container maxWidth="md">
48+
<Typography variant="h1">Unit Ledger</Typography>
49+
<Typography variant="subtitle2">{unit?.id}</Typography>
50+
<Typography gutterBottom component="p" variant="h5">
51+
Charges against: {unit?.name}
52+
</Typography>
53+
<Typography gutterBottom>
54+
<strong>Billed to</strong>: unit <em>{unit?.name}</em>
55+
</Typography>
56+
<Typography gutterBottom component="h2" variant="h4">
57+
Billing period
58+
</Typography>
59+
60+
{unit?.billing_day && unit.created && (
61+
<SelectBillingCycle
62+
billingDay={unit.billing_day}
63+
created={unit.created}
64+
monthDelta={monthDelta}
65+
onChange={setMonthDelta}
66+
/>
67+
)}
68+
69+
<Typography gutterBottom sx={{ mt: 2 }} variant="h2">
70+
Charges
71+
</Typography>
72+
<Paper>
73+
<Table size="small" sx={{ marginBottom: 2 }}>
74+
<TableHead>
75+
<TableRow>
76+
<TableCell></TableCell>
77+
<TableCell>Product Type</TableCell>
78+
<TableCell>Storage Charges</TableCell>
79+
<TableCell>Processing Charges</TableCell>
80+
<TableCell>Actions</TableCell>
81+
</TableRow>
82+
</TableHead>
83+
<TableBody>
84+
{charges?.products.map((product, index) => (
85+
<TableRow key={product.product_id}>
86+
<TableCell>{index + 1}</TableCell>
87+
<TableCell>{productTypes[product.product_type]}</TableCell>
88+
<TableCell>
89+
{formatCoins(
90+
product.charges.find((charge) => charge.type === "STORAGE")?.coins ?? 0,
91+
)}
92+
</TableCell>
93+
<TableCell>
94+
{formatCoins(
95+
product.charges.find((charge) => charge.type === "PROCESSING")?.coins ?? 0,
96+
)}
97+
</TableCell>
98+
<TableCell>
99+
<ChargesLinkIconButton productId={product.product_id} />
100+
</TableCell>
101+
</TableRow>
102+
))}
103+
</TableBody>
104+
</Table>
105+
</Paper>
106+
107+
<Divider sx={{ my: 2 }} />
108+
<Box textAlign="right">
109+
<Typography variant="h4">Subtotal Total</Typography>
110+
Processing: {!!processingTotal && formatCoins(processingTotal)}
111+
<br />
112+
Storage: {!!storageTotal && formatCoins(storageTotal)}
113+
<Typography variant="h3">Total Charges</Typography>
114+
<Typography variant="subtitle1">To be paid by the unit owner</Typography>
115+
Total: {!!totalCharges && formatCoins(totalCharges)}
116+
</Box>
117+
</Container>
118+
);
119+
};

components/products/AdjustProjectProduct.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { TextField } from "formik-mui";
1515

1616
import { useEnqueueError } from "../../hooks/useEnqueueStackError";
1717
import { useGetStorageCost } from "../../hooks/useGetStorageCost";
18-
import { coinsFormatter } from "../../utils/app/coins";
18+
import { formatCoins } from "../../utils/app/coins";
1919
import { getErrorMessage } from "../../utils/next/orvalError";
2020
import { FormikModalWrapper } from "../modals/FormikModalWrapper";
2121

@@ -75,7 +75,7 @@ export const AdjustProjectProduct = ({ product, allowance }: AdjustProjectProduc
7575
sx={{ maxWidth: 100 }}
7676
type="number"
7777
/>
78-
{cost && <span>Cost: {coinsFormatter.format(cost * values.allowance).slice(1)}C</span>}
78+
{cost && <span>Cost: {formatCoins(cost * values.allowance).slice(1)}C</span>}
7979
</Box>
8080
)}
8181
</FormikModalWrapper>

components/userContext/SelectUnit.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { UnitGetResponse } from "@squonk/account-server-client";
22
import { useGetOrganisationUnits } from "@squonk/account-server-client/unit";
33

4+
import ReceiptIcon from "@mui/icons-material/Receipt";
45
import type { AutocompleteProps } from "@mui/material";
5-
import { Autocomplete, Box, TextField, Typography } from "@mui/material";
6+
import { Autocomplete, Box, IconButton, TextField, Tooltip, Typography } from "@mui/material";
67

78
import { projectPayload, useCurrentProjectId } from "../../hooks/projectHooks";
89
import { useSelectedOrganisation } from "../../state/organisationSelection";
@@ -50,7 +51,25 @@ export const SelectUnit = (props: SelectUnitProps) => {
5051
{...params}
5152
InputProps={{
5253
...params.InputProps,
53-
startAdornment: <ItemIcons item={unit} />,
54+
startAdornment: (
55+
<>
56+
<ItemIcons item={unit} />
57+
{!!unit && (
58+
<Tooltip title="Charges">
59+
<span>
60+
<IconButton
61+
href={`/unit/${unit.id}/charges`}
62+
size="small"
63+
sx={{ p: "1px" }}
64+
target="_blank"
65+
>
66+
<ReceiptIcon />
67+
</IconButton>
68+
</span>
69+
</Tooltip>
70+
)}
71+
</>
72+
),
5473
}}
5574
label="Unit"
5675
/>
@@ -73,12 +92,10 @@ export const SelectUnit = (props: SelectUnitProps) => {
7392
setUnit(newUnit ?? undefined);
7493
}}
7594
/>
76-
{
77-
// N.B. This isn't helperText as MUI doesn't make that selectable
78-
<Typography color="text.secondary" variant="body2">
79-
{unit?.id}
80-
</Typography>
81-
}
95+
{/* N.B. This isn't helperText as MUI doesn't make that selectable */}
96+
<Typography color="text.secondary" variant="body2">
97+
{unit?.id}
98+
</Typography>
8299
</>
83100
);
84101
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { UnitDetail } from "@squonk/account-server-client";
2+
import { useGetUnit } from "@squonk/account-server-client/unit";
3+
4+
import Head from "next/head";
5+
6+
import { UnitCharges } from "../../components/finance/UnitCharges";
7+
8+
export interface UnitChargesViewProps {
9+
unitId: UnitDetail["id"];
10+
}
11+
12+
export const UnitChargesView = ({ unitId }: UnitChargesViewProps) => {
13+
const { data } = useGetUnit(unitId);
14+
15+
return (
16+
<>
17+
<Head>
18+
<title>Squonk | {data?.name} Charges</title>
19+
</Head>
20+
<UnitCharges unitId={unitId} />
21+
</>
22+
);
23+
};

hooks/useSyncProject.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export const useSyncProject = () => {
4141
if (isReady && projectId && !project) {
4242
const newQuery = { ...query, project: projectId };
4343
if (!compare(query, newQuery)) {
44-
console.log(query, newQuery);
4544
const href = { query: newQuery, pathname };
4645

4746
replace(href, undefined);

0 commit comments

Comments
 (0)