Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature invoice #22

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ justify-content: center;
.Component-kanban-card-row{
margin-top: 32px;
margin-left: 10px;
margin-right: 10px;
}
.Component-upload-progress{
margin-top: 5px;
Expand Down
8 changes: 4 additions & 4 deletions src/auth/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {app} from "../config";
// (user: User): void;
// }

const provider = new GoogleAuthProvider();
export const provider = new GoogleAuthProvider();
provider.addScope("https://www.googleapis.com/auth/drive");
provider.addScope("https://www.googleapis.com/auth/drive.file");
provider.addScope("https://www.googleapis.com/auth/drive.resource");
Expand All @@ -20,8 +20,8 @@ const auth = getAuth(app);
export async function googleSignIn(): Promise<User> {
const result: UserCredential = await signInWithPopup(auth, provider);
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential?.accessToken;

// const token = credential?.accessToken;
if (isEmailValid(result.user.email)) {
console.log(result.user);
} else {
Expand All @@ -35,7 +35,7 @@ export async function googleSignIn(): Promise<User> {
isAuthorizer: false,
isTreasurer: false,
photoURL: result.user.photoURL ?? "",
token: token ?? "",
// token: token ?? "",
};
}

Expand Down
42 changes: 35 additions & 7 deletions src/auth/session.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
import { getAuth, onAuthStateChanged, signOut } from "firebase/auth";
import {
getAuth,
GoogleAuthProvider,
reauthenticateWithPopup,
onAuthStateChanged,
signOut,
} from "firebase/auth";
import {app} from "../config";
import {User} from "./types";
import {isEmailValid, provider} from "./google";

/**
* Retrieves the google access token by signing in the user again
*/
export async function retrieveToken() {
const auth = getAuth(app);
if (auth.currentUser == null) {
throw new Error("user not signed in");
}

const result = await reauthenticateWithPopup(auth.currentUser, provider);
if (result == null || !isEmailValid(result.user.email)) {
throw new Error("could not authenticate")
}

const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential?.accessToken;
if (token == null) {
throw new Error("no access token");
}

return token;
}

/**
* Try firebase relogin on a new page refresh by returning the user object if successful
*/
export async function retainSession(): Promise<User> {
return new Promise((resolve, reject) => {
const auth = getAuth(app);
onAuthStateChanged(auth, async (user) => {
const unsub = onAuthStateChanged(auth, async (user) => {
if (user) {
// gets user id token
const token = await user.getIdToken();

// create user
const result = {
id: user.uid,
name: user.displayName ?? "",
email: user.email ?? "",
isAuthorizer: false,
isTreasurer: false,
photoURL: user.photoURL ?? "",
token: token ?? "",
photoURL: user.photoURL ?? ""
};
unsub();
resolve(result);
} else {
unsub();
reject();
}
});
Expand Down
103 changes: 101 additions & 2 deletions src/components/AnalyticChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {useEffect, useRef} from "react";
import {Chart, Colors, PieController, ArcElement, Legend, Title, Tooltip} from "chart.js";
import {useEffect, useRef, useState} from "react";
import {Chart, Colors, BarElement, BarController, PieController, ArcElement, LinearScale, CategoryScale, Legend, Title, Tooltip} from "chart.js";
import {GetInvoicesByTime, InvoiceSchema} from "../database/invoice";
import {Finance} from "../database/analytics";

Chart.register(PieController);
Chart.register(ArcElement);
Expand All @@ -8,6 +10,11 @@ Chart.register(Title);
Chart.register(Tooltip);
Chart.register(Colors);

Chart.register(BarController);
Chart.register(BarElement);
Chart.register(CategoryScale);
Chart.register(LinearScale);

interface PieChartProps {
departmentCosts: Record<string, string>
}
Expand Down Expand Up @@ -81,4 +88,96 @@ export function PieChart(
return <div>
<canvas ref={canvas}></canvas>
</div>;
}


export function BarChart() {
const chart = useRef<Chart<any, any[], any> | null>(null);
const canvas = useRef<any | null>(null);
const [invoices, setInvoices] = useState<InvoiceSchema[]>([]);

// Create a chart instance with options
const createChart = (canvas: any) => {
const data = {
labels: [],
datasets: [{
label: 'Cashflow',
data: [],
}]
};
return new Chart(
canvas,
{
type: 'bar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {},
title: {
display: true,
text: 'Monthly Cashflows',
font: {
size: 24,
}
},
colors: {
// automatic coloring of the slices
forceOverride: true
}
}
}
}
);
};

// Loading chart on mount
useEffect(() => {
chart.current = createChart(canvas.current);
return () => {
if (chart.current != null) {
chart.current.destroy();
}
};
}, []);

// Updating chart on department cost update
useEffect(() => {
(async () => {
// fetch invoices
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
const result = await GetInvoicesByTime(date);
setInvoices(result);
})();
}, []);

useEffect(() => {
if (chart.current == null) {
return;
}
let cashflow: Record<string, string> = {};
for (const invoice of invoices) {
let total = "0.00";
for (const {amount} of invoice.items) {
total = Finance.addPrice(total, amount);
}

const month = invoice.timestamp.toLocaleString('default', {month: 'long'});
if (!Object.hasOwn(cashflow, month)) {
cashflow[month] = total;
} else {
cashflow[month] = Finance.addPrice(cashflow[month], total);
}
}

chart.current.data.labels = Object.keys(cashflow);
chart.current.data.datasets[0].data = Object.values(cashflow);
chart.current.update();
}, [invoices]);

return <div style={{minHeight: 400, marginTop: "5rem"}}>
<canvas ref={canvas}></canvas>
</div>;
}
39 changes: 39 additions & 0 deletions src/components/Card/InvoiceButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import { InvoiceSchema, StatusEnum, UpdateInvoice } from '../../database/invoice';

type InvoiceButtonProps = {
invoice: InvoiceSchema,
status: StatusEnum
}

export default function InvoiceButton({
invoice,
status
}: InvoiceButtonProps) {

let color = "red";

if (status === "Approve"){
color = "green";
}


return (
<React.Fragment>
<Button variant="contained"
size="small"
style={{
margin: "10px",
backgroundColor: color
}}
onClick={() => {
UpdateInvoice(invoice.invoice_id, {status: status});
}}>

<b>{status}</b>

</Button>
</React.Fragment>
);
}
84 changes: 84 additions & 0 deletions src/components/Card/InvoiceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Card from "@mui/joy/Card";
import CardContent from "@mui/joy/CardContent";
import Typography from "@mui/joy/Typography";

import { KanbanCardProps } from "./KanbanCardProps";

import { Box, List, ListItem } from "@mui/material";
import { InvoiceSchema } from "../../database/invoice";
import InvoiceButton from "./InvoiceButton";

export default function InvoiceCard({
info,
isTreasurer,
isAuthorizer
}: KanbanCardProps){

const invoiceInfo = info as InvoiceSchema;


const handleClick = () => {
if (invoiceInfo.driveUrl !== ""){
window.open(invoiceInfo.driveUrl, '_blank');
}
}

// console.log(invoiceInfo.docId);

return (
<Card className="Component-expense-card-container" sx={{backgroundColor: "black"}}>

{isTreasurer?
<div>
<InvoiceButton status="Approve" invoice={invoiceInfo}/>
<InvoiceButton status="Reject" invoice={invoiceInfo}/>
</div>
: <></>}

<CardContent sx={{justifyContent: "flex-end", cursor: 'pointer'}}
onClick={handleClick}>
<Typography level="h2" fontSize="lg" textColor="#fff" mb={1}>
ID: {invoiceInfo.invoice_id}
</Typography>
<Box
style={{
flexDirection: "row",
}}
>
<Typography
textColor="neutral.300"
>
Timestamp: {invoiceInfo.timestamp.toLocaleString()}
</Typography>
<Typography
textColor="neutral.300"
>
Recipient: {invoiceInfo.recipient}
</Typography>
<Typography
textColor="neutral.300"
>
ABN: {invoiceInfo.recipient_abn}
</Typography>
<Typography
textColor="neutral.300"
>
Address: {invoiceInfo.recipient_address}
</Typography>
<div>
<Typography level="h2" fontSize="lg" textColor="#fff" mb={1}>Items</Typography>
<List>
{invoiceInfo.items.map((item, i) => (
<ListItem key={i}>
<Typography textColor="neutral.300">
{item.description} | {item.amount}
</Typography>
</ListItem>
))}
</List>
</div>
</Box>
</CardContent>
</Card>
);
}
5 changes: 5 additions & 0 deletions src/components/Card/KanbanCardProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type KanbanCardProps = {
info: any,
isTreasurer: boolean,
isAuthorizer?: boolean
};
Loading