diff --git a/backend/src/controllers/CategoriesController.ts b/backend/src/controllers/CategoriesController.ts index 7dbb1c9..cc2445e 100644 --- a/backend/src/controllers/CategoriesController.ts +++ b/backend/src/controllers/CategoriesController.ts @@ -1,16 +1,22 @@ -import { Get, Put, Route, Path, Security, Post, Body, Controller, Tags, Request, Example, Query } from 'tsoa' +import { Get, Put, Route, Path, Security, Post, Body, Controller, Tags, Request, Example, Query, Delete } from 'tsoa' import { Budget } from '../entities/Budget' import { ExpressRequest } from './requests' import { ErrorResponse } from './responses' import { CategoryGroup } from '../entities/CategoryGroup' import { CategoryGroupRequest, CategoryGroupResponse, CategoryGroupsResponse } from '../models/CategoryGroup' -import { CategoryResponse } from '../models/Category' +import { + CategoryResponse, + DeleteCategoryModel, + DeleteCategoryRequest, + DeleteCategoryResponse, +} from '../models/Category' import { CategoryRequest } from '../models/Category' import { Category } from '../entities/Category' import { CategoryMonthRequest, CategoryMonthResponse, CategoryMonthsResponse } from '../models/CategoryMonth' import { CategoryMonth } from '../entities/CategoryMonth' import { getCustomRepository, getRepository, MoreThanOrEqual } from 'typeorm' import { CategoryMonths } from '../repositories/CategoryMonths' +import { Transaction } from '../entities/Transaction' @Tags('Categories') @Route('budgets/{budgetId}/categories') @@ -193,6 +199,7 @@ export class CategoriesController extends Controller { inflow: false, locked: false, order: 0, + hidden: false, created: new Date('2011-10-05T14:48:00.000Z'), updated: new Date('2011-10-05T14:48:00.000Z'), }, @@ -245,6 +252,7 @@ export class CategoriesController extends Controller { inflow: false, locked: false, order: 0, + hidden: false, created: new Date('2011-10-05T14:48:00.000Z'), updated: new Date('2011-10-05T14:48:00.000Z'), }, @@ -272,6 +280,10 @@ export class CategoriesController extends Controller { category.name = requestBody.name category.order = requestBody.order + if (requestBody.hidden !== undefined) { + category.hidden = requestBody.hidden + } + delete category.categoryGroup category.categoryGroupId = requestBody.categoryGroupId @@ -299,6 +311,90 @@ export class CategoriesController extends Controller { } } + /** + * Deete a category + */ + @Security('jwtRequired') + @Delete('{id}') + @Example({ + message: 'success', + data: { + id: 'abc123', + categoryGroupId: 'def456', + trackingAccountId: null, + name: 'Expenses', + inflow: false, + locked: false, + order: 0, + hidden: false, + created: new Date('2011-10-05T14:48:00.000Z'), + updated: new Date('2011-10-05T14:48:00.000Z'), + }, + }) + public async deleteCategory( + @Path() budgetId: string, + @Path() id: string, + @Body() requestBody: DeleteCategoryRequest, + @Request() request: ExpressRequest, + ): Promise { + try { + const budget = await getRepository(Budget).findOne(budgetId) + if (!budget || budget.userId !== request.user.id) { + this.setStatus(404) + return { + message: 'Not found', + } + } + + const response: DeleteCategoryModel = { + transactions: [], + categoryMonths: [], + } + + const category = await getRepository(Category).findOne(id, { relations: ['transactions', 'categoryMonths'] }) + + // First, transfer all existing transactions to the new category + for (const transaction of category.transactions) { + transaction.update({ + categoryId: requestBody.newCategoryId, + }) + await getRepository(Transaction).update(transaction.id, transaction.getUpdatePayload()) + + response.transactions.push(transaction) + } + + // Next, update all category months for budgeted items to the new category + for (const categoryMonth of category.categoryMonths) { + const newCategoryMonth = await getCustomRepository(CategoryMonths).findOrCreate( + budgetId, + requestBody.newCategoryId, + categoryMonth.month, + ) + + newCategoryMonth.update({ budgeted: newCategoryMonth.budgeted + categoryMonth.budgeted }) + await getRepository(CategoryMonth).update(newCategoryMonth.id, newCategoryMonth.getUpdatePayload()) + + response.categoryMonths.push(newCategoryMonth) + } + + // Delete old category months now + for (const categoryMonth of category.categoryMonths) { + await getRepository(CategoryMonth).remove(categoryMonth) + } + + // Finally, delete the category + await getRepository(Category).remove(category) + + return { + message: 'success', + data: response, + } + } catch (err) { + console.log(err) + return { message: err.message } + } + } + /** * Update category month */ diff --git a/backend/src/entities/Category.ts b/backend/src/entities/Category.ts index ca9a1c5..2e8bcac 100644 --- a/backend/src/entities/Category.ts +++ b/backend/src/entities/Category.ts @@ -33,6 +33,9 @@ export class Category { @Column({ type: 'int', default: 0 }) order: number = 0 + @Column({ type: 'boolean', default: false }) + hidden: boolean + @CreateDateColumn() created: Date @@ -73,6 +76,7 @@ export class Category { inflow: this.inflow, locked: this.locked, order: this.order, + hidden: this.hidden, } } @@ -85,6 +89,7 @@ export class Category { inflow: this.inflow, locked: this.locked, order: this.order, + hidden: this.hidden, created: this.created, updated: this.updated, } diff --git a/backend/src/migrations/1654008829738-cascade-deletes.ts b/backend/src/migrations/1654008829738-cascade-deletes.ts new file mode 100644 index 0000000..5cc5402 --- /dev/null +++ b/backend/src/migrations/1654008829738-cascade-deletes.ts @@ -0,0 +1,196 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class cascadeDeletes1654008829738 implements MigrationInterface { + name = 'cascadeDeletes1654008829738' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_398c07457719d1899ba4f11914"`); + await queryRunner.query(`DROP INDEX "IDX_0c21df54422306fdf78621fc18"`); + await queryRunner.query(`CREATE TABLE "temporary_budget_months" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "month" varchar NOT NULL, "income" integer NOT NULL DEFAULT (0), "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "underfunded" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), "available" integer NOT NULL DEFAULT (0))`); + await queryRunner.query(`INSERT INTO "temporary_budget_months"("id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available") SELECT "id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available" FROM "budget_months"`); + await queryRunner.query(`DROP TABLE "budget_months"`); + await queryRunner.query(`ALTER TABLE "temporary_budget_months" RENAME TO "budget_months"`); + await queryRunner.query(`CREATE INDEX "IDX_398c07457719d1899ba4f11914" ON "budget_months" ("budgetId") `); + await queryRunner.query(`CREATE INDEX "IDX_0c21df54422306fdf78621fc18" ON "budget_months" ("month") `); + await queryRunner.query(`DROP INDEX "IDX_23f4c8894717fb764a2b88ff29"`); + await queryRunner.query(`DROP INDEX "IDX_de0f1ed5fe7ad4f2254bb815be"`); + await queryRunner.query(`DROP INDEX "IDX_cba488e36ca6ff6eec83e91440"`); + await queryRunner.query(`CREATE TABLE "temporary_category_months" ("id" varchar PRIMARY KEY NOT NULL, "categoryId" varchar NOT NULL, "budgetMonthId" varchar NOT NULL, "month" varchar NOT NULL, "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "balance" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_de0f1ed5fe7ad4f2254bb815bef" FOREIGN KEY ("budgetMonthId") REFERENCES "budget_months" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_category_months"("id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated") SELECT "id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated" FROM "category_months"`); + await queryRunner.query(`DROP TABLE "category_months"`); + await queryRunner.query(`ALTER TABLE "temporary_category_months" RENAME TO "category_months"`); + await queryRunner.query(`CREATE INDEX "IDX_23f4c8894717fb764a2b88ff29" ON "category_months" ("month") `); + await queryRunner.query(`CREATE INDEX "IDX_de0f1ed5fe7ad4f2254bb815be" ON "category_months" ("budgetMonthId") `); + await queryRunner.query(`CREATE INDEX "IDX_cba488e36ca6ff6eec83e91440" ON "category_months" ("categoryId") `); + await queryRunner.query(`DROP INDEX "IDX_af173d6048d44da16b00e49e24"`); + await queryRunner.query(`DROP INDEX "IDX_e6d5be2f8c1fbd283150e043a0"`); + await queryRunner.query(`CREATE TABLE "temporary_categories" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "categoryGroupId" varchar NOT NULL, "trackingAccountId" varchar, "name" varchar NOT NULL, "inflow" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_e6d5be2f8c1fbd283150e043a08" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_categories"("id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated") SELECT "id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated" FROM "categories"`); + await queryRunner.query(`DROP TABLE "categories"`); + await queryRunner.query(`ALTER TABLE "temporary_categories" RENAME TO "categories"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_af173d6048d44da16b00e49e24" ON "categories" ("trackingAccountId") `); + await queryRunner.query(`CREATE INDEX "IDX_e6d5be2f8c1fbd283150e043a0" ON "categories" ("budgetId") `); + await queryRunner.query(`CREATE TABLE "temporary_payees" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferAccountId" varchar, "name" varchar NOT NULL, "internal" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_f61fc1e67d3abfb79503cdd382" UNIQUE ("transferAccountId"))`); + await queryRunner.query(`INSERT INTO "temporary_payees"("id", "budgetId", "transferAccountId", "name", "internal", "created", "updated") SELECT "id", "budgetId", "transferAccountId", "name", "internal", "created", "updated" FROM "payees"`); + await queryRunner.query(`DROP TABLE "payees"`); + await queryRunner.query(`ALTER TABLE "temporary_payees" RENAME TO "payees"`); + await queryRunner.query(`DROP INDEX "IDX_7098ffeb5373b7d6344f4f1663"`); + await queryRunner.query(`CREATE TABLE "temporary_transactions" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "accountId" varchar NOT NULL, "payeeId" varchar NOT NULL, "transferAccountId" varchar, "transferTransactionId" varchar, "categoryId" varchar, "amount" integer NOT NULL DEFAULT (0), "date" datetime NOT NULL, "memo" varchar NOT NULL DEFAULT (''), "status" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_86e965e74f9cc66149cf6c90f64" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_44075a45926dfce0379d2c81c83" FOREIGN KEY ("payeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_9552f6354aafa8f1818aa571aaf" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_transactions"("id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated") SELECT "id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated" FROM "transactions"`); + await queryRunner.query(`DROP TABLE "transactions"`); + await queryRunner.query(`ALTER TABLE "temporary_transactions" RENAME TO "transactions"`); + await queryRunner.query(`CREATE INDEX "IDX_7098ffeb5373b7d6344f4f1663" ON "transactions" ("transferTransactionId") `); + await queryRunner.query(`CREATE TABLE "temporary_accounts" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferPayeeId" varchar, "name" varchar NOT NULL, "type" integer NOT NULL, "balance" integer NOT NULL DEFAULT (0), "cleared" integer NOT NULL DEFAULT (0), "uncleared" integer NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_c2a8be4512a377b0a8614170e3" UNIQUE ("transferPayeeId"), CONSTRAINT "FK_c2a8be4512a377b0a8614170e33" FOREIGN KEY ("transferPayeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_accounts"("id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated") SELECT "id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated" FROM "accounts"`); + await queryRunner.query(`DROP TABLE "accounts"`); + await queryRunner.query(`ALTER TABLE "temporary_accounts" RENAME TO "accounts"`); + await queryRunner.query(`DROP INDEX "IDX_0dcceebef7c019bc892be7b5d0"`); + await queryRunner.query(`CREATE TABLE "temporary_category_groups" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "name" varchar NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`INSERT INTO "temporary_category_groups"("id", "budgetId", "name", "internal", "locked", "order", "created", "updated") SELECT "id", "budgetId", "name", "internal", "locked", "order", "created", "updated" FROM "category_groups"`); + await queryRunner.query(`DROP TABLE "category_groups"`); + await queryRunner.query(`ALTER TABLE "temporary_category_groups" RENAME TO "category_groups"`); + await queryRunner.query(`CREATE INDEX "IDX_0dcceebef7c019bc892be7b5d0" ON "category_groups" ("budgetId") `); + await queryRunner.query(`DROP INDEX "IDX_398c07457719d1899ba4f11914"`); + await queryRunner.query(`DROP INDEX "IDX_0c21df54422306fdf78621fc18"`); + await queryRunner.query(`CREATE TABLE "temporary_budget_months" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "month" varchar NOT NULL, "income" integer NOT NULL DEFAULT (0), "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "underfunded" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), "available" integer NOT NULL DEFAULT (0), CONSTRAINT "FK_398c07457719d1899ba4f11914d" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_budget_months"("id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available") SELECT "id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available" FROM "budget_months"`); + await queryRunner.query(`DROP TABLE "budget_months"`); + await queryRunner.query(`ALTER TABLE "temporary_budget_months" RENAME TO "budget_months"`); + await queryRunner.query(`CREATE INDEX "IDX_398c07457719d1899ba4f11914" ON "budget_months" ("budgetId") `); + await queryRunner.query(`CREATE INDEX "IDX_0c21df54422306fdf78621fc18" ON "budget_months" ("month") `); + await queryRunner.query(`DROP INDEX "IDX_23f4c8894717fb764a2b88ff29"`); + await queryRunner.query(`DROP INDEX "IDX_de0f1ed5fe7ad4f2254bb815be"`); + await queryRunner.query(`DROP INDEX "IDX_cba488e36ca6ff6eec83e91440"`); + await queryRunner.query(`CREATE TABLE "temporary_category_months" ("id" varchar PRIMARY KEY NOT NULL, "categoryId" varchar NOT NULL, "budgetMonthId" varchar NOT NULL, "month" varchar NOT NULL, "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "balance" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_de0f1ed5fe7ad4f2254bb815bef" FOREIGN KEY ("budgetMonthId") REFERENCES "budget_months" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_cba488e36ca6ff6eec83e914409" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_category_months"("id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated") SELECT "id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated" FROM "category_months"`); + await queryRunner.query(`DROP TABLE "category_months"`); + await queryRunner.query(`ALTER TABLE "temporary_category_months" RENAME TO "category_months"`); + await queryRunner.query(`CREATE INDEX "IDX_23f4c8894717fb764a2b88ff29" ON "category_months" ("month") `); + await queryRunner.query(`CREATE INDEX "IDX_de0f1ed5fe7ad4f2254bb815be" ON "category_months" ("budgetMonthId") `); + await queryRunner.query(`CREATE INDEX "IDX_cba488e36ca6ff6eec83e91440" ON "category_months" ("categoryId") `); + await queryRunner.query(`DROP INDEX "IDX_af173d6048d44da16b00e49e24"`); + await queryRunner.query(`DROP INDEX "IDX_e6d5be2f8c1fbd283150e043a0"`); + await queryRunner.query(`CREATE TABLE "temporary_categories" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "categoryGroupId" varchar NOT NULL, "trackingAccountId" varchar, "name" varchar NOT NULL, "inflow" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_e6d5be2f8c1fbd283150e043a08" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d05bb3b46b9b190eb9c20ad3c21" FOREIGN KEY ("categoryGroupId") REFERENCES "category_groups" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_categories"("id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated") SELECT "id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated" FROM "categories"`); + await queryRunner.query(`DROP TABLE "categories"`); + await queryRunner.query(`ALTER TABLE "temporary_categories" RENAME TO "categories"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_af173d6048d44da16b00e49e24" ON "categories" ("trackingAccountId") `); + await queryRunner.query(`CREATE INDEX "IDX_e6d5be2f8c1fbd283150e043a0" ON "categories" ("budgetId") `); + await queryRunner.query(`CREATE TABLE "temporary_payees" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferAccountId" varchar, "name" varchar NOT NULL, "internal" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_f61fc1e67d3abfb79503cdd382" UNIQUE ("transferAccountId"), CONSTRAINT "FK_57c7e807805962198abcae57c66" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f61fc1e67d3abfb79503cdd3821" FOREIGN KEY ("transferAccountId") REFERENCES "accounts" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_payees"("id", "budgetId", "transferAccountId", "name", "internal", "created", "updated") SELECT "id", "budgetId", "transferAccountId", "name", "internal", "created", "updated" FROM "payees"`); + await queryRunner.query(`DROP TABLE "payees"`); + await queryRunner.query(`ALTER TABLE "temporary_payees" RENAME TO "payees"`); + await queryRunner.query(`DROP INDEX "IDX_7098ffeb5373b7d6344f4f1663"`); + await queryRunner.query(`CREATE TABLE "temporary_transactions" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "accountId" varchar NOT NULL, "payeeId" varchar NOT NULL, "transferAccountId" varchar, "transferTransactionId" varchar, "categoryId" varchar, "amount" integer NOT NULL DEFAULT (0), "date" datetime NOT NULL, "memo" varchar NOT NULL DEFAULT (''), "status" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_86e965e74f9cc66149cf6c90f64" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_44075a45926dfce0379d2c81c83" FOREIGN KEY ("payeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_9552f6354aafa8f1818aa571aaf" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_26d8aec71ae9efbe468043cd2b9" FOREIGN KEY ("accountId") REFERENCES "accounts" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_transactions"("id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated") SELECT "id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated" FROM "transactions"`); + await queryRunner.query(`DROP TABLE "transactions"`); + await queryRunner.query(`ALTER TABLE "temporary_transactions" RENAME TO "transactions"`); + await queryRunner.query(`CREATE INDEX "IDX_7098ffeb5373b7d6344f4f1663" ON "transactions" ("transferTransactionId") `); + await queryRunner.query(`CREATE TABLE "temporary_accounts" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferPayeeId" varchar, "name" varchar NOT NULL, "type" integer NOT NULL, "balance" integer NOT NULL DEFAULT (0), "cleared" integer NOT NULL DEFAULT (0), "uncleared" integer NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_c2a8be4512a377b0a8614170e3" UNIQUE ("transferPayeeId"), CONSTRAINT "FK_c2a8be4512a377b0a8614170e33" FOREIGN KEY ("transferPayeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_81acfbf2205a3be5b1c41455329" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_accounts"("id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated") SELECT "id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated" FROM "accounts"`); + await queryRunner.query(`DROP TABLE "accounts"`); + await queryRunner.query(`ALTER TABLE "temporary_accounts" RENAME TO "accounts"`); + await queryRunner.query(`DROP INDEX "IDX_0dcceebef7c019bc892be7b5d0"`); + await queryRunner.query(`CREATE TABLE "temporary_category_groups" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "name" varchar NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_0dcceebef7c019bc892be7b5d0e" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_category_groups"("id", "budgetId", "name", "internal", "locked", "order", "created", "updated") SELECT "id", "budgetId", "name", "internal", "locked", "order", "created", "updated" FROM "category_groups"`); + await queryRunner.query(`DROP TABLE "category_groups"`); + await queryRunner.query(`ALTER TABLE "temporary_category_groups" RENAME TO "category_groups"`); + await queryRunner.query(`CREATE INDEX "IDX_0dcceebef7c019bc892be7b5d0" ON "category_groups" ("budgetId") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_0dcceebef7c019bc892be7b5d0"`); + await queryRunner.query(`ALTER TABLE "category_groups" RENAME TO "temporary_category_groups"`); + await queryRunner.query(`CREATE TABLE "category_groups" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "name" varchar NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`INSERT INTO "category_groups"("id", "budgetId", "name", "internal", "locked", "order", "created", "updated") SELECT "id", "budgetId", "name", "internal", "locked", "order", "created", "updated" FROM "temporary_category_groups"`); + await queryRunner.query(`DROP TABLE "temporary_category_groups"`); + await queryRunner.query(`CREATE INDEX "IDX_0dcceebef7c019bc892be7b5d0" ON "category_groups" ("budgetId") `); + await queryRunner.query(`ALTER TABLE "accounts" RENAME TO "temporary_accounts"`); + await queryRunner.query(`CREATE TABLE "accounts" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferPayeeId" varchar, "name" varchar NOT NULL, "type" integer NOT NULL, "balance" integer NOT NULL DEFAULT (0), "cleared" integer NOT NULL DEFAULT (0), "uncleared" integer NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_c2a8be4512a377b0a8614170e3" UNIQUE ("transferPayeeId"), CONSTRAINT "FK_c2a8be4512a377b0a8614170e33" FOREIGN KEY ("transferPayeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "accounts"("id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated") SELECT "id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated" FROM "temporary_accounts"`); + await queryRunner.query(`DROP TABLE "temporary_accounts"`); + await queryRunner.query(`DROP INDEX "IDX_7098ffeb5373b7d6344f4f1663"`); + await queryRunner.query(`ALTER TABLE "transactions" RENAME TO "temporary_transactions"`); + await queryRunner.query(`CREATE TABLE "transactions" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "accountId" varchar NOT NULL, "payeeId" varchar NOT NULL, "transferAccountId" varchar, "transferTransactionId" varchar, "categoryId" varchar, "amount" integer NOT NULL DEFAULT (0), "date" datetime NOT NULL, "memo" varchar NOT NULL DEFAULT (''), "status" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_86e965e74f9cc66149cf6c90f64" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_44075a45926dfce0379d2c81c83" FOREIGN KEY ("payeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_9552f6354aafa8f1818aa571aaf" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "transactions"("id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated") SELECT "id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated" FROM "temporary_transactions"`); + await queryRunner.query(`DROP TABLE "temporary_transactions"`); + await queryRunner.query(`CREATE INDEX "IDX_7098ffeb5373b7d6344f4f1663" ON "transactions" ("transferTransactionId") `); + await queryRunner.query(`ALTER TABLE "payees" RENAME TO "temporary_payees"`); + await queryRunner.query(`CREATE TABLE "payees" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferAccountId" varchar, "name" varchar NOT NULL, "internal" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_f61fc1e67d3abfb79503cdd382" UNIQUE ("transferAccountId"))`); + await queryRunner.query(`INSERT INTO "payees"("id", "budgetId", "transferAccountId", "name", "internal", "created", "updated") SELECT "id", "budgetId", "transferAccountId", "name", "internal", "created", "updated" FROM "temporary_payees"`); + await queryRunner.query(`DROP TABLE "temporary_payees"`); + await queryRunner.query(`DROP INDEX "IDX_e6d5be2f8c1fbd283150e043a0"`); + await queryRunner.query(`DROP INDEX "IDX_af173d6048d44da16b00e49e24"`); + await queryRunner.query(`ALTER TABLE "categories" RENAME TO "temporary_categories"`); + await queryRunner.query(`CREATE TABLE "categories" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "categoryGroupId" varchar NOT NULL, "trackingAccountId" varchar, "name" varchar NOT NULL, "inflow" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_e6d5be2f8c1fbd283150e043a08" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "categories"("id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated") SELECT "id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated" FROM "temporary_categories"`); + await queryRunner.query(`DROP TABLE "temporary_categories"`); + await queryRunner.query(`CREATE INDEX "IDX_e6d5be2f8c1fbd283150e043a0" ON "categories" ("budgetId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_af173d6048d44da16b00e49e24" ON "categories" ("trackingAccountId") `); + await queryRunner.query(`DROP INDEX "IDX_cba488e36ca6ff6eec83e91440"`); + await queryRunner.query(`DROP INDEX "IDX_de0f1ed5fe7ad4f2254bb815be"`); + await queryRunner.query(`DROP INDEX "IDX_23f4c8894717fb764a2b88ff29"`); + await queryRunner.query(`ALTER TABLE "category_months" RENAME TO "temporary_category_months"`); + await queryRunner.query(`CREATE TABLE "category_months" ("id" varchar PRIMARY KEY NOT NULL, "categoryId" varchar NOT NULL, "budgetMonthId" varchar NOT NULL, "month" varchar NOT NULL, "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "balance" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_de0f1ed5fe7ad4f2254bb815bef" FOREIGN KEY ("budgetMonthId") REFERENCES "budget_months" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "category_months"("id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated") SELECT "id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated" FROM "temporary_category_months"`); + await queryRunner.query(`DROP TABLE "temporary_category_months"`); + await queryRunner.query(`CREATE INDEX "IDX_cba488e36ca6ff6eec83e91440" ON "category_months" ("categoryId") `); + await queryRunner.query(`CREATE INDEX "IDX_de0f1ed5fe7ad4f2254bb815be" ON "category_months" ("budgetMonthId") `); + await queryRunner.query(`CREATE INDEX "IDX_23f4c8894717fb764a2b88ff29" ON "category_months" ("month") `); + await queryRunner.query(`DROP INDEX "IDX_0c21df54422306fdf78621fc18"`); + await queryRunner.query(`DROP INDEX "IDX_398c07457719d1899ba4f11914"`); + await queryRunner.query(`ALTER TABLE "budget_months" RENAME TO "temporary_budget_months"`); + await queryRunner.query(`CREATE TABLE "budget_months" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "month" varchar NOT NULL, "income" integer NOT NULL DEFAULT (0), "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "underfunded" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), "available" integer NOT NULL DEFAULT (0))`); + await queryRunner.query(`INSERT INTO "budget_months"("id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available") SELECT "id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available" FROM "temporary_budget_months"`); + await queryRunner.query(`DROP TABLE "temporary_budget_months"`); + await queryRunner.query(`CREATE INDEX "IDX_0c21df54422306fdf78621fc18" ON "budget_months" ("month") `); + await queryRunner.query(`CREATE INDEX "IDX_398c07457719d1899ba4f11914" ON "budget_months" ("budgetId") `); + await queryRunner.query(`DROP INDEX "IDX_0dcceebef7c019bc892be7b5d0"`); + await queryRunner.query(`ALTER TABLE "category_groups" RENAME TO "temporary_category_groups"`); + await queryRunner.query(`CREATE TABLE "category_groups" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "name" varchar NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_0dcceebef7c019bc892be7b5d0e" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "category_groups"("id", "budgetId", "name", "internal", "locked", "order", "created", "updated") SELECT "id", "budgetId", "name", "internal", "locked", "order", "created", "updated" FROM "temporary_category_groups"`); + await queryRunner.query(`DROP TABLE "temporary_category_groups"`); + await queryRunner.query(`CREATE INDEX "IDX_0dcceebef7c019bc892be7b5d0" ON "category_groups" ("budgetId") `); + await queryRunner.query(`ALTER TABLE "accounts" RENAME TO "temporary_accounts"`); + await queryRunner.query(`CREATE TABLE "accounts" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferPayeeId" varchar, "name" varchar NOT NULL, "type" integer NOT NULL, "balance" integer NOT NULL DEFAULT (0), "cleared" integer NOT NULL DEFAULT (0), "uncleared" integer NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_c2a8be4512a377b0a8614170e3" UNIQUE ("transferPayeeId"), CONSTRAINT "FK_c2a8be4512a377b0a8614170e33" FOREIGN KEY ("transferPayeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_81acfbf2205a3be5b1c41455329" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "accounts"("id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated") SELECT "id", "budgetId", "transferPayeeId", "name", "type", "balance", "cleared", "uncleared", "order", "created", "updated" FROM "temporary_accounts"`); + await queryRunner.query(`DROP TABLE "temporary_accounts"`); + await queryRunner.query(`DROP INDEX "IDX_7098ffeb5373b7d6344f4f1663"`); + await queryRunner.query(`ALTER TABLE "transactions" RENAME TO "temporary_transactions"`); + await queryRunner.query(`CREATE TABLE "transactions" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "accountId" varchar NOT NULL, "payeeId" varchar NOT NULL, "transferAccountId" varchar, "transferTransactionId" varchar, "categoryId" varchar, "amount" integer NOT NULL DEFAULT (0), "date" datetime NOT NULL, "memo" varchar NOT NULL DEFAULT (''), "status" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_86e965e74f9cc66149cf6c90f64" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_44075a45926dfce0379d2c81c83" FOREIGN KEY ("payeeId") REFERENCES "payees" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_26d8aec71ae9efbe468043cd2b9" FOREIGN KEY ("accountId") REFERENCES "accounts" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_9552f6354aafa8f1818aa571aaf" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "transactions"("id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated") SELECT "id", "budgetId", "accountId", "payeeId", "transferAccountId", "transferTransactionId", "categoryId", "amount", "date", "memo", "status", "created", "updated" FROM "temporary_transactions"`); + await queryRunner.query(`DROP TABLE "temporary_transactions"`); + await queryRunner.query(`CREATE INDEX "IDX_7098ffeb5373b7d6344f4f1663" ON "transactions" ("transferTransactionId") `); + await queryRunner.query(`ALTER TABLE "payees" RENAME TO "temporary_payees"`); + await queryRunner.query(`CREATE TABLE "payees" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "transferAccountId" varchar, "name" varchar NOT NULL, "internal" boolean NOT NULL, "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "REL_f61fc1e67d3abfb79503cdd382" UNIQUE ("transferAccountId"), CONSTRAINT "FK_f61fc1e67d3abfb79503cdd3821" FOREIGN KEY ("transferAccountId") REFERENCES "accounts" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "payees"("id", "budgetId", "transferAccountId", "name", "internal", "created", "updated") SELECT "id", "budgetId", "transferAccountId", "name", "internal", "created", "updated" FROM "temporary_payees"`); + await queryRunner.query(`DROP TABLE "temporary_payees"`); + await queryRunner.query(`DROP INDEX "IDX_e6d5be2f8c1fbd283150e043a0"`); + await queryRunner.query(`DROP INDEX "IDX_af173d6048d44da16b00e49e24"`); + await queryRunner.query(`ALTER TABLE "categories" RENAME TO "temporary_categories"`); + await queryRunner.query(`CREATE TABLE "categories" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "categoryGroupId" varchar NOT NULL, "trackingAccountId" varchar, "name" varchar NOT NULL, "inflow" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_d05bb3b46b9b190eb9c20ad3c21" FOREIGN KEY ("categoryGroupId") REFERENCES "category_groups" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_e6d5be2f8c1fbd283150e043a08" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "categories"("id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated") SELECT "id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated" FROM "temporary_categories"`); + await queryRunner.query(`DROP TABLE "temporary_categories"`); + await queryRunner.query(`CREATE INDEX "IDX_e6d5be2f8c1fbd283150e043a0" ON "categories" ("budgetId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_af173d6048d44da16b00e49e24" ON "categories" ("trackingAccountId") `); + await queryRunner.query(`DROP INDEX "IDX_cba488e36ca6ff6eec83e91440"`); + await queryRunner.query(`DROP INDEX "IDX_de0f1ed5fe7ad4f2254bb815be"`); + await queryRunner.query(`DROP INDEX "IDX_23f4c8894717fb764a2b88ff29"`); + await queryRunner.query(`ALTER TABLE "category_months" RENAME TO "temporary_category_months"`); + await queryRunner.query(`CREATE TABLE "category_months" ("id" varchar PRIMARY KEY NOT NULL, "categoryId" varchar NOT NULL, "budgetMonthId" varchar NOT NULL, "month" varchar NOT NULL, "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "balance" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_de0f1ed5fe7ad4f2254bb815bef" FOREIGN KEY ("budgetMonthId") REFERENCES "budget_months" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_cba488e36ca6ff6eec83e914409" FOREIGN KEY ("categoryId") REFERENCES "categories" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "category_months"("id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated") SELECT "id", "categoryId", "budgetMonthId", "month", "budgeted", "activity", "balance", "created", "updated" FROM "temporary_category_months"`); + await queryRunner.query(`DROP TABLE "temporary_category_months"`); + await queryRunner.query(`CREATE INDEX "IDX_cba488e36ca6ff6eec83e91440" ON "category_months" ("categoryId") `); + await queryRunner.query(`CREATE INDEX "IDX_de0f1ed5fe7ad4f2254bb815be" ON "category_months" ("budgetMonthId") `); + await queryRunner.query(`CREATE INDEX "IDX_23f4c8894717fb764a2b88ff29" ON "category_months" ("month") `); + await queryRunner.query(`DROP INDEX "IDX_0c21df54422306fdf78621fc18"`); + await queryRunner.query(`DROP INDEX "IDX_398c07457719d1899ba4f11914"`); + await queryRunner.query(`ALTER TABLE "budget_months" RENAME TO "temporary_budget_months"`); + await queryRunner.query(`CREATE TABLE "budget_months" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "month" varchar NOT NULL, "income" integer NOT NULL DEFAULT (0), "budgeted" integer NOT NULL DEFAULT (0), "activity" integer NOT NULL DEFAULT (0), "underfunded" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), "available" integer NOT NULL DEFAULT (0), CONSTRAINT "FK_398c07457719d1899ba4f11914d" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "budget_months"("id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available") SELECT "id", "budgetId", "month", "income", "budgeted", "activity", "underfunded", "created", "updated", "available" FROM "temporary_budget_months"`); + await queryRunner.query(`DROP TABLE "temporary_budget_months"`); + await queryRunner.query(`CREATE INDEX "IDX_0c21df54422306fdf78621fc18" ON "budget_months" ("month") `); + await queryRunner.query(`CREATE INDEX "IDX_398c07457719d1899ba4f11914" ON "budget_months" ("budgetId") `); + } + +} diff --git a/backend/src/migrations/1654008868237-hidden-categories.ts b/backend/src/migrations/1654008868237-hidden-categories.ts new file mode 100644 index 0000000..75b44f2 --- /dev/null +++ b/backend/src/migrations/1654008868237-hidden-categories.ts @@ -0,0 +1,28 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class hiddenCategories1654008868237 implements MigrationInterface { + name = 'hiddenCategories1654008868237' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_e6d5be2f8c1fbd283150e043a0"`); + await queryRunner.query(`DROP INDEX "IDX_af173d6048d44da16b00e49e24"`); + await queryRunner.query(`CREATE TABLE "temporary_categories" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "categoryGroupId" varchar NOT NULL, "trackingAccountId" varchar, "name" varchar NOT NULL, "inflow" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), "hidden" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_d05bb3b46b9b190eb9c20ad3c21" FOREIGN KEY ("categoryGroupId") REFERENCES "category_groups" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_e6d5be2f8c1fbd283150e043a08" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_categories"("id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated") SELECT "id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated" FROM "categories"`); + await queryRunner.query(`DROP TABLE "categories"`); + await queryRunner.query(`ALTER TABLE "temporary_categories" RENAME TO "categories"`); + await queryRunner.query(`CREATE INDEX "IDX_e6d5be2f8c1fbd283150e043a0" ON "categories" ("budgetId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_af173d6048d44da16b00e49e24" ON "categories" ("trackingAccountId") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_af173d6048d44da16b00e49e24"`); + await queryRunner.query(`DROP INDEX "IDX_e6d5be2f8c1fbd283150e043a0"`); + await queryRunner.query(`ALTER TABLE "categories" RENAME TO "temporary_categories"`); + await queryRunner.query(`CREATE TABLE "categories" ("id" varchar PRIMARY KEY NOT NULL, "budgetId" varchar NOT NULL, "categoryGroupId" varchar NOT NULL, "trackingAccountId" varchar, "name" varchar NOT NULL, "inflow" boolean NOT NULL DEFAULT (0), "locked" boolean NOT NULL DEFAULT (0), "order" integer NOT NULL DEFAULT (0), "created" datetime NOT NULL DEFAULT (datetime('now')), "updated" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_d05bb3b46b9b190eb9c20ad3c21" FOREIGN KEY ("categoryGroupId") REFERENCES "category_groups" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_e6d5be2f8c1fbd283150e043a08" FOREIGN KEY ("budgetId") REFERENCES "budgets" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "categories"("id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated") SELECT "id", "budgetId", "categoryGroupId", "trackingAccountId", "name", "inflow", "locked", "order", "created", "updated" FROM "temporary_categories"`); + await queryRunner.query(`DROP TABLE "temporary_categories"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_af173d6048d44da16b00e49e24" ON "categories" ("trackingAccountId") `); + await queryRunner.query(`CREATE INDEX "IDX_e6d5be2f8c1fbd283150e043a0" ON "categories" ("budgetId") `); + } + +} diff --git a/backend/src/models/Category.ts b/backend/src/models/Category.ts index 6a585d6..e1e5141 100644 --- a/backend/src/models/Category.ts +++ b/backend/src/models/Category.ts @@ -1,6 +1,8 @@ import { AccountTypes } from '../entities/Account' import { DataResponse } from '../controllers/responses' import { TransactionStatus } from '../entities/Transaction' +import { TransactionModel } from './Transaction' +import { CategoryMonthModel } from './CategoryMonth' /** * @example { @@ -47,6 +49,11 @@ export interface CategoryModel { */ order: number + /** + * Hidden flag + */ + hidden: boolean + /** * Datetime transaction was created */ @@ -68,6 +75,25 @@ export interface CategoryRequest { categoryGroupId: string name: string order: number + hidden?: boolean +} + +export interface DeleteCategoryRequest { + newCategoryId: string +} + +export interface DeleteCategoryModel { + /** + * All updated transactions + */ + transactions: TransactionModel[] + + /** + * All updated category months + */ + categoryMonths: CategoryMonthModel[] } export type CategoryResponse = DataResponse + +export type DeleteCategoryResponse = DataResponse diff --git a/backend/src/subscribers/AccountSubscriber.ts b/backend/src/subscribers/AccountSubscriber.ts index 6038e56..611770d 100644 --- a/backend/src/subscribers/AccountSubscriber.ts +++ b/backend/src/subscribers/AccountSubscriber.ts @@ -14,10 +14,7 @@ export class AccountSubscriber implements EntitySubscriberInterface { await Promise.all([this.createCreditCardCategory(event), this.createAccountPayee(event)]) } - private async createAccountPayee(event: InsertEvent) { - const account = event.entity - const manager = event.manager - + private async createAccountPayee({ entity: account, manager }: InsertEvent) { const payee = manager.create(Payee, { budgetId: account.budgetId, name: `Transfer : ${account.name}`, @@ -30,10 +27,7 @@ export class AccountSubscriber implements EntitySubscriberInterface { await manager.update(Account, account.id, account.getUpdatePayload()) } - private async createCreditCardCategory(event: InsertEvent) { - const account = event.entity - const manager = event.manager - + private async createCreditCardCategory({ entity: account, manager }: InsertEvent) { if (account.type === AccountTypes.CreditCard) { // Create CC payments category if it doesn't exist const ccGroup = @@ -61,9 +55,7 @@ export class AccountSubscriber implements EntitySubscriberInterface { } } - async beforeUpdate(event: UpdateEvent) { - const account = event.entity - + async beforeUpdate({ entity: account }: UpdateEvent) { account.balance = account.cleared + account.uncleared } } diff --git a/backend/src/subscribers/BudgetMonthSubscriber.ts b/backend/src/subscribers/BudgetMonthSubscriber.ts index 16a9efb..b1e2e13 100644 --- a/backend/src/subscribers/BudgetMonthSubscriber.ts +++ b/backend/src/subscribers/BudgetMonthSubscriber.ts @@ -10,9 +10,7 @@ export class BudgetMonthSubscriber implements EntitySubscriberInterface) { - const budgetMonth = event.entity - const manager = event.manager + async beforeInsert({ entity: budgetMonth, manager }: InsertEvent) { const prevMonth = getDateFromString(budgetMonth.month).minus({ month: 1 }) const prevBudgetMonth = await manager.findOne(BudgetMonth, { @@ -27,10 +25,7 @@ export class BudgetMonthSubscriber implements EntitySubscriberInterface) { - const budgetMonth = event.entity - const manager = event.manager - + async afterUpdate({ entity: budgetMonth, manager }: UpdateEvent) { const nextMonth = getDateFromString(budgetMonth.month).plus({ month: 1 }) const nextBudgetMonth = await manager.findOne(BudgetMonth, { budgetId: budgetMonth.budgetId, @@ -64,9 +59,7 @@ export class BudgetMonthSubscriber implements EntitySubscriberInterface) { - const budgetMonth = event.entity - const manager = event.manager + async afterInsert({ entity: budgetMonth, manager }: InsertEvent) { const prevMonth = getDateFromString(budgetMonth.month).minus({ month: 1 }) const prevBudgetMonth = await manager.findOne(BudgetMonth, { diff --git a/backend/src/subscribers/BudgetSubscriber.ts b/backend/src/subscribers/BudgetSubscriber.ts index 1162ba2..4a59806 100644 --- a/backend/src/subscribers/BudgetSubscriber.ts +++ b/backend/src/subscribers/BudgetSubscriber.ts @@ -12,10 +12,7 @@ export class BudgetSubscriber implements EntitySubscriberInterface { return Budget } - async afterInsert(event: InsertEvent) { - const manager = event.manager - const budget = event.entity - + async afterInsert({ entity: budget, manager }: InsertEvent) { const today = getMonthString() const prevMonth = getMonthStringFromNow(-1) const nextMonth = getMonthStringFromNow(1) diff --git a/backend/src/subscribers/CategoryMonthSubscriber.ts b/backend/src/subscribers/CategoryMonthSubscriber.ts index 2a80416..49543e8 100644 --- a/backend/src/subscribers/CategoryMonthSubscriber.ts +++ b/backend/src/subscribers/CategoryMonthSubscriber.ts @@ -1,5 +1,12 @@ import { Budget } from '../entities/Budget' -import { EntityManager, EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm' +import { + EntityManager, + EntitySubscriberInterface, + EventSubscriber, + InsertEvent, + RemoveEvent, + UpdateEvent, +} from 'typeorm' import { formatMonthFromDateString, getDateFromString } from '../utils' import { BudgetMonth } from '../entities/BudgetMonth' import { Category } from '../entities/Category' @@ -15,18 +22,15 @@ export class CategoryMonthSubscriber implements EntitySubscriberInterface) { - const categoryMonth = event.entity - const manager = event.manager - + async beforeInsert({ entity: categoryMonth, manager }: InsertEvent) { const prevMonth = getDateFromString(categoryMonth.month).minus({ month: 1 }) const prevCategoryMonth = await manager.findOne(CategoryMonth, { categoryId: categoryMonth.categoryId, month: formatMonthFromDateString(prevMonth.toJSDate()), }) - const category = await event.manager.findOne(Category, { - id: event.entity.categoryId, + const category = await manager.findOne(Category, { + id: categoryMonth.categoryId, }) if (prevCategoryMonth && (category.trackingAccountId || prevCategoryMonth.balance > 0)) { @@ -34,16 +38,29 @@ export class CategoryMonthSubscriber implements EntitySubscriberInterface) { - if (event.entity.balance === 0) { + async afterInsert({ entity: categoryMonth, manager }: InsertEvent) { + if (categoryMonth.balance === 0) { return } - await this.bookkeeping(event.entity as CategoryMonth, event.manager) + await this.bookkeeping(categoryMonth as CategoryMonth, manager) } - async afterUpdate(event: UpdateEvent) { - await this.bookkeeping(event.entity as CategoryMonth, event.manager) + async afterUpdate({ entity: categoryMonth, manager }: UpdateEvent) { + await this.bookkeeping(categoryMonth as CategoryMonth, manager) + } + + /** + * Although the insert / updating of this entity is recursive / cascades, a removal will not + * as we should only be removing category months when a category is being removed. So we are + * removing the category months individually, so no need to cascade any updates from a single one. + */ + async beforeRemove({ entity: categoryMonth, manager }: RemoveEvent) { + const budgetMonth = await manager.findOne(BudgetMonth, categoryMonth.budgetMonthId) + budgetMonth.available += categoryMonth.budgeted + budgetMonth.budgeted -= categoryMonth.budgeted + + await manager.update(BudgetMonth, budgetMonth.id, budgetMonth.getUpdatePayload()) } /** @@ -70,14 +87,11 @@ export class CategoryMonthSubscriber implements EntitySubscriberInterface { return Category } - async afterInsert(event: InsertEvent) { - const category = event.entity - const manager = event.manager - + async afterInsert({ entity: category, manager }: InsertEvent) { // Create a category month for all existing months const budgetMonths = await manager.find(BudgetMonth, { budgetId: category.budgetId }) diff --git a/backend/src/subscribers/TransactionSubscriber.ts b/backend/src/subscribers/TransactionSubscriber.ts index 12a9c42..c38ebd7 100644 --- a/backend/src/subscribers/TransactionSubscriber.ts +++ b/backend/src/subscribers/TransactionSubscriber.ts @@ -21,35 +21,32 @@ export class TransactionSubscriber implements EntitySubscriberInterface) { + async beforeInsert({ entity: transaction, manager }: InsertEvent) { await Promise.all([ - this.checkCreateTransferTransaction(event.entity as Transaction, event.manager), - this.createCategoryMonth(event.entity as Transaction, event.manager), + this.checkCreateTransferTransaction(transaction as Transaction, manager), + this.createCategoryMonth(transaction as Transaction, manager), ]) } - async beforeUpdate(event: UpdateEvent) { + async beforeUpdate({ entity: transaction, manager }: UpdateEvent) { await Promise.all([ - this.createCategoryMonth(event.entity as Transaction, event.manager), - this.updateTransferTransaction(event.entity as Transaction, event.manager), + this.createCategoryMonth(transaction as Transaction, manager), + this.updateTransferTransaction(transaction as Transaction, manager), - this.updateAccountBalanceOnUpdate(event.entity as Transaction, event.manager), - this.bookkeepingOnUpdate(event.entity as Transaction, event.manager), + this.updateAccountBalanceOnUpdate(transaction as Transaction, manager), + this.bookkeepingOnUpdate(transaction as Transaction, manager), ]) } - async afterInsert(event: InsertEvent) { + async afterInsert({ entity: transaction, manager }: InsertEvent) { await Promise.all([ - this.updateAccountBalanceOnAdd(event.entity as Transaction, event.manager), - this.bookkeepingOnAdd(event.entity as Transaction, event.manager), - this.createTransferTransaction(event.entity as Transaction, event.manager), + this.updateAccountBalanceOnAdd(transaction as Transaction, manager), + this.bookkeepingOnAdd(transaction as Transaction, manager), + this.createTransferTransaction(transaction as Transaction, manager), ]) } - async beforeRemove(event: RemoveEvent) { - const transaction = event.entity - const manager = event.manager - + async beforeRemove({ entity: transaction, manager }: RemoveEvent) { if (transaction.transferTransactionId === null) { return } @@ -59,10 +56,10 @@ export class TransactionSubscriber implements EntitySubscriberInterface) { + async afterRemove({ entity: transaction, manager }: RemoveEvent) { await Promise.all([ - this.updateAccountBalanceOnRemove(event.entity as Transaction, event.manager), - this.bookkeepingOnDelete(event.entity as Transaction, event.manager), + this.updateAccountBalanceOnRemove(transaction as Transaction, manager), + this.bookkeepingOnDelete(transaction as Transaction, manager), ]) } diff --git a/frontend/src/api.js b/frontend/src/api.js index 5fad007..56ebc8f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -194,16 +194,26 @@ export default class API { return response.data.data } - static async updateCategory(categoryId, name, order, categoryGroupId, budgetId) { + static async updateCategory(categoryId, name, order, hidden, categoryGroupId, budgetId) { const response = await axios.put(`/api/budgets/${budgetId}/categories/${categoryId}`, { name, order, + hidden, categoryGroupId, }) return response.data.data } + static async deleteCategory(categoryId, newCategoryId, budgetId) { + const response = await axios.delete(`/api/budgets/${budgetId}/categories/${categoryId}`, { + data: { + newCategoryId, + }, + }) + return response.data.data + } + static async fetchBudgetMonth(budgetId, month) { const response = await axios.get(`/api/budgets/${budgetId}/months/${month}`) diff --git a/frontend/src/components/BudgetTable/BudgetTable.js b/frontend/src/components/BudgetTable/BudgetTable.js index b9b3823..197483a 100644 --- a/frontend/src/components/BudgetTable/BudgetTable.js +++ b/frontend/src/components/BudgetTable/BudgetTable.js @@ -12,7 +12,7 @@ import Grid from '@mui/material/Grid' import { equal, isPositive, isNegative, isZero } from 'dinero.js' import { FromAPI, Currency } from '../../utils/Currency' import { useTheme } from '@mui/styles' -import PopupState, { bindTrigger } from 'material-ui-popup-state' +import PopupState, { bindTrigger, bindContextMenu } from 'material-ui-popup-state' import CategoryGroupForm from '../CategoryGroupForm' import CategoryForm from '../CategoryForm' import Tooltip from '@mui/material/Tooltip' @@ -34,8 +34,6 @@ import ExpandMore from '@mui/icons-material/ExpandMore' import ChevronRightIcon from '@mui/icons-material/ChevronRight' import { styled } from '@mui/material/styles' import WarningAmberIcon from '@mui/icons-material/WarningAmber' -import Divider from '@mui/material/Divider' -import BudgetTableHeader from './BudgetTableHeader' const BudgetTableCell = styled(TableCell)(({ theme }) => ({ paddingTop: '4px', @@ -50,13 +48,11 @@ export default function BudgetTable(props) { * Redux block */ const dispatch = useDispatch() - const budgetId = useSelector(state => state.budgets.activeBudgetId) const month = useSelector(state => state.budgets.currentMonth) const availableMonths = useSelector(state => state.budgets.availableMonths) const nextMonth = getDateFromString(month) nextMonth.setMonth(nextMonth.getMonth() + 1) - const nextMonthExists = availableMonths.includes(formatMonthFromDateString(nextMonth)) const currentTheme = useSelector(state => state.app.theme) @@ -105,6 +101,7 @@ export default function BudgetTable(props) { [categoryGroupsSelectors.selectAll, categoriesSelectors.selectAll, selectCategoryMonths], (groups, categories, categoryMonths) => { let retval = [] + const hiddenCategories = [] const underfunded = !budgetMonth.id ? false : isPositive(budgetMonth.underfunded) && !isZero(budgetMonth.underfunded) @@ -132,6 +129,7 @@ export default function BudgetTable(props) { id: category.id, name: category.name, order: category.order, + hidden: false, groupId: group.id, categoryId: category.id, month, @@ -159,20 +157,49 @@ export default function BudgetTable(props) { groupRow.underfunded = underfunded } - groupRow.subRows.push({ - ...categoryMonth, - name: category.name, - order: category.order, - groupId: group.id, - trackingAccountId: category.trackingAccountId, - underfunded, - }) + if (category.hidden === true) { + hiddenCategories.push({ + ...categoryMonth, + name: category.name, + order: category.order, + hidden: category.hidden, + groupId: group.id, + trackingAccountId: category.trackingAccountId, + underfunded, + }) + } else { + groupRow.subRows.push({ + ...categoryMonth, + name: category.name, + order: category.order, + hidden: category.hidden, + groupId: group.id, + trackingAccountId: category.trackingAccountId, + underfunded, + }) + } } groupRow.subRows.sort((a, b) => (a.order < b.order ? -1 : 1)) retval.push(groupRow) }) + if (hiddenCategories.length > 0) { + retval.push({ + id: 0, + name: 'Hidden', + locked: true, + trackingAccountId: true, + order: 9999, + categoryId: 0, + month, + budgeted: hiddenCategories.reduce((total, cat) => total + cat.budgeted, 0), + activity: hiddenCategories.reduce((total, cat) => total + cat.activity, 0), + balance: hiddenCategories.reduce((total, cat) => total + cat.balance, 0), + subRows: hiddenCategories, + }) + } + return retval.sort((a, b) => (a.order < b.order ? -1 : 1)) }, ) @@ -180,7 +207,6 @@ export default function BudgetTable(props) { const budgetData = useSelector(state => selectData(state, month)) const data = useMemo(() => budgetData, [budgetData]) - const openCategoryGroupDialog = props.openCategoryGroupDialog const DragState = { row: -1, dropRow: -1, // drag target @@ -261,6 +287,7 @@ export default function BudgetTable(props) { }), }} {...(!props.row.original.trackingAccountId && bindTrigger(popupState))} + {...(!props.row.original.trackingAccountId && bindContextMenu(popupState))} > {props.row.values.name} @@ -280,6 +307,7 @@ export default function BudgetTable(props) { mode={'edit'} name={props.row.values.name} order={props.row.original.order} + hidden={props.row.original.hidden} categoryId={props.row.original.categoryId} categoryGroupId={props.row.original.groupId} /> @@ -459,6 +487,12 @@ export default function BudgetTable(props) { ) const reorderRows = async (from, to) => { + // This is the auto-generated 'hidden' category group - can't be moved + // and shouldn't be able to move others 'after' it + if (from.id === 0 || to.id === 0) { + return + } + if (from.groupId) { /// updating a category, not a group if (!to.groupId) { @@ -472,7 +506,13 @@ export default function BudgetTable(props) { } await dispatch( - updateCategory({ id: from.categoryId, name: from.name, order: from.order, categoryGroupId: from.groupId }), + updateCategory({ + id: from.categoryId, + name: from.name, + order: from.order, + hidden: from.hidden, + categoryGroupId: from.groupId, + }), ) } else { if (to.groupId) { @@ -512,7 +552,7 @@ export default function BudgetTable(props) { expanded: useMemo( () => data.reduce((result, current, index) => { - result[index] = true + result[index] = data[index].id === 0 ? false : true // Hidden group (id === 0) is collapsed by default return result }, {}), [], @@ -532,10 +572,6 @@ export default function BudgetTable(props) { height: '100vh', }} > - {/* - - */} - diff --git a/frontend/src/components/BudgetTable/BudgetTableAssignedCell.js b/frontend/src/components/BudgetTable/BudgetTableAssignedCell.js index 6575356..b875611 100644 --- a/frontend/src/components/BudgetTable/BudgetTableAssignedCell.js +++ b/frontend/src/components/BudgetTable/BudgetTableAssignedCell.js @@ -45,7 +45,7 @@ export default function BudgetTableAssignedCell({ budgeted, onSubmit }) { } const onChange = e => { - const operators = ['+', '-', '*', '/'] + const operators = ['+', '-', '*', '/', '(', ')'] const value = e.target.value if (operators.includes(value)) { diff --git a/frontend/src/components/CategoryForm.js b/frontend/src/components/CategoryForm.js index 46df811..b3b8c41 100644 --- a/frontend/src/components/CategoryForm.js +++ b/frontend/src/components/CategoryForm.js @@ -1,6 +1,6 @@ import React, { useState } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { createCategory, updateCategory } from '../redux/slices/Categories' +import { createCategory, deleteCategory, updateCategory } from '../redux/slices/Categories' import TextField from '@mui/material/TextField' import Button from '@mui/material/Button' import Select from '@mui/material/Select' @@ -9,7 +9,10 @@ import Popover from '@mui/material/Popover' import Box from '@mui/material/Box' import { bindPopover } from 'material-ui-popup-state/hooks' import Stack from '@mui/material/Stack' -import { categoryGroupsSelectors } from '../redux/slices/CategoryGroups' +import { categoryGroupsSelectors, fetchCategories } from '../redux/slices/CategoryGroups' +import API from '../api' +import DeleteEnvelopeDialog from './DeleteEnvelopeDialog' +import { refreshBudgetCategory } from '../redux/slices/BudgetMonths' export default function NewCategoryDialog(props) { /** @@ -17,14 +20,16 @@ export default function NewCategoryDialog(props) { */ const dispatch = useDispatch() const categoryGroups = useSelector(categoryGroupsSelectors.selectAll) - - console.log(props) + const budgetId = useSelector(state => state.budgets.activeBudgetId) /** * State block */ const [name, setName] = useState(props.name) const [categoryGroup, setCategoryGroup] = useState(props.categoryGroupId) + const availableMonths = useSelector(state => state.budgets.availableMonths) + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const submit = async () => { switch (props.mode) { @@ -42,6 +47,7 @@ export default function NewCategoryDialog(props) { id: props.categoryId, name: name, order: props.order, + hidden: props.hidden, categoryGroupId: categoryGroup, }), ) @@ -51,6 +57,45 @@ export default function NewCategoryDialog(props) { props.popupState.close() } + const openDeleteDialog = () => { + setDeleteDialogOpen(true) + } + + const closeDeleteDialog = () => { + setDeleteDialogOpen(false) + } + + const onCategoryDelete = async newCategory => { + await dispatch( + deleteCategory({ + categoryId: props.categoryId, + newCategoryId: newCategory, + }), + ) + + dispatch(fetchCategories()) + for (const month of availableMonths) { + dispatch(refreshBudgetCategory({ month, categoryId: newCategory })) + } + + setDeleteDialogOpen(false) + props.popupState.close() + } + + const hideCategory = async () => { + await dispatch( + updateCategory({ + id: props.categoryId, + name: props.name, + order: props.order, + hidden: !props.hidden, + categoryGroupId: props.categoryGroupId, + }), + ) + + props.popupState.close() + } + return ( props.popupState.close() }} > + + - - + + + {props.mode === 'edit' && ( + <> + + + + + )} + + + + + diff --git a/frontend/src/components/DeleteEnvelopeDialog.js b/frontend/src/components/DeleteEnvelopeDialog.js new file mode 100644 index 0000000..caa3358 --- /dev/null +++ b/frontend/src/components/DeleteEnvelopeDialog.js @@ -0,0 +1,61 @@ +import React, { useState } from 'react' +import Button from '@mui/material/Button' +import Dialog from '@mui/material/Dialog' +import DialogActions from '@mui/material/DialogActions' +import DialogContent from '@mui/material/DialogContent' +import DialogTitle from '@mui/material/DialogTitle' +import { useSelector } from 'react-redux' +import MenuItem from '@mui/material/MenuItem' +import { categoriesSelectors } from '../redux/slices/Categories' +import Select from '@mui/material/Select' + +export default function DeleteEnvelopeDialog(props) { + /** + * State block + */ + const [newCategory, setNewCategory] = useState('') + const categories = useSelector(categoriesSelectors.selectAll) + + const handleDeleteEvenlope = async () => { + props.submit(newCategory) + props.close() + } + + return ( +
+ false} maxWidth="xs"> + Delete Envelope + + Select a new category for all existing transactions and budgets + + + + + + + + +
+ ) +} diff --git a/frontend/src/redux/slices/Accounts.js b/frontend/src/redux/slices/Accounts.js index e60eb24..9245858 100644 --- a/frontend/src/redux/slices/Accounts.js +++ b/frontend/src/redux/slices/Accounts.js @@ -142,6 +142,17 @@ const accountsSlice = createSlice({ setEditingRow: (state, { payload }) => { state.editingRow = payload }, + + setTransactions: (state, { payload }) => { + for (const transaction of payload.transactions) { + console.log(transaction) + try { + transactionsAdapter.upsertOne(state.entities[transaction.accountId].transactions, transaction) + } catch (e) { + console.log(e) + } + } + }, }, extraReducers: builder => { @@ -212,7 +223,7 @@ const accountsSlice = createSlice({ }, }) -export const { setAccounts, mapIdToAccount, setEditingRow } = accountsSlice.actions +export const { setAccounts, mapIdToAccount, setEditingRow, setTransactions } = accountsSlice.actions export const accountsSelectors = accountsAdapter.getSelectors(state => state.accounts) export const transactionsSelectors = transactionsAdapter.getSelectors(state => state.transactions) diff --git a/frontend/src/redux/slices/Categories.js b/frontend/src/redux/slices/Categories.js index a35e4c9..3681183 100644 --- a/frontend/src/redux/slices/Categories.js +++ b/frontend/src/redux/slices/Categories.js @@ -1,5 +1,6 @@ import { createSlice, createAsyncThunk, createEntityAdapter, createSelector } from '@reduxjs/toolkit' import api from '../../api' +import { setTransactions } from './Accounts' export const createCategory = createAsyncThunk( 'categories/createCategory', @@ -11,13 +12,26 @@ export const createCategory = createAsyncThunk( export const updateCategory = createAsyncThunk( 'categories/updateCategory', - async ({ id, name, order, categoryGroupId }, { getState }) => { + async ({ id, name, order, hidden, categoryGroupId }, { getState }) => { const store = getState() - const category = await api.updateCategory(id, name, order, categoryGroupId, store.budgets.activeBudgetId) + const category = await api.updateCategory(id, name, order, hidden, categoryGroupId, store.budgets.activeBudgetId) return category }, ) +export const deleteCategory = createAsyncThunk( + 'categories/deleteCategory', + async ({ categoryId, newCategoryId }, { getState, dispatch }) => { + const store = getState() + const response = await api.deleteCategory(categoryId, newCategoryId, store.budgets.activeBudgetId) + + dispatch(setTransactions({ transactions: response.transactions })) + // dispatch(setCategoryMonths({ categoryMonths: response.categoryMonths })) + + return response + }, +) + const categoriesAdapter = createEntityAdapter() const categoriesSlice = createSlice({ @@ -45,6 +59,10 @@ const categoriesSlice = createSlice({ builder.addCase(updateCategory.fulfilled, (state, { payload }) => { categoriesAdapter.upsertOne(state, payload) }) + + builder.addCase(deleteCategory.fulfilled, (state, { payload }) => { + return state + }) }, })