Introduce invocing page

main
MiguelMLorente 2025-12-07 00:50:51 +01:00
parent 68149c1976
commit c15012afa7
11 changed files with 238 additions and 11 deletions

View File

@ -1,14 +1,15 @@
import "./App.css"; import "./App.css";
import { Login } from "./pages/Login.tsx"; import { Login } from "./pages/auth/Login.tsx";
import icon from "./paella-icon.png"; import icon from "./paella-icon.png";
import { Outlet, Route, Routes } from "react-router"; import { Outlet, Route, Routes } from "react-router";
import { SignUpPage } from "./pages/signup/SignUpPage.tsx"; import { SignUpPage } from "./pages/signup/SignUpPage.tsx";
import { Register } from "./pages/Register.tsx"; import { Register } from "./pages/auth/Register.tsx";
import { Buy } from "./pages/Buy.tsx"; import { Buy } from "./pages/buy/Buy.tsx";
import { NotFound } from "./pages/NotFound.tsx"; import { NotFound } from "./pages/NotFound.tsx";
import { BuyReturn } from "./pages/BuyReturn.tsx"; import { BuyReturn } from "./pages/buy/BuyReturn.tsx";
import { Grid, Paper } from "@mui/material"; import { Grid, Paper } from "@mui/material";
import { AuthenticatedRoute } from "./pages/auth/AuthenticatedRoute.tsx"; import { AuthenticatedRoute } from "./pages/auth/AuthenticatedRoute.tsx";
import { InvoicePage } from "./pages/invoice/InvoicePage.tsx";
const App = () => ( const App = () => (
<Paper <Paper
@ -38,6 +39,7 @@ const App = () => (
<Route path="/signup" element={<SignUpPage />} /> <Route path="/signup" element={<SignUpPage />} />
<Route path="/buy" element={<Buy />} /> <Route path="/buy" element={<Buy />} />
<Route path="/buy/return" element={<BuyReturn />} /> <Route path="/buy/return" element={<BuyReturn />} />
<Route path="/summary" element={<InvoicePage />} />
</Route> </Route>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>

View File

@ -86,3 +86,23 @@ export const getAvailableTokenCount = () =>
(axios.get("/tokens", getJwtHeader()) as Promise<AxiosResponse<Tokens>>).then( (axios.get("/tokens", getJwtHeader()) as Promise<AxiosResponse<Tokens>>).then(
(response) => response.data, (response) => response.data,
); );
export interface InvoiceSummary {
purchaseData: {
product: string;
units: number;
status: string;
date: string;
}[];
sessionsData: {
date: string;
status: string;
}[];
}
export const getInvoicingSummary = () =>
(
axios.get("/invoice", getJwtHeader()) as Promise<
AxiosResponse<InvoiceSummary>
>
).then((response) => response.data);

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { login } from "../api/client"; import { login } from "../../api/client";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { import {
Button, Button,

View File

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import * as zod from "zod"; import * as zod from "zod";
import { registerUser } from "../api/client"; import { registerUser } from "../../api/client";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { import {
Alert, Alert,

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { startBuyFlow } from "../api/client"; import { startBuyFlow } from "../../api/client";
import { import {
Button, Button,
FormControlLabel, FormControlLabel,
@ -22,7 +22,6 @@ export const Buy = () => {
return ( return (
<Grid container direction="column" spacing={2} alignItems="center"> <Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Buy tokens</Typography> <Typography variant="h3">Buy tokens</Typography>
<Paper <Paper
variant="outlined" variant="outlined"
sx={{ sx={{

View File

@ -1,4 +1,4 @@
import { completeBuyFlow } from "../api/client"; import { completeBuyFlow } from "../../api/client";
import { CircularProgress, Grid, Typography } from "@mui/material"; import { CircularProgress, Grid, Typography } from "@mui/material";
export const BuyReturn = () => { export const BuyReturn = () => {

View File

@ -0,0 +1,39 @@
import { Async } from "react-async";
import { getInvoicingSummary, type InvoiceSummary } from "../../api/client";
import { Alert, Button, Grid, Typography } from "@mui/material";
import { PurchaseSummaryTable } from "./PurchaseSummaryTable";
import { SessionSummaryTable } from "./SessionSummaryTable";
import { useNavigate } from "react-router";
export const InvoicePage = () => {
const invoiceSummaryPromise = getInvoicingSummary();
const navigate = useNavigate();
return (
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Summary</Typography>
<Async promise={invoiceSummaryPromise}>
<Async.Fulfilled<InvoiceSummary>>
{(invoiceSummary) => (
<Grid container direction="column" spacing={2}>
<PurchaseSummaryTable invoiceSummary={invoiceSummary} />
<SessionSummaryTable invoiceSummary={invoiceSummary} />
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={() => navigate("/signup")}
>
Back to sessions
</Button>
</Grid>
)}
</Async.Fulfilled>
<Async.Rejected>
<Alert severity="error">Something went wrong</Alert>
</Async.Rejected>
</Async>
</Grid>
);
};

View File

@ -0,0 +1,85 @@
import { type InvoiceSummary } from "../../api/client";
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Typography,
} from "@mui/material";
import { useState } from "react";
import { FormattedDate } from "react-intl";
export const PurchaseSummaryTable = (props: {
invoiceSummary: InvoiceSummary;
}) => {
const { invoiceSummary } = props;
const [page, setPage] = useState(0);
const rowsPerPage = 5;
return (
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Typography
sx={{
paddingBottom: "10px",
}}
variant="h6"
>
Purchases summary
</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Purchase date
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Units
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Purchase status
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{invoiceSummary.purchaseData
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map(({ date, units, status }) => (
<TableRow>
<TableCell align="center">
<FormattedDate
value={new Date(date)}
day="numeric"
month="short"
year="numeric"
/>
</TableCell>
<TableCell align="center">{units}</TableCell>
<TableCell align="center">{status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={invoiceSummary.purchaseData.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={(_, newPage) => setPage(newPage)}
rowsPerPageOptions={[]}
/>
</Paper>
);
};

View File

@ -0,0 +1,81 @@
import { type InvoiceSummary } from "../../api/client";
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Typography,
} from "@mui/material";
import { useState } from "react";
import { FormattedDate } from "react-intl";
export const SessionSummaryTable = (props: {
invoiceSummary: InvoiceSummary;
}) => {
const { invoiceSummary } = props;
const [page, setPage] = useState(0);
const rowsPerPage = 5;
return (
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Typography
sx={{
paddingBottom: "10px",
}}
variant="h6"
>
Sessions summary
</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Session date
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Session status
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{invoiceSummary.sessionsData
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map(({ date, status }) => (
<TableRow>
<TableCell align="center">
<FormattedDate
value={new Date(date)}
day="numeric"
month="short"
year="numeric"
/>
</TableCell>
<TableCell align="center">{status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={invoiceSummary.purchaseData.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={(_, newPage) => setPage(newPage)}
rowsPerPageOptions={[]}
/>
</Paper>
);
};

View File

@ -68,9 +68,9 @@ export const TokensSummary = (props: { tokens: Tokens }) => {
variant="contained" variant="contained"
color="secondary" color="secondary"
sx={{ width: "100%" }} sx={{ width: "100%" }}
onClick={() => navigate("/invoices")} onClick={() => navigate("/summary")}
> >
See all purchases See your summary
</Button> </Button>
</Grid> </Grid>
</Paper> </Paper>

View File

@ -8,6 +8,7 @@ const serverRoutes = [
"/buy/cancel", "/buy/cancel",
"/session", "/session",
"/tokens", "/tokens",
"/invoice",
]; ];
const baseServerConfigBuilder = (domain: string) => ({ const baseServerConfigBuilder = (domain: string) => ({
proxy: Object.fromEntries( proxy: Object.fromEntries(