Skip to content

Commit

Permalink
database dump & history viewing
Browse files Browse the repository at this point in the history
  • Loading branch information
squi-ddy committed Oct 24, 2024
1 parent 468d2e9 commit c39e845
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 14 deletions.
31 changes: 27 additions & 4 deletions backend/src/api/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { TopupTable } from "types/topup"
import { TransactionTable } from "types/transaction"
import { UserTable, UserType } from "types/user"
import Archiver from "archiver"
import ObjectsToCsv from "objects-to-csv"
import { objectsToCsv } from "utils"

const router = Router()
Expand Down Expand Up @@ -122,6 +121,30 @@ router.get("/dump", async (_, res) => {
SELECT * FROM Users
`

const topupHeaders = (
await sql<{ column_name: string }[]>`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'topup'
`
).map((row) => row.column_name)

const transactionHeaders = (
await sql<{ column_name: string }[]>`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'transactions'
`
).map((row) => row.column_name)

const userHeaders = (
await sql<{ column_name: string }[]>`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users'
`
).map((row) => row.column_name)

res.writeHead(200, {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename=dump-${new Date().toISOString()}.zip`,
Expand All @@ -131,13 +154,13 @@ router.get("/dump", async (_, res) => {
archive.pipe(res)

await archive
.append(objectsToCsv(topups), {
.append(objectsToCsv(topups, topupHeaders), {
name: "topups.csv",
})
.append(objectsToCsv(transactions), {
.append(objectsToCsv(transactions, transactionHeaders), {
name: "transactions.csv",
})
.append(objectsToCsv(users), {
.append(objectsToCsv(users, userHeaders), {
name: "users.csv",
})
.finalize()
Expand Down
10 changes: 6 additions & 4 deletions backend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ export async function getUser(id: string): Promise<UserDetails> {
}
}

export function objectsToCsv(data: Record<string, unknown>[]): string {
const keys = Object.keys(data[0])
const csv = [keys.join(",")]
export function objectsToCsv(
data: Record<string, unknown>[],
headers: string[],
): string {
const csv = [headers.join(",")]

for (const row of data) {
csv.push(keys.map((key) => row[key]).join(","))
csv.push(headers.map((key) => row[key]).join(","))
}

return csv.join("\n")
Expand Down
35 changes: 33 additions & 2 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import axios from "axios";
import { settings } from "./settings";
import { UserDetails } from "./types/user";
import { TopupDetails, TopupToken } from "./types/topup";
import { TransactionDetails, TransactionToken } from "./types/transaction";
import { TopupDetails, TopupHistoryDetails, TopupToken } from "./types/topup";
import {
TransactionDetails,
TransactionHistoryDetails,
TransactionToken,
} from "./types/transaction";

const fetcher = axios.create({
withCredentials: true,
Expand All @@ -13,6 +17,10 @@ export function getMSLoginUrl() {
return `${settings.BACKEND_URL}/ms`;
}

export function getDumpUrl() {
return `${settings.BACKEND_URL}/admin/dump`;
}

export async function getUser(): Promise<UserDetails | null> {
try {
const resp = await fetcher.get("/me");
Expand Down Expand Up @@ -134,3 +142,26 @@ export async function addMoney(
return false;
}
}

export async function getTransactionHistory(): Promise<
TransactionHistoryDetails[] | null
> {
try {
const resp = await fetcher.get("/student/getTransactions");
return resp.data.map((obj: Record<string, string>) => ({
...obj,
completed_timestamp: new Date(obj.completed_timestamp),
})) as TransactionHistoryDetails[];
} catch (error) {
return null;
}
}

export async function getTopupHistory(): Promise<TopupHistoryDetails[] | null> {
try {
const resp = await fetcher.get("/student/getTopups");
return resp.data as TopupHistoryDetails[];
} catch (error) {
return null;
}
}
10 changes: 9 additions & 1 deletion frontend/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ export default function Header() {
<Stack
direction="row"
spacing={2}
sx={{ p: 2, justifyContent: "space-between", alignItems: "center" }}
sx={{
p: 2,
justifyContent: "space-between",
alignItems: "center",
position: "sticky",
top: 0,
zIndex: 1000,
backgroundColor: "#121212",
}}
>
<Typography variant="h5">NUSHPay</Typography>
<Button
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import BoothMainPage from "./routes/booth_main_view";
import BoothConfirmPaymentPage from "./routes/booth_confirm_payment_view";
import AdminPage from "./routes/admin_view";
import UserProvider from "./UserProvider";
import StudentTransactionPage from "./routes/student_transactions_view";

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -48,6 +49,14 @@ const router = createBrowserRouter([
path: "/student/payment",
element: <StudentPaymentPage />,
},
{
path: "/student/transactions",
element: <StudentTransactionPage />,
},
{
path: "/student/topups",
element: <StudentTopupPage />,
},
{
path: "/booth",
element: <BoothMainPage />,
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/routes/admin_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import Header from "../components/header";
import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner";
import { useContext, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { addMoney, getTopup } from "../api";
import { addMoney, getDumpUrl, getTopup } from "../api";
import { UserType } from "../types/user";
import { Scanner } from "@yudiel/react-qr-scanner";
import { TopupDetails } from "../types/topup";
import Decimal from "decimal.js";
import { UserContext } from "../UserProvider";
import Download from "@mui/icons-material/Download";

export default function AdminPage() {
const [topup, setTopup] = useState<TopupDetails | null>(null);
Expand Down Expand Up @@ -54,6 +55,16 @@ export default function AdminPage() {
}}
>
<Typography variant="body1">Administration</Typography>
<Button
variant="contained"
size="large"
startIcon={<Download />}
onClick={() => {
window.open(getDumpUrl());
}}
>
Database Dump
</Button>
<Button
variant="contained"
size="large"
Expand Down
1 change: 0 additions & 1 deletion frontend/src/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export default function LoginPage() {
const resp = await login(username.current, password.current);
if (resp) {
await updateUser();
window.location.reload();
} else {
alert("Login failed");
}
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/routes/student_main_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ export default function StudentMainPage() {
Input the amount you want to pay, then press "Make Payment".
</Typography>
</Stack>

<Stack direction={"row"} spacing={2}>
<Button
variant="contained"
onClick={() => navigate("/student/transactions")}
>
Past Transactions
</Button>
<Button
variant="contained"
onClick={() => navigate("/student/topups")}
>
Past Topups
</Button>
</Stack>
</Stack>
</Container>
);
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/routes/student_topups_view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Container, Stack, Typography, Card, CardContent } from "@mui/material";
import { useNavigate } from "react-router-dom";
import Header from "../components/header";
import { useContext, useEffect, useState } from "react";
import { UserType } from "../types/user";
import Decimal from "decimal.js";
import { UserContext } from "../UserProvider";
import { getTopupHistory } from "../api";
import { TopupHistoryDetails } from "../types/topup";

export default function StudentTopupPage() {
const navigate = useNavigate();
const { user } = useContext(UserContext);
const [topupDetails, setTopupDetails] = useState<
TopupHistoryDetails[] | null
>(null);

useEffect(() => {
if (user === undefined) {
return;
} else if (user === null) {
return navigate("/");
} else if (user.type === UserType.STUDENT) {
getTopupHistory().then(setTopupDetails);
return;
} else if (user.type === UserType.ADMIN) {
navigate("/admin");
} else if (user.type === UserType.BOOTH) {
navigate("/booth");
}
}, [user]);

if (!user || !topupDetails) {
return <></>;
}

return (
<Container maxWidth="sm">
<Header />
<Stack
direction="column"
spacing={3}
sx={{
justifyContent: "center",
alignItems: "center",
}}
>
<Typography variant="body1">Topup History</Typography>
{topupDetails.length > 0 ? (
topupDetails.map((topup, idx) => (
<Card variant="outlined" key={idx}>
<CardContent>
<Typography variant="h6">
{new Decimal(topup.amount).toFixed(2)}
</Typography>
<Typography variant="body1">
Lucky draw code: {topup.lucky_draw_code}
</Typography>
</CardContent>
</Card>
))
) : (
<Typography variant="body2">No topups yet</Typography>
)}
</Stack>
</Container>
);
}
69 changes: 69 additions & 0 deletions frontend/src/routes/student_transactions_view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Container, Stack, Typography, Card, CardContent } from "@mui/material";
import { useNavigate } from "react-router-dom";
import Header from "../components/header";
import { useContext, useEffect, useState } from "react";
import { UserType } from "../types/user";
import Decimal from "decimal.js";
import { UserContext } from "../UserProvider";
import { TransactionHistoryDetails } from "../types/transaction";
import { getTransactionHistory } from "../api";

export default function StudentTransactionPage() {
const navigate = useNavigate();
const { user } = useContext(UserContext);
const [transactionDetails, setTransactionDetails] = useState<
TransactionHistoryDetails[] | null
>(null);

useEffect(() => {
if (user === undefined) {
return;
} else if (user === null) {
return navigate("/");
} else if (user.type === UserType.STUDENT) {
getTransactionHistory().then(setTransactionDetails);
return;
} else if (user.type === UserType.ADMIN) {
navigate("/admin");
} else if (user.type === UserType.BOOTH) {
navigate("/booth");
}
}, [user]);

if (!user || !transactionDetails) {
return <></>;
}

return (
<Container maxWidth="sm">
<Header />
<Stack
direction="column"
spacing={3}
sx={{
justifyContent: "center",
alignItems: "center",
}}
>
<Typography variant="body1">Transaction History</Typography>
{transactionDetails.length > 0 ? (
transactionDetails.map((transaction, idx) => (
<Card variant="outlined" key={idx}>
<CardContent>
<Typography variant="h6">
{new Decimal(transaction.amount).toFixed(2)}
</Typography>
<Typography variant="body1">To {transaction.name}</Typography>
<Typography variant="body1">
Timestamp: {transaction.completed_timestamp.toLocaleString()}
</Typography>
</CardContent>
</Card>
))
) : (
<Typography variant="body2">No transactions yet</Typography>
)}
</Stack>
</Container>
);
}
8 changes: 8 additions & 0 deletions frontend/src/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ const theme = createTheme({
color: "#30d4c9", // find a way to fix this
},
},
{
props: { variant: "body2" },
style: {
fontSize: "1.2rem",
textAlign: "center",
fontStyle: "italic",
},
},
{
props: { variant: "h3" },
style: {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types/topup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export type TopupDetails = {
student_uid: string;
student_name: string;
};

export type TopupHistoryDetails = {
lucky_draw_code: string;
amount: string;
};
Loading

0 comments on commit c39e845

Please sign in to comment.