monotax

Colorful dashboard to display financial stats for a side business.

TypeScript
React
Node
SQLite

Overview

Quick app to display a client’s earnings, expenses, savings and invoices in material design, including a PDF invoice parser.

Features:

  • Views for all-time, quarterly, yearly and monthly stats
  • Views for earnings, expenses, savings and invoices
  • CLI utility for quick PDF-to-SQLite conversion
  • Colorful animated charts and tabbed tables
  • Expanding panels, pagination and sorting

Screenshots

Operation

PDF invoices are placed at data/pdf and parsed with npm run parse. The invoices must be formatted as those issued under the monotax scheme of Argentina’s Federal Administration of Public Revenue.

Every PDF is converted to a JSON object and is persisted in the database at data/sql/monotax.db. The db is pre-populated with earnings, expenses and ARS-USD exchange rate data, queriable through 35+ SQL views, many of which are used by the backend. A demo db and its full schema are included.

PDF invoice sample:

JSON object sample:

{
	date: "Mon Dec 23 2019 12:41:30 GMT-0300 (Argentina Standard Time)",
	id: "692",
	name: "Lorem Ipsum",
	address: {
		street: "Dolor Sit",
		province: "Amet",
	}
	client_id: {
		type: "DNI",
		number: "12934612",
	}
	vatStatus: "Consumidor Final",
	amount: 1900.00
}

The frontend relies on React Admin to auto-fetch data for views corresponding to earnings, expenses, savings and invoices. The client also makes requests custom endpoints to fetch data for the global overview and the yearly stats views.

// setAutomaticRoutes.ts → four React Admin endpoints
const setAutomaticRoutes = (app: express.Application) => {
	const autoRoutes = ["invoices", "earnings", "expenses", "savings"];
	autoRoutes.forEach(route =>
		app.get(
			"/api/" + route,
			(request: express.Request, response: express.Response) =>
				createAutomaticRoute(request, response, route)
		)
	);
};

const createAutomaticRoute = (
	request: express.Request,
	response: express.Response,
	route: string
) => {
	const { offset, limit } = request.query;
	const [field, order] = JSON.parse(request.query.sort);
	const results = DatabaseService.getAllRecords(
		VIEWS[route],
		offset,
		limit,
		field,
		order
	);
	const total = DatabaseService.getCount(VIEWS[route]);
	return response
		.status(200)
		.set("Content-Range", route + "0-15/" + total)
		.set("Access-Control-Expose-Headers", "Content-Range")
		.json(results);
};
// dataProvider.ts → four API calls
getList: (resource: string, params: GetListParams) => {
	const { page, perPage } = params.pagination;
	const { field, order } = params.sort;

	const query = {
		filter: JSON.stringify(params.filter),
		offset: JSON.stringify((page - 1) * perPage),
		limit: JSON.stringify(perPage),
		sort: JSON.stringify([field, order])
	};
	const url = `${apiUrl}/${resource}?${stringify(query)}`;

	return httpClient(url).then(({ json, headers }: any) => {
		return {
			data: json,
			total: parseInt(
				headers
					.get("Content-Range")
					.split("/")
					.pop(),
				10
			)
		};
	});
};
// setCustomRoutes.ts → four custom endpoints
const setCustomRoutes = (app: express.Application) => {
	app.get("/api/overview/", (request, response) => {
		const quarterlyData = DatabaseService.getQuarterlyData();
		const allTimeTotals = DatabaseService.getAllTimeTotals();
		const yearlyTotals = DatabaseService.getYearlyTotals();
		const monthlyAveragesPerYear = DatabaseService.getMonthlyAveragesPerYear();
		const lastSixMonthsValues = DatabaseService.getLastSixMonthsValues();

		response.status(200).json({
			quarterlyData,
			allTimeTotals,
			yearlyTotals,
			monthlyAveragesPerYear,
			lastSixMonthsValues
		});
	});

	const years = ["2017", "2018", "2019"];
	years.forEach(year =>
		app.get("/api/year/" + year, (request, response) => {
			const yearData = DatabaseService.getYearData(year);
			const totalsForYear = calculateTotals(yearData);

			response.status(200).json({
				earningsForYear: yearData["earnings"],
				expensesForYear: yearData["expenses"],
				savingsForYear: yearData["savings"],
				invoicedForYear: yearData["invoiced"],
				totalsForYear
			});
		})
	);
};

The dashboard borrows from Material UI icons and Material React components, to display data in colorful boxes, animated charts and tabbed tables. Material React components must be placed at client/src/mdr and are not included in this repo.

    

A dev proxy is set up so that any request from the client at http://localhost:3000/ made to an /api/ endpoint is redirected to the server at http://localhost:5000/. See the CRA docs on API request proxying.

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function(app) {
	app.use(
		"/api",
		createProxyMiddleware({
			target: "http://localhost:5000",
			changeOrigin: true
		})
	);
};

Author

© 2020 Iván Ovejero

License

Distributed under the MIT License. See LICENSE.md