Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
276f50ddd7 | |||
489b05886b | |||
cafc808395 | |||
f6f9819058 |
BIN
data/database.sqlite
Normal file
BIN
data/database.sqlite
Normal file
Binary file not shown.
0
data/dummy
Normal file
0
data/dummy
Normal file
110
less/app.less
Normal file
110
less/app.less
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
@dark: #222f3e;
|
||||||
|
@light-grey: #576574;
|
||||||
|
@light: #c8d6e5;
|
||||||
|
|
||||||
|
@green: #1dd1a1;
|
||||||
|
@yellow: #feca57;
|
||||||
|
@red: #ff6b6b;
|
||||||
|
@pink: #ff9ff3;
|
||||||
|
@purple: #5f27cd;
|
||||||
|
@cyan: #48dbfb;
|
||||||
|
@blue: #54a0ff;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--dark: @dark;
|
||||||
|
--light-grey: @light-grey;
|
||||||
|
--light: @light;
|
||||||
|
|
||||||
|
--green: @green;
|
||||||
|
--yellow: @yellow;
|
||||||
|
--red: @red;
|
||||||
|
--pink: @pink;
|
||||||
|
--purple: @purple;
|
||||||
|
--cyan: @cyan;
|
||||||
|
--blue: @blue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'robotomono-light';
|
||||||
|
src: url('/assets/font/RobotoMono-Light.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "robotomono-light";
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--dark);
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--cyan);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group.half {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group .text {
|
||||||
|
width: 200px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group .input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group .input input,
|
||||||
|
form .form-group .input textarea,
|
||||||
|
form .form-group .input select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .form-group {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"],input[type="time"],input[type="date"],select{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
input[type="number"],input[type="time"],input[type="date"],select,input[type="text"],textarea{
|
||||||
|
background:@light-grey;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "table.less";
|
||||||
|
@import "responsive.less";
|
16
less/responsive.less
Normal file
16
less/responsive.less
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@media (max-width: 1250px) {
|
||||||
|
|
||||||
|
.nav,
|
||||||
|
.container {
|
||||||
|
width: 95vw;
|
||||||
|
margin: auto;
|
||||||
|
padding: 1vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
form .form-group.half{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
74
less/table.less
Normal file
74
less/table.less
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.amount {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.income {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.expense {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.transfer {
|
||||||
|
color: var(--pink);
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.amount:after {
|
||||||
|
content: " €";
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.middle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.right {
|
||||||
|
text-align: right
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(3n+2) {
|
||||||
|
background-color: lighten(@dark, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(3n+3) {
|
||||||
|
background-color: lighten(@dark, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr{
|
||||||
|
td:nth-child(1){
|
||||||
|
border-left:solid transparent 5px;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.fullList{
|
||||||
|
tr.transfer:nth-child(2n+1) {
|
||||||
|
td:nth-child(1) {
|
||||||
|
border-top: 1px solid @pink;
|
||||||
|
border-left: 5px solid @pink;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
border-top: 1px solid @pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.transfer:nth-child(2n+2) {
|
||||||
|
td:nth-child(1) {
|
||||||
|
border-left: 5px solid @pink;
|
||||||
|
border-bottom: 1px solid @pink;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
border-bottom: 1px solid @pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2609
package-lock.json
generated
Normal file
2609
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "smol-budget-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "- database",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "rm -rf ./build && tsc",
|
||||||
|
"start": "npm run build && lessc less/app.less public/css/app.css && node build/index.js",
|
||||||
|
"lessc":"lessc less/app.less public/css/app.css"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"mariadb": "^3.2.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"pug": "^3.0.2",
|
||||||
|
"sequelize": "^6.35.2",
|
||||||
|
"sqlite3": "^5.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.10.4",
|
||||||
|
"@types/pug": "^2.0.10",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
151
public/css/app.css
Normal file
151
public/css/app.css
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
:root {
|
||||||
|
--dark: #222f3e;
|
||||||
|
--light-grey: #576574;
|
||||||
|
--light: #c8d6e5;
|
||||||
|
--green: #1dd1a1;
|
||||||
|
--yellow: #feca57;
|
||||||
|
--red: #ff6b6b;
|
||||||
|
--pink: #ff9ff3;
|
||||||
|
--purple: #5f27cd;
|
||||||
|
--cyan: #48dbfb;
|
||||||
|
--blue: #54a0ff;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'robotomono-light';
|
||||||
|
src: url('/assets/font/RobotoMono-Light.ttf');
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
font-family: "robotomono-light";
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: var(--dark);
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--cyan);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
form .form-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
form .form-group.half {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
form .form-group .text {
|
||||||
|
width: 200px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
form .form-group .input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
form .form-group .input input,
|
||||||
|
form .form-group .input textarea,
|
||||||
|
form .form-group .input select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
form .form-group {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
input[type="number"],
|
||||||
|
input[type="time"],
|
||||||
|
input[type="date"],
|
||||||
|
select {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
input[type="number"],
|
||||||
|
input[type="time"],
|
||||||
|
input[type="date"],
|
||||||
|
select,
|
||||||
|
input[type="text"],
|
||||||
|
textarea {
|
||||||
|
background: #576574;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
table td.amount {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
table td.income {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
table td.expense {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
table td.transfer {
|
||||||
|
color: var(--pink);
|
||||||
|
}
|
||||||
|
table td.amount:after {
|
||||||
|
content: " €";
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th.middle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
th.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
tr:nth-child(3n+2) {
|
||||||
|
background-color: #2b3b4e;
|
||||||
|
}
|
||||||
|
tr:nth-child(3n+3) {
|
||||||
|
background-color: #34485f;
|
||||||
|
}
|
||||||
|
tr td:nth-child(1) {
|
||||||
|
border-left: solid transparent 5px;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
table.fullList tr.transfer:nth-child(2n+1) td:nth-child(1) {
|
||||||
|
border-top: 1px solid #ff9ff3;
|
||||||
|
border-left: 5px solid #ff9ff3;
|
||||||
|
}
|
||||||
|
table.fullList tr.transfer:nth-child(2n+1) td:nth-child(2) {
|
||||||
|
border-top: 1px solid #ff9ff3;
|
||||||
|
}
|
||||||
|
table.fullList tr.transfer:nth-child(2n+2) td:nth-child(1) {
|
||||||
|
border-left: 5px solid #ff9ff3;
|
||||||
|
border-bottom: 1px solid #ff9ff3;
|
||||||
|
}
|
||||||
|
table.fullList tr.transfer:nth-child(2n+2) td:nth-child(2) {
|
||||||
|
border-bottom: 1px solid #ff9ff3;
|
||||||
|
}
|
||||||
|
@media (max-width: 1250px) {
|
||||||
|
.nav,
|
||||||
|
.container {
|
||||||
|
width: 95vw;
|
||||||
|
margin: auto;
|
||||||
|
padding: 1vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
form .form-group.half {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
0
public/font/dummy
Normal file
0
public/font/dummy
Normal file
14
src/controller/helper/dateConversion.ts
Normal file
14
src/controller/helper/dateConversion.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
import { Transaction } from "../models";
|
||||||
|
|
||||||
|
export function convertDateArray(arr1:any):any{
|
||||||
|
let arr = [];
|
||||||
|
for (let index = 0; index < arr1.length; index++) {
|
||||||
|
let element:Transaction = arr1[index];
|
||||||
|
let e:any = element;
|
||||||
|
e.Date2 = moment(element.Date).format("YYYY-MM-DD HH:MM")
|
||||||
|
console.log(moment(element.Date).format("YYYY-MM-DD HH:MM"))
|
||||||
|
arr[index] = e;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
7
src/controller/helper/getFirstLastDayOfMonth.ts
Normal file
7
src/controller/helper/getFirstLastDayOfMonth.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export const getFirstLastDayOfMonth = (date: moment.Moment) => {
|
||||||
|
let startDate = moment(date).startOf('month');
|
||||||
|
let endDate = moment(date).endOf('month');
|
||||||
|
return { startDate, endDate }
|
||||||
|
}
|
1
src/controller/helper/index.ts
Normal file
1
src/controller/helper/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { getFirstLastDayOfMonth } from "./getFirstLastDayOfMonth";
|
53
src/controller/models/Account.ts
Normal file
53
src/controller/models/Account.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Account model definition
|
||||||
|
import { Sequelize, DataTypes, Model, Optional, Attributes, CreateOptions, ModelStatic } from 'sequelize';
|
||||||
|
import { HookReturn } from 'sequelize/types/hooks';
|
||||||
|
|
||||||
|
import { Transaction } from './Transaction'; // Import the Transaction model
|
||||||
|
|
||||||
|
interface AccountAttributes {
|
||||||
|
AccountID: number;
|
||||||
|
Name: string;
|
||||||
|
// Other account fields...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountCreationAttributes extends Optional<AccountAttributes, 'AccountID'> { }
|
||||||
|
|
||||||
|
class Account extends Model<AccountAttributes, AccountCreationAttributes> implements AccountAttributes {
|
||||||
|
public AccountID!: number;
|
||||||
|
public Name!: string;
|
||||||
|
|
||||||
|
static initialize(sequelize: Sequelize): void {
|
||||||
|
Account.init(
|
||||||
|
{
|
||||||
|
AccountID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
// Other account fields...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'Account',
|
||||||
|
hooks: {
|
||||||
|
afterCreate: (attributes: Account, options: CreateOptions<AccountAttributes>) => {
|
||||||
|
console.log(attributes)
|
||||||
|
},
|
||||||
|
beforeCreate(attributes: Account, options: CreateOptions<AccountAttributes>) {
|
||||||
|
console.log(attributes)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate(models: { Transaction: typeof Transaction }): void {
|
||||||
|
Account.hasMany(models.Transaction, { foreignKey: 'AccountID' }); // An account can have multiple transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Account };
|
54
src/controller/models/Category.ts
Normal file
54
src/controller/models/Category.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Sequelize, DataTypes, Model, Optional } from 'sequelize';
|
||||||
|
import { User } from './User';
|
||||||
|
import { Transaction } from './Transaction';
|
||||||
|
|
||||||
|
// Category attributes interface
|
||||||
|
interface CategoryAttributes {
|
||||||
|
CategoryID: number;
|
||||||
|
Name: string;
|
||||||
|
Description: string | null;
|
||||||
|
// Other fields...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category creation attributes (optional fields)
|
||||||
|
interface CategoryCreationAttributes extends Optional<CategoryAttributes, 'CategoryID'> {}
|
||||||
|
|
||||||
|
// Define the Category model
|
||||||
|
class Category extends Model<CategoryAttributes, CategoryCreationAttributes> implements CategoryAttributes {
|
||||||
|
public CategoryID!: number;
|
||||||
|
public Name!: string;
|
||||||
|
public Description!: string | null;
|
||||||
|
// Other fields...
|
||||||
|
|
||||||
|
static initialize(sequelize: Sequelize): void {
|
||||||
|
Category.init(
|
||||||
|
{
|
||||||
|
CategoryID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Description: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
// Other fields...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'Category',
|
||||||
|
// Other model options...
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static associate(models: { User: typeof User; Transaction: typeof Transaction }): void {
|
||||||
|
Category.belongsToMany(models.Transaction,{through:"TransactionCategory"}); // A category can have multiple transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {Category };
|
127
src/controller/models/MonthlyReport.ts
Normal file
127
src/controller/models/MonthlyReport.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// MonthlyReport model definition
|
||||||
|
import { Sequelize, DataTypes, Model, Optional, Op, fn, col } from 'sequelize';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Transaction } from './Transaction'; // Assuming you have a Transaction model
|
||||||
|
import { Account } from './Account';
|
||||||
|
import { getFirstLastDayOfMonth } from '../helper';
|
||||||
|
|
||||||
|
interface MonthlyReportAttributes {
|
||||||
|
MonthlyReportID: number;
|
||||||
|
Month: number;
|
||||||
|
Year: number;
|
||||||
|
TotalAmount: number;
|
||||||
|
|
||||||
|
// Other report fields...
|
||||||
|
AccountID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonthlyReportCreationAttributes extends Optional<MonthlyReportAttributes, 'MonthlyReportID'> { }
|
||||||
|
|
||||||
|
class MonthlyReport extends Model<MonthlyReportAttributes, MonthlyReportCreationAttributes> implements MonthlyReportAttributes {
|
||||||
|
public MonthlyReportID!: number;
|
||||||
|
public Month!: number;
|
||||||
|
public Year!: number;
|
||||||
|
public TotalAmount!: number;
|
||||||
|
public AccountID!: number;
|
||||||
|
|
||||||
|
static initialize(sequelize: Sequelize): void {
|
||||||
|
MonthlyReport.init(
|
||||||
|
{
|
||||||
|
MonthlyReportID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
Month: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Year: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
TotalAmount: {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
AccountID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
}
|
||||||
|
// Other report fields...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'MonthlyReport',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate(models: { Transaction: typeof Transaction, Account: typeof Account }): void {
|
||||||
|
MonthlyReport.hasMany(models.Transaction, { foreignKey: 'MonthlyReportID' }); // A report can have multiple transactions
|
||||||
|
MonthlyReport.belongsTo(models.Account)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generateMonthlyReport(month: number, year: number, AccountID: number|null = null): Promise<void> {
|
||||||
|
try {
|
||||||
|
moment.locale("de")
|
||||||
|
|
||||||
|
|
||||||
|
let m = moment().set({year:year,month:month-1,date:15});
|
||||||
|
let { startDate, endDate } = getFirstLastDayOfMonth(m);
|
||||||
|
|
||||||
|
// Fetch transaction data and calculate total amount
|
||||||
|
let transactions = null;
|
||||||
|
if(AccountID==null){
|
||||||
|
transactions = await Transaction.findAll({
|
||||||
|
where: {
|
||||||
|
Date: {
|
||||||
|
[Op.between]: [startDate, endDate],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
transactions = await Transaction.findAll({
|
||||||
|
where: {
|
||||||
|
Date: {
|
||||||
|
[Op.between]: [startDate, endDate],
|
||||||
|
},
|
||||||
|
AccountID: AccountID
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(transactions)
|
||||||
|
|
||||||
|
|
||||||
|
for(let i=0;i<transactions.length;i++){
|
||||||
|
await Transaction.update({
|
||||||
|
Locked:true
|
||||||
|
},{
|
||||||
|
where:{
|
||||||
|
TransactionID:transactions[i].TransactionID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = transactions.reduce((total, transaction) => total + transaction.Amount, 0);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(totalAmount)
|
||||||
|
// Create a MonthlyReport entry with the generated data
|
||||||
|
await MonthlyReport.create({
|
||||||
|
Month: month,
|
||||||
|
Year: year,
|
||||||
|
TotalAmount: totalAmount,
|
||||||
|
AccountID: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Monthly report generated for ${month}/${year}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating monthly report:', err);
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MonthlyReport };
|
154
src/controller/models/Transaction.ts
Normal file
154
src/controller/models/Transaction.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { Sequelize, DataTypes, Model, Optional, Attributes, CreateOptions, ModelStatic } from 'sequelize';
|
||||||
|
import { User } from './User';
|
||||||
|
import { Category } from './Category';
|
||||||
|
import { Account } from './Account';
|
||||||
|
import { HookReturn } from 'sequelize/types/hooks';
|
||||||
|
import { MonthlyReport } from './MonthlyReport';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Transaction attributes interface
|
||||||
|
interface TransactionAttributes {
|
||||||
|
TransactionID: number;
|
||||||
|
Date: Date;
|
||||||
|
Description: string | null;
|
||||||
|
UserID: number;
|
||||||
|
Amount: number;
|
||||||
|
Type: 'income' | 'expense' | 'transfer';
|
||||||
|
AccountID: number;
|
||||||
|
Refound:boolean,
|
||||||
|
Locked: boolean
|
||||||
|
// Other fields...
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction creation attributes (optional fields)
|
||||||
|
interface TransactionCreationAttributes extends Optional<TransactionAttributes, 'TransactionID'> { }
|
||||||
|
|
||||||
|
// Define the Transaction model
|
||||||
|
class Transaction extends Model<TransactionAttributes, TransactionCreationAttributes> implements TransactionAttributes {
|
||||||
|
public TransactionID!: number;
|
||||||
|
public Date!: Date;
|
||||||
|
public Description!: string | null;
|
||||||
|
public Amount!: number;
|
||||||
|
public Type!: 'income' | 'expense' | 'transfer';
|
||||||
|
public Refound!: boolean;
|
||||||
|
public Locked!:boolean;
|
||||||
|
// Other fields...
|
||||||
|
|
||||||
|
public UserID!: number;
|
||||||
|
public AccountID!: number;
|
||||||
|
public CategoryID!: number;
|
||||||
|
|
||||||
|
static initialize(sequelize: Sequelize): void {
|
||||||
|
Transaction.init(
|
||||||
|
{
|
||||||
|
TransactionID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
Date: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: new Date()
|
||||||
|
},
|
||||||
|
Description: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
UserID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
AccountID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
Amount: {
|
||||||
|
type: DataTypes.FLOAT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Type: {
|
||||||
|
type: DataTypes.ENUM('income', 'expense','transfer'),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Refound:{
|
||||||
|
type:DataTypes.BOOLEAN,
|
||||||
|
defaultValue:false
|
||||||
|
},
|
||||||
|
Locked:{
|
||||||
|
type:DataTypes.BOOLEAN,
|
||||||
|
defaultValue:false
|
||||||
|
}
|
||||||
|
// Other fields...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'Transaction',
|
||||||
|
// Other model options...
|
||||||
|
hooks: {
|
||||||
|
afterCreate: (attributes: Transaction, options: CreateOptions<TransactionAttributes>) => {
|
||||||
|
console.log(attributes)
|
||||||
|
},
|
||||||
|
beforeCreate(attributes: Transaction, options: CreateOptions<TransactionAttributes>) {
|
||||||
|
if (attributes.Type == "expense") {
|
||||||
|
attributes.Amount = -0 - attributes.Amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static associate(models: { User: typeof User; Category: typeof Category; Account: typeof Account; MonthlyReport: typeof MonthlyReport }): void {
|
||||||
|
Transaction.belongsTo(models.User); // A transaction belongs to a user
|
||||||
|
Transaction.belongsTo(models.Category, { foreignKey: 'CategoryID' }); // A transaction belongs to a category
|
||||||
|
Transaction.belongsTo(models.Account, { foreignKey: 'AccountID' }); // A transaction belongs to an account
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTotalAmountForUser(userId: number): Promise<number> {
|
||||||
|
const result = await Transaction.sum('Amount', { where: { UserID: userId } });
|
||||||
|
return result || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async transferAmountFromTo(userID: number, fromAccountID: number = 1, toAccountID: number = 1, Amount: number = 0,CategoryID:number = 1){
|
||||||
|
console.log(`User: ${userID}, fromAccount: ${fromAccountID}, toAccount${toAccountID}, Amount: ${Amount}`)
|
||||||
|
await Transaction.create({
|
||||||
|
Amount:-Amount,
|
||||||
|
Date:new Date(),
|
||||||
|
Type:"transfer",
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: userID, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: CategoryID,
|
||||||
|
AccountID: fromAccountID
|
||||||
|
})
|
||||||
|
await Transaction.create({
|
||||||
|
Amount:Amount,
|
||||||
|
Date:new Date(),
|
||||||
|
Type:"transfer",
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: userID, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: CategoryID,
|
||||||
|
AccountID: toAccountID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async changeTransaction(id:number,amount:number,Description:string, CategoryID:number, AccountID:number){
|
||||||
|
let foundTransaction = await Transaction.findOne({where:{
|
||||||
|
TransactionID:id
|
||||||
|
}})
|
||||||
|
if(foundTransaction!=null){
|
||||||
|
if(foundTransaction.Locked == false){
|
||||||
|
|
||||||
|
}else{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { Transaction };
|
62
src/controller/models/User.ts
Normal file
62
src/controller/models/User.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Sequelize, DataTypes, Model, Optional } from 'sequelize';
|
||||||
|
import { Category } from './Category';
|
||||||
|
import { Transaction } from './Transaction';
|
||||||
|
|
||||||
|
// User attributes interface
|
||||||
|
interface UserAttributes {
|
||||||
|
UserID: number;
|
||||||
|
Name: string;
|
||||||
|
Email: string;
|
||||||
|
Password: string;
|
||||||
|
// Other fields...
|
||||||
|
}
|
||||||
|
|
||||||
|
// User creation attributes (optional fields)
|
||||||
|
interface UserCreationAttributes extends Optional<UserAttributes, 'UserID'> { }
|
||||||
|
|
||||||
|
// Define the User model
|
||||||
|
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
||||||
|
public UserID!: number;
|
||||||
|
public Name!: string;
|
||||||
|
public Email!: string;
|
||||||
|
public Password!: string;
|
||||||
|
|
||||||
|
// Other fields...
|
||||||
|
|
||||||
|
static initialize(sequelize: Sequelize): void {
|
||||||
|
User.init(
|
||||||
|
{
|
||||||
|
UserID: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
Email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
// Other fields...
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'User',
|
||||||
|
// Other model options...
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static associate(models: { Category: typeof Category; Transaction: typeof Transaction }): void {
|
||||||
|
User.hasMany(models.Transaction, { foreignKey: 'UserID' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { User };
|
4
src/controller/models/index.ts
Normal file
4
src/controller/models/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { Category } from "./Category";
|
||||||
|
export { Transaction } from "./Transaction";
|
||||||
|
export { User } from "./User";
|
||||||
|
export { Account } from "./Account";
|
112
src/controller/router.ts
Normal file
112
src/controller/router.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// server.ts
|
||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { User, Category, Transaction, Account } from './models'; // Adjust the path to your models
|
||||||
|
import pug from "pug"
|
||||||
|
import path from 'path';
|
||||||
|
import { TransactionRouter } from './router/transactions';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import { CategoryRouter } from './router/categories';
|
||||||
|
|
||||||
|
export class Router {
|
||||||
|
app: any;
|
||||||
|
constructor(Path = "", PORT = 3000) {
|
||||||
|
let app = express();
|
||||||
|
|
||||||
|
// Set the view engine to Pug
|
||||||
|
app.set('view engine', 'pug');
|
||||||
|
app.set('views', './views'); // Set the views directory
|
||||||
|
console.log(Path)
|
||||||
|
// parse application/x-www-form-urlencoded
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }))
|
||||||
|
|
||||||
|
// parse application/json
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use('/assets', express.static(Path));
|
||||||
|
|
||||||
|
app.get('/', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const users = await User.findAll();
|
||||||
|
const categories = await Category.findAll();
|
||||||
|
const transactions = await Transaction.findAll();
|
||||||
|
|
||||||
|
res.render('index', {
|
||||||
|
title: 'Home',
|
||||||
|
userCount: users.length,
|
||||||
|
categoryCount: categories.length,
|
||||||
|
transactionCount: transactions.length,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let transactionR = TransactionRouter.routes();
|
||||||
|
app.use('/transactions', transactionR)
|
||||||
|
app.get('/users', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const users = await User.findAll();
|
||||||
|
res.render('users', { title: 'Users', users });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let categoriesR = CategoryRouter.routes();
|
||||||
|
app.use('/categories', categoriesR)
|
||||||
|
/*
|
||||||
|
app.get('/transactions', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const transactions = await Transaction.findAll({
|
||||||
|
include: Account
|
||||||
|
});
|
||||||
|
const categories = await Category.findAll({});
|
||||||
|
const accounts = await Account.findAll({});
|
||||||
|
console.log(transactions)
|
||||||
|
res.render('transactions/transactions', { title: 'Transactions', transactions, categories, accounts });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get('/transactions/acc/:acc', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const transactions = await Transaction.findAll({
|
||||||
|
include: Account,
|
||||||
|
where: {
|
||||||
|
AccountID: req.params.acc
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(transactions)
|
||||||
|
res.render('transactions/transactions', { title: 'Transactions', transactions });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.get('/transactions/cat/:cat', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const transactions = await Transaction.findAll({
|
||||||
|
include: [Account, Category],
|
||||||
|
where: {
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: req.params.cat
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(transactions)
|
||||||
|
res.render('transactions/transactions', { title: 'Transactions', transactions });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
0
src/controller/router/accounts.ts
Normal file
0
src/controller/router/accounts.ts
Normal file
71
src/controller/router/categories.ts
Normal file
71
src/controller/router/categories.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { Account, Category, Transaction } from "../models";
|
||||||
|
import moment from "moment";
|
||||||
|
import { convertDateArray } from '../helper/dateConversion';
|
||||||
|
|
||||||
|
export class CategoryRouter {
|
||||||
|
static routes() {
|
||||||
|
let router = express.Router();
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
console.log("TEST")
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
router.post('/addCategory',async(req,res)=>{
|
||||||
|
let name = req.body.categoryName;
|
||||||
|
let description = req.body.categoryDescription;
|
||||||
|
await Category.create({
|
||||||
|
Name:name,
|
||||||
|
Description:description
|
||||||
|
})
|
||||||
|
res.redirect("/categories");
|
||||||
|
//res.render('categories/listCategories', { title: 'Categories', categories });
|
||||||
|
|
||||||
|
})
|
||||||
|
router.get('/addCategory',async(req,res)=>{
|
||||||
|
try {
|
||||||
|
res.render('categories/formCategory', { title: 'Add Categories' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
router.post('/modifyCategory/:id',async(req,res)=>{
|
||||||
|
let name = req.body.categoryName;
|
||||||
|
let description = req.body.categoryDescription;
|
||||||
|
await Category.update({
|
||||||
|
Name:name,
|
||||||
|
Description:description
|
||||||
|
},{
|
||||||
|
where:{
|
||||||
|
CategoryID:req.params.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res.redirect("/categories");
|
||||||
|
//res.render('categories/listCategories', { title: 'Categories', categories });
|
||||||
|
|
||||||
|
})
|
||||||
|
router.get('/modifyCategory/:id',async(req,res)=>{
|
||||||
|
try {
|
||||||
|
let categoryObj = await Category.findOne({where:{
|
||||||
|
CategoryID: req.params.id
|
||||||
|
}})
|
||||||
|
res.render('categories/modifyCategory', { title: 'Add Categories',categoryObj });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
router.all('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const categories = await Category.findAll({});
|
||||||
|
const accounts = await Account.findAll({});
|
||||||
|
res.render('categories/listCategories', { title: 'Categories', categories });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
}
|
0
src/controller/router/reports.ts
Normal file
0
src/controller/router/reports.ts
Normal file
120
src/controller/router/transactions.ts
Normal file
120
src/controller/router/transactions.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import { Account, Category, Transaction } from "../models";
|
||||||
|
import moment from "moment";
|
||||||
|
import { convertDateArray } from '../helper/dateConversion';
|
||||||
|
|
||||||
|
|
||||||
|
import { addTransaction, addTransfer, listTransactions } from './transactions/index';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
export class TransactionRouter {
|
||||||
|
static routes() {
|
||||||
|
let router = express.Router();
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
console.log("TEST")
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/addTransaction', addTransaction.POST)
|
||||||
|
router.get('/addTransaction', addTransaction.GET)
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/addTransfer', addTransfer.POST)
|
||||||
|
router.get('/addTransfer', addTransfer.GET)
|
||||||
|
|
||||||
|
router.get('/modifyTransfer/:id', async (req, res,next ) => {
|
||||||
|
try {
|
||||||
|
await listTransactions.GET(req, res, next, {
|
||||||
|
where: {
|
||||||
|
AccountID: req.params.acc
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/acc/:acc/_/:year/:month',async(req,res,next)=>{
|
||||||
|
try {
|
||||||
|
let m = moment();
|
||||||
|
m.set({
|
||||||
|
year: parseInt(req.params.year),
|
||||||
|
month: parseInt(req.params.month) - 1,
|
||||||
|
date: 1
|
||||||
|
});
|
||||||
|
await listTransactions.GET(req, res, next, {
|
||||||
|
date: m.toDate(),
|
||||||
|
where: {
|
||||||
|
AccountID: req.params.acc
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
router.get('/acc/:acc', async (req: Request, res: Response,next) => {
|
||||||
|
try {
|
||||||
|
await listTransactions.GET(req, res, next, {
|
||||||
|
where: {
|
||||||
|
AccountID: req.params.acc
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.get('/cat/:cat/_/:year/:month', async (req: Request, res: Response, next) => {
|
||||||
|
try {
|
||||||
|
let m = moment();
|
||||||
|
m.set({
|
||||||
|
year: parseInt(req.params.year),
|
||||||
|
month: parseInt(req.params.month) - 1,
|
||||||
|
date: 1
|
||||||
|
});
|
||||||
|
await listTransactions.GET(req, res, next, {
|
||||||
|
date: m.toDate(),
|
||||||
|
where: {
|
||||||
|
CategoryID: req.params.cat
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.get('/cat/:cat', async (req: Request, res: Response, next) => {
|
||||||
|
try {
|
||||||
|
await listTransactions.GET(req, res, next, {
|
||||||
|
where: {
|
||||||
|
CategoryID: req.params.cat
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.get('/_/:year/:month', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
let m = moment();
|
||||||
|
m.set({
|
||||||
|
year: parseInt(req.params.year),
|
||||||
|
month: parseInt(req.params.month) - 1,
|
||||||
|
date: 1
|
||||||
|
});
|
||||||
|
await listTransactions.GET(req, res, next, { date: m.toDate() })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
router.get('/', listTransactions.GET)
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
}
|
43
src/controller/router/transactions/addTransaction.ts
Normal file
43
src/controller/router/transactions/addTransaction.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Request, NextFunction, Response } from "express";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Category, Transaction, Account } from "../../models";
|
||||||
|
|
||||||
|
const POST = async (req: Request, res: Response, next: NextFunction, data = {}) => {
|
||||||
|
try {
|
||||||
|
let date = moment(req.body.date).set({
|
||||||
|
hour: req.body.time.split(":")[0],
|
||||||
|
minute: req.body.time.split(":")[1]
|
||||||
|
})
|
||||||
|
console.log(date.toDate())
|
||||||
|
await Transaction.create({
|
||||||
|
Amount: req.body.amount,
|
||||||
|
Date: date.toDate(),
|
||||||
|
Type: req.body.type,
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: 1, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: req.body.category,
|
||||||
|
AccountID: req.body.account
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
res.redirect("/transactions");
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET = async (req: Request, res: Response, next: NextFunction, data = {}) => {
|
||||||
|
try {
|
||||||
|
const transactions = await Transaction.findAll({
|
||||||
|
include: Account
|
||||||
|
});
|
||||||
|
const categories = await Category.findAll({order:[["Name","ASC"]]});
|
||||||
|
const accounts = await Account.findAll({order:[["Name","ASC"]]});
|
||||||
|
//console.log(transactions)
|
||||||
|
res.render('transactions/formTransaction', { title: 'Transactions', transactions, categories, accounts });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { POST, GET };
|
57
src/controller/router/transactions/addTransfer.ts
Normal file
57
src/controller/router/transactions/addTransfer.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Request, NextFunction, Response } from "express";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Category, Transaction, Account } from "../../models";
|
||||||
|
|
||||||
|
const POST = async (req: Request, res: Response, next: NextFunction, data = {}) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
if(
|
||||||
|
req.body.date == undefined
|
||||||
|
|| req.body.Type == undefined
|
||||||
|
|| req.body.CategoryID == undefined
|
||||||
|
|| req.body.AccountID == undefined
|
||||||
|
){throw "ERROR"}
|
||||||
|
*/
|
||||||
|
let date = moment(req.body.date).set({
|
||||||
|
hour: req.body.time.split(":")[0],
|
||||||
|
minute: req.body.time.split(":")[1]
|
||||||
|
})
|
||||||
|
console.log(date.toDate())
|
||||||
|
await Transaction.transferAmountFromTo(1, req.body.fromAccount, req.body.toAccount, req.body.amount, req.body.categoryID)
|
||||||
|
await Transaction.create({
|
||||||
|
Amount: req.body.amount,
|
||||||
|
Date: date.toDate(),
|
||||||
|
Type: req.body.type,
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: 1, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: req.body.category,
|
||||||
|
AccountID: req.body.account
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
res.redirect("/transactions");
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET = async (req: Request, res: Response, next: NextFunction, data = {}) => {
|
||||||
|
try {
|
||||||
|
const transactions = await Transaction.findAll({
|
||||||
|
include: Account
|
||||||
|
});
|
||||||
|
const categories = await Category.findAll({
|
||||||
|
order: [["Name", "ASC"]]
|
||||||
|
});
|
||||||
|
const accounts = await Account.findAll({
|
||||||
|
order: [["Name", "ASC"]]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.render('transactions/formTransfer', { title: 'Transactions', transactions, categories, accounts });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { POST, GET };
|
5
src/controller/router/transactions/index.ts
Normal file
5
src/controller/router/transactions/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import addTransaction from "./addTransaction";
|
||||||
|
import addTransfer from "./addTransfer";
|
||||||
|
import listTransactions from "./listTransactions";
|
||||||
|
|
||||||
|
export { addTransaction, addTransfer, listTransactions }
|
34
src/controller/router/transactions/listTransactions.ts
Normal file
34
src/controller/router/transactions/listTransactions.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Request, NextFunction, Response } from "express";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Op } from "sequelize";
|
||||||
|
|
||||||
|
import { Category, Transaction, Account } from "../../models";
|
||||||
|
import { convertDateArray } from "../../helper/dateConversion";
|
||||||
|
import { getFirstLastDayOfMonth } from "../../helper";
|
||||||
|
|
||||||
|
const GET = async (req: Request, res: Response, next: NextFunction, data:any = {date:new Date(),where:{}}) => {
|
||||||
|
try {
|
||||||
|
moment.locale("de")
|
||||||
|
let m = moment(data.date);
|
||||||
|
let { startDate, endDate } = getFirstLastDayOfMonth(m);
|
||||||
|
let transactions:any = await Transaction.findAll({
|
||||||
|
include: [Account,Category],
|
||||||
|
where:{
|
||||||
|
Date:{
|
||||||
|
[Op.between]:[startDate.toDate(),endDate.toDate()]
|
||||||
|
},
|
||||||
|
...data.where
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const categories = await Category.findAll({});
|
||||||
|
const accounts = await Account.findAll({});
|
||||||
|
let arr = convertDateArray(transactions);
|
||||||
|
|
||||||
|
res.render('transactions/listTransactions', { title: 'Transactions', transactions: arr, categories, accounts, cssClass:"fullList" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { GET };
|
29
src/controller/router/transactions/modifyTransaction.ts
Normal file
29
src/controller/router/transactions/modifyTransaction.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Request, NextFunction, Response } from "express";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Op } from "sequelize";
|
||||||
|
|
||||||
|
import { Category, Transaction, Account } from "../../models";
|
||||||
|
import { convertDateArray } from "../../helper/dateConversion";
|
||||||
|
import { getFirstLastDayOfMonth } from "../../helper";
|
||||||
|
|
||||||
|
const GET = async (req: Request, res: Response, next: NextFunction, data:any = {date:new Date(),where:{}}) => {
|
||||||
|
try {
|
||||||
|
const transactions:any = await Transaction.findOne({
|
||||||
|
include: [Account,Category],
|
||||||
|
where:{
|
||||||
|
TransactionID: req.params.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const categories = await Category.findAll({});
|
||||||
|
const accounts = await Account.findAll({});
|
||||||
|
|
||||||
|
const arr = convertDateArray(transactions);
|
||||||
|
|
||||||
|
res.render('transactions/modifyTransaction', { title: 'Transactions', transaction: transaction, categories, accounts, cssClass:"fullList" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { GET };
|
5
src/controller/storage.ts
Normal file
5
src/controller/storage.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class Storage{
|
||||||
|
static Path=""
|
||||||
|
static __filename: string;
|
||||||
|
static __dirname: string;
|
||||||
|
}
|
7
src/database.ts
Normal file
7
src/database.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Sequelize, DataTypes, Model, Optional } from 'sequelize';
|
||||||
|
export { Category, Transaction, User, Account } from './controller/models';
|
||||||
|
|
||||||
|
// Set associations between models (similarly as before)
|
||||||
|
|
||||||
|
// Sync models with the database (similarly as before)
|
||||||
|
|
58
src/index.ts
Normal file
58
src/index.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Sequelize, DataTypes, Model, Optional } from 'sequelize';
|
||||||
|
import { Account, Category, Transaction, User } from './controller/models';
|
||||||
|
import { Router } from './controller/router';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { Storage } from './controller/storage';
|
||||||
|
import { MonthlyReport } from './controller/models/MonthlyReport';
|
||||||
|
import { testData } from './test-data';
|
||||||
|
|
||||||
|
|
||||||
|
Storage.__dirname = __dirname
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let init = async () => {
|
||||||
|
// Initialize Sequelize instance and define database connection
|
||||||
|
/*const sequelize = new Sequelize('sqlite::memory:', {
|
||||||
|
host: 'localhost',
|
||||||
|
dialect: 'sqlite', // Change this to your database dialect
|
||||||
|
// Other options...
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage:`${path.join(__dirname,'../data')}/database.sqlite`
|
||||||
|
});
|
||||||
|
|
||||||
|
User.initialize(sequelize);
|
||||||
|
|
||||||
|
Category.initialize(sequelize);
|
||||||
|
|
||||||
|
Transaction.initialize(sequelize);
|
||||||
|
|
||||||
|
|
||||||
|
Account.initialize(sequelize);
|
||||||
|
|
||||||
|
MonthlyReport.initialize(sequelize);
|
||||||
|
// Set up associations between models
|
||||||
|
User.associate({ Transaction, Category });
|
||||||
|
Category.associate({ User, Transaction });
|
||||||
|
Transaction.associate({ User, Category, Account, MonthlyReport });
|
||||||
|
Account.associate({Transaction})
|
||||||
|
|
||||||
|
MonthlyReport.associate({ Transaction, Account });
|
||||||
|
|
||||||
|
await sequelize.sync({ force: true });
|
||||||
|
|
||||||
|
await testData(User,Category,Account,MonthlyReport)
|
||||||
|
|
||||||
|
new Router(path.join(__dirname,'../public'));
|
||||||
|
|
||||||
|
//console.log(path.join(__dirname,'../public'))
|
||||||
|
|
||||||
|
//MonthlyReport.generateMonthlyReport(12,2023,2)
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
100
src/test-data.ts
Normal file
100
src/test-data.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Account, Category, Transaction, User } from "./database"
|
||||||
|
import { MonthlyReport } from "./controller/models/MonthlyReport"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function testData(User:any,Category:any,Account:any,MonthlyReport:any){
|
||||||
|
await User.create({
|
||||||
|
Name: "Max Mustermann",
|
||||||
|
Email: "test@test.de",
|
||||||
|
Password: "sdfsf"
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name: "Allgemein",
|
||||||
|
Description: null
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Server",
|
||||||
|
Description:"Server"
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Games",
|
||||||
|
Description:"Games"
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Server/Netcup",
|
||||||
|
Description:""
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Server/GPortal",
|
||||||
|
Description:""
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Discord",
|
||||||
|
Description:""
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Games/WarThunder"
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Games/Steam"
|
||||||
|
})
|
||||||
|
await Category.create({
|
||||||
|
Name:"Games/League"
|
||||||
|
})
|
||||||
|
await Account.create({
|
||||||
|
Name:"None"
|
||||||
|
})
|
||||||
|
await Account.create({
|
||||||
|
Name:"Bar"
|
||||||
|
})
|
||||||
|
await Account.create({
|
||||||
|
Name:"Girokonto"
|
||||||
|
})
|
||||||
|
|
||||||
|
let z = await Transaction.create({
|
||||||
|
Amount:50.0,
|
||||||
|
Date:new Date(),
|
||||||
|
Type:"expense",
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: 1, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
await Transaction.create({
|
||||||
|
Amount:10.0,
|
||||||
|
Date:new Date(),
|
||||||
|
Type:"expense",
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: 1, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: 1,
|
||||||
|
})
|
||||||
|
await Transaction.create({
|
||||||
|
Amount:60.0,
|
||||||
|
Date:new Date(),
|
||||||
|
Type:"income",
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: 1, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: 1,
|
||||||
|
})
|
||||||
|
await Transaction.create({
|
||||||
|
Amount:60.0,
|
||||||
|
Date:new Date(),
|
||||||
|
Type:"expense",
|
||||||
|
//@ts-ignore
|
||||||
|
UserID: 1, // Associate the transaction with the user
|
||||||
|
//@ts-ignore
|
||||||
|
CategoryID: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Transaction.transferAmountFromTo(1,1,2,50);
|
||||||
|
|
||||||
|
|
||||||
|
let x = await Transaction.getTotalAmountForUser(1)
|
||||||
|
await Transaction.transferAmountFromTo(1,1,2,50);
|
||||||
|
//console.log(x)
|
||||||
|
}
|
109
tsconfig.json
Normal file
109
tsconfig.json
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
"lib": ["es6","ES2023"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
"rootDir": "src", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "build", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
10
views/categories/categories.pug
Normal file
10
views/categories/categories.pug
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
extends ../layout/base.pug
|
||||||
|
|
||||||
|
block nav
|
||||||
|
a(href="/categories") Categories
|
||||||
|
a(href="/categories/addCategory") + Category
|
||||||
|
|
||||||
|
block headline
|
||||||
|
h1 Categories
|
||||||
|
|
||||||
|
block content
|
13
views/categories/formCategory.pug
Normal file
13
views/categories/formCategory.pug
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
extends ./categories.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(action="/categories/addCategory",method="POST")
|
||||||
|
.form-group
|
||||||
|
.text Name
|
||||||
|
.input
|
||||||
|
input(type="text", name="categoryName")
|
||||||
|
.form-group
|
||||||
|
.text Description
|
||||||
|
.input
|
||||||
|
textarea(name="categoryDescription", cols="30", rows="10")
|
||||||
|
button(type="submit") Add Category
|
20
views/categories/listCategories.pug
Normal file
20
views/categories/listCategories.pug
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
extends ./categories.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
table
|
||||||
|
thead
|
||||||
|
th.center ID
|
||||||
|
th Name
|
||||||
|
th Description
|
||||||
|
th Transactions
|
||||||
|
th Modify Category
|
||||||
|
tbody
|
||||||
|
each category in categories
|
||||||
|
tr
|
||||||
|
td=category.CategoryID
|
||||||
|
td=category.Name
|
||||||
|
td=category.Description
|
||||||
|
td
|
||||||
|
a(href=`/transactions/cat/${category.CategoryID}`) Transactions by Category
|
||||||
|
td
|
||||||
|
a(href=`/categories/ModifyCategory/${category.CategoryID}`) ###
|
13
views/categories/modifyCategory.pug
Normal file
13
views/categories/modifyCategory.pug
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
extends ./categories.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(action=`/categories/modifyCategory/${categoryObj.CategoryID}`,method="POST")
|
||||||
|
.form-group
|
||||||
|
.text Name
|
||||||
|
.input
|
||||||
|
input(type="text", name="categoryName",value=categoryObj.Name)
|
||||||
|
.form-group
|
||||||
|
.text Description
|
||||||
|
.input
|
||||||
|
textarea(name="categoryDescription", cols="30", rows="10")=categoryObj.Description
|
||||||
|
button(type="submit") Modify Category
|
23
views/index.pug
Normal file
23
views/index.pug
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
extends layout/base.pug
|
||||||
|
|
||||||
|
block nav
|
||||||
|
a(href="/transactions") Transactions
|
||||||
|
a(href="/transactions/addTransaction") + Transaction
|
||||||
|
a(href="/transactions/addTransfer") + Transfer
|
||||||
|
|
||||||
|
block headline
|
||||||
|
h1 Home
|
||||||
|
|
||||||
|
block content
|
||||||
|
a(href='/users') View Users
|
||||||
|
|
||||||
|
div
|
||||||
|
div
|
||||||
|
p Transactions
|
||||||
|
p=transactionCount
|
||||||
|
div
|
||||||
|
p Categorys
|
||||||
|
p=categoryCount
|
||||||
|
div
|
||||||
|
p Users
|
||||||
|
p=userCount
|
5
views/layout/_navbar.pug
Normal file
5
views/layout/_navbar.pug
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
nav
|
||||||
|
a(href="/") Home
|
||||||
|
a(href="/transactions") Transactions
|
||||||
|
a(href="/categories") Categories
|
||||||
|
a(href="/accounts") Accounts
|
18
views/layout/base.pug
Normal file
18
views/layout/base.pug
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title= title
|
||||||
|
link(href="/assets/css/app.css",rel="stylesheet")
|
||||||
|
|
||||||
|
block additionalAssets
|
||||||
|
|
||||||
|
body
|
||||||
|
div.nav
|
||||||
|
include ./_navbar.pug
|
||||||
|
div.nav
|
||||||
|
block nav
|
||||||
|
|
||||||
|
div.container
|
||||||
|
block headline
|
||||||
|
|
||||||
|
block content
|
41
views/transactions/_addTransactions.pug
Normal file
41
views/transactions/_addTransactions.pug
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
details
|
||||||
|
summary Create new transaction
|
||||||
|
|
||||||
|
form(action="/transactions",method="POST")
|
||||||
|
div.form-group
|
||||||
|
.text Description
|
||||||
|
.input
|
||||||
|
textarea(name="description", cols="30", rows="5") Test
|
||||||
|
div.form-group.half
|
||||||
|
.text Date
|
||||||
|
.input
|
||||||
|
input(type="date",name="date")
|
||||||
|
div.form-group.half
|
||||||
|
.text Time
|
||||||
|
.input
|
||||||
|
input(type="time",name="time")
|
||||||
|
div.form-group.half
|
||||||
|
.text Type
|
||||||
|
.input
|
||||||
|
select(name="type")
|
||||||
|
option(value="income") Income
|
||||||
|
option(value="expense",select) Expense
|
||||||
|
option(value="transfer") Transfer
|
||||||
|
div.form-group.half
|
||||||
|
.text Amount
|
||||||
|
.input
|
||||||
|
input(type="text",name="amount",required)
|
||||||
|
div.form-group
|
||||||
|
.text Account
|
||||||
|
.input
|
||||||
|
select(name="account")
|
||||||
|
each account in accounts
|
||||||
|
option(value=account.AccountID)=account.Name
|
||||||
|
div.form-group
|
||||||
|
.text Category
|
||||||
|
.input
|
||||||
|
select(name="category")
|
||||||
|
each category in categories
|
||||||
|
option(value=category.CategoryID,title=category.Description)=category.Name
|
||||||
|
button(type="submit") Add Transaction
|
39
views/transactions/formTransaction.pug
Normal file
39
views/transactions/formTransaction.pug
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
extends ./transactions.pug
|
||||||
|
block content
|
||||||
|
form(action="/transactions/addTransaction",method="POST")
|
||||||
|
div.form-group
|
||||||
|
.text Description
|
||||||
|
.input
|
||||||
|
textarea(name="description", cols="30", rows="5")
|
||||||
|
div.form-group.half
|
||||||
|
.text Date
|
||||||
|
.input
|
||||||
|
input(type="date",name="date")
|
||||||
|
div.form-group.half
|
||||||
|
.text Time
|
||||||
|
.input
|
||||||
|
input(type="time",name="time")
|
||||||
|
div.form-group.half
|
||||||
|
.text Type
|
||||||
|
.input
|
||||||
|
select(name="type")
|
||||||
|
option(value="income") Income
|
||||||
|
option(value="expense",select) Expense
|
||||||
|
option(value="transfer") Transfer
|
||||||
|
div.form-group.half
|
||||||
|
.text Account
|
||||||
|
.input
|
||||||
|
select(name="account")
|
||||||
|
each account in accounts
|
||||||
|
option(value=account.AccountID)=account.Name
|
||||||
|
div.form-group.half
|
||||||
|
.text Amount
|
||||||
|
.input
|
||||||
|
input(type="number",min="0",step="0.01",name="amount",required)
|
||||||
|
div.form-group.half
|
||||||
|
.text Category
|
||||||
|
.input
|
||||||
|
select(name="category")
|
||||||
|
each category in categories
|
||||||
|
option(value=category.CategoryID,title=category.Description)=category.Name
|
||||||
|
button(type="submit") Add Transaction
|
40
views/transactions/formTransfer.pug
Normal file
40
views/transactions/formTransfer.pug
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
extends ./transactions.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(action="/transactions/addTransfer",method="POST")
|
||||||
|
div.form-group
|
||||||
|
.text Description
|
||||||
|
.input
|
||||||
|
textarea(name="description", cols="30", rows="5")
|
||||||
|
div.form-group.half
|
||||||
|
.text Date
|
||||||
|
.input
|
||||||
|
input(type="date",name="date")
|
||||||
|
div.form-group.half
|
||||||
|
.text Time
|
||||||
|
.input
|
||||||
|
input(type="time",name="time")
|
||||||
|
|
||||||
|
div.form-group.half
|
||||||
|
.text from Account
|
||||||
|
.input
|
||||||
|
select(name="fromAccount")
|
||||||
|
each account in accounts
|
||||||
|
option(value=account.AccountID)=account.Name
|
||||||
|
div.form-group.half
|
||||||
|
.text to Account
|
||||||
|
.input
|
||||||
|
select(name="toAccount")
|
||||||
|
each account in accounts
|
||||||
|
option(value=account.AccountID)=account.Name
|
||||||
|
div.form-group.half
|
||||||
|
.text Amount
|
||||||
|
.input
|
||||||
|
input(type="number",min="0",step="0.01",name="amount",required)
|
||||||
|
div.form-group.half
|
||||||
|
.text Category
|
||||||
|
.input
|
||||||
|
select(name="category")
|
||||||
|
each category in categories
|
||||||
|
option(value=category.CategoryID,title=category.Description)=category.Name
|
||||||
|
button(type="submit") Add Transaction
|
22
views/transactions/listTransactions.pug
Normal file
22
views/transactions/listTransactions.pug
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
extends ./transactions.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
table(class=`${cssClass}`)
|
||||||
|
thead
|
||||||
|
th ID
|
||||||
|
th Date
|
||||||
|
th Description
|
||||||
|
th Category
|
||||||
|
th Account
|
||||||
|
th.right Amount
|
||||||
|
tbody
|
||||||
|
each transaction in transactions
|
||||||
|
tr(class=`${transaction.Type}`)
|
||||||
|
td=transaction.TransactionID
|
||||||
|
td=transaction.Date2
|
||||||
|
td=transaction.Description
|
||||||
|
td
|
||||||
|
a(href=`/transactions/cat/${transaction.Category.CategoryID}`)=transaction.Category.Name
|
||||||
|
td
|
||||||
|
a(href=`/transactions/acc/${transaction.Account.AccountID}`)=transaction.Account.Name
|
||||||
|
td(class=`${transaction.Type} amount` )=transaction.Amount
|
42
views/transactions/modifyTransaction.pug
Normal file
42
views/transactions/modifyTransaction.pug
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
extends ./transactions.pug
|
||||||
|
block content
|
||||||
|
form(action=`/transactions/modifyTransaction/${transaction.TransactionID}`,method="POST")
|
||||||
|
div.form-group
|
||||||
|
.text Description
|
||||||
|
.input
|
||||||
|
textarea(name="description", cols="30", rows="5")
|
||||||
|
div.form-group.half
|
||||||
|
.text Date
|
||||||
|
.input
|
||||||
|
input(type="date",name="date")
|
||||||
|
div.form-group.half
|
||||||
|
.text Time
|
||||||
|
.input
|
||||||
|
input(type="time",name="time")
|
||||||
|
div.form-group.half
|
||||||
|
.text Type
|
||||||
|
.input
|
||||||
|
select(name="type")
|
||||||
|
option(value="income") Income
|
||||||
|
option(value="expense",select) Expense
|
||||||
|
option(value="transfer") Transfer
|
||||||
|
div.form-group.half
|
||||||
|
.text Account
|
||||||
|
.input
|
||||||
|
select(name="account")
|
||||||
|
each account in accounts
|
||||||
|
option(value=account.AccountID)=account.Name
|
||||||
|
div.form-group.half
|
||||||
|
.text Amount
|
||||||
|
if transaction.locked
|
||||||
|
input(type="number",min="0",step="0.01",name="amount",disabled)
|
||||||
|
else
|
||||||
|
.input
|
||||||
|
input(type="number",min="0",step="0.01",name="amount",required)
|
||||||
|
div.form-group.half
|
||||||
|
.text Category
|
||||||
|
.input
|
||||||
|
select(name="category")
|
||||||
|
each category in categories
|
||||||
|
option(value=category.CategoryID,title=category.Description)=category.Name
|
||||||
|
button(type="submit") Modify Transaction
|
11
views/transactions/transactions.pug
Normal file
11
views/transactions/transactions.pug
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
extends ../layout/base.pug
|
||||||
|
|
||||||
|
block nav
|
||||||
|
a(href="/transactions") Transactions
|
||||||
|
a(href="/transactions/addTransaction") + Transaction
|
||||||
|
a(href="/transactions/addTransfer") + Transfer
|
||||||
|
|
||||||
|
block headline
|
||||||
|
h1 Transactions
|
||||||
|
|
||||||
|
block content
|
9
views/users.pug
Normal file
9
views/users.pug
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title= title
|
||||||
|
body
|
||||||
|
h1 Users
|
||||||
|
ul
|
||||||
|
each user in users
|
||||||
|
li= user.Name
|
Loading…
Reference in New Issue
Block a user