From ebdb138d8df8ed1b362ad365a0363ddffcbac6c9 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Tue, 12 Dec 2023 14:34:37 -0700 Subject: [PATCH 01/21] :truck: Rename centre/router to centre/routes. Why? To match the content and export name. --- web/src/modules/centre/{router.ts => routes.ts} | 0 web/src/routes.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename web/src/modules/centre/{router.ts => routes.ts} (100%) diff --git a/web/src/modules/centre/router.ts b/web/src/modules/centre/routes.ts similarity index 100% rename from web/src/modules/centre/router.ts rename to web/src/modules/centre/routes.ts diff --git a/web/src/routes.ts b/web/src/routes.ts index be76acd1..2048b09c 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -2,7 +2,7 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router" import homeRoutes from "@/modules/home/router" import adminstrationRoutes from "@/modules/administration/router" import authenticationRoutes from "@/modules/authentication/router" -import centreRoutes from "@/modules/centre/router" +import centreRoutes from "@/modules/centre/routes" const routes: RouteRecordRaw[] = [ { From 052f3bb6b20c2a20a8c39f4321c79c7be6265112 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Tue, 12 Dec 2023 14:56:04 -0700 Subject: [PATCH 02/21] :construction: Begin work on centre dashboard employees layout. --- ...DashboardEmployeesMonthlyWorksheetPage.vue | 18 ++++ .../pages/CentreDashboardEmployeesPage.vue | 89 ++++++++++++++++++- web/src/modules/centre/routes.ts | 22 +++-- 3 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue diff --git a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue new file mode 100644 index 00000000..09d901f5 --- /dev/null +++ b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue @@ -0,0 +1,18 @@ + + + diff --git a/web/src/modules/centre/pages/CentreDashboardEmployeesPage.vue b/web/src/modules/centre/pages/CentreDashboardEmployeesPage.vue index 9baf5968..6ec780b7 100644 --- a/web/src/modules/centre/pages/CentreDashboardEmployeesPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardEmployeesPage.vue @@ -1 +1,88 @@ - + + + diff --git a/web/src/modules/centre/routes.ts b/web/src/modules/centre/routes.ts index a332955c..fffcc7b5 100644 --- a/web/src/modules/centre/routes.ts +++ b/web/src/modules/centre/routes.ts @@ -94,16 +94,24 @@ const routes: RouteRecordRaw[] = [ { path: "employees", name: "CentreDashboardEmployeesPage", - component: () => import("./pages/CentreDashboardEmployeesPage.vue"), + component: () => import("@/modules/centre/pages/CentreDashboardEmployeesPage.vue"), props: (route) => ({ centreId: parseInt(route.params.centreId as string), + fiscalYearSlug: route.params.fiscalYearSlug, }), - // children: [ - // { - // path: ":month", - // name: "CentreDashboardEmployeesMonthlyWorksheetPage", - // }, - // ], + children: [ + { + path: ":month", + name: "CentreDashboardEmployeesMonthlyWorksheetPage", + component: () => + "@/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue", + props: (route) => ({ + centreId: parseInt(route.params.centreId as string), + fiscalYearSlug: route.params.fiscalYearSlug, + month: route.params.month, + }), + }, + ], }, ], }, From 3b9144bc62050da9f0a55ead5a56d1b6821c1aa8 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Tue, 12 Dec 2023 15:00:38 -0700 Subject: [PATCH 03/21] :recycle: Swap fiscal seeds out to using slug safe values. Why? Easier querying. --- ...able.ts => 2023.12.12T00.25.24.fill-fiscal-periods-table.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename api/src/db/seeds/development/{2023.12.12T00.25.24.fill-funding-periods-table.ts => 2023.12.12T00.25.24.fill-fiscal-periods-table.ts} (94%) diff --git a/api/src/db/seeds/development/2023.12.12T00.25.24.fill-funding-periods-table.ts b/api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts similarity index 94% rename from api/src/db/seeds/development/2023.12.12T00.25.24.fill-funding-periods-table.ts rename to api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts index 6fee7aba..bf07559e 100644 --- a/api/src/db/seeds/development/2023.12.12T00.25.24.fill-funding-periods-table.ts +++ b/api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts @@ -11,7 +11,7 @@ export const up: SeedMigration = async ({ context: { FiscalPeriod } }) => { for (let i = 0; i < 12; i++) { const dateStart = moment(date).startOf("month") const dateEnd = moment(dateStart).endOf("month").milliseconds(0) - const dateName = dateStart.format("MMMM") + const dateName = dateStart.format("MMMM").toLowerCase() const fiscalYear = `${year}-${(year + 1).toString().slice(-2)}` From 326f81eb795580dec1559267c5fa2af2a5806947 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Tue, 12 Dec 2023 16:46:08 -0700 Subject: [PATCH 04/21] :japanese_castle: Avoid rendering route tabs until fiscal years slug is available. --- .../centre/pages/CentreDashboardPage.vue | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/web/src/modules/centre/pages/CentreDashboardPage.vue b/web/src/modules/centre/pages/CentreDashboardPage.vue index 45a37853..0ad5970f 100644 --- a/web/src/modules/centre/pages/CentreDashboardPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardPage.vue @@ -150,36 +150,38 @@ class="mb-5 fill-height" elevation="3" > - - - Summary - - - Worksheets - - - Employees - - + @@ -264,6 +266,7 @@ export default { }, }, methods: { + isEmpty, ...mapActions(useCentreStore, ["selectCentreById", "unselectCentre", "editCentre"]), FormatDate(input: Date | undefined) { return input != null ? FormatDate(input) : "" From 2b8936056ec98cedf08490d34acd9d14070b88df Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Tue, 12 Dec 2023 16:49:07 -0700 Subject: [PATCH 05/21] :construction: Not quite working employee monthly worksheet page. --- ...DashboardEmployeesMonthlyWorksheetPage.vue | 10 ++- .../pages/CentreDashboardEmployeesPage.vue | 75 ++++++++++++------- web/src/modules/centre/routes.ts | 2 +- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue index 09d901f5..3ce43292 100644 --- a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue @@ -1,4 +1,12 @@ - + diff --git a/web/src/modules/centre/routes.ts b/web/src/modules/centre/routes.ts index fffcc7b5..5bde09e6 100644 --- a/web/src/modules/centre/routes.ts +++ b/web/src/modules/centre/routes.ts @@ -104,7 +104,7 @@ const routes: RouteRecordRaw[] = [ path: ":month", name: "CentreDashboardEmployeesMonthlyWorksheetPage", component: () => - "@/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue", + import("@/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue"), props: (route) => ({ centreId: parseInt(route.params.centreId as string), fiscalYearSlug: route.params.fiscalYearSlug, From fb15f7cf7d9fca312c1649abb6844e8b3ec5012b Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Tue, 12 Dec 2023 18:12:38 -0700 Subject: [PATCH 06/21] :construction: Build out skeleton of employee benefits widget. --- .../components/EditEmployeeBenefitWidget.vue | 124 ++++++++++++++++++ ...DashboardEmployeesMonthlyWorksheetPage.vue | 91 ++++++++++++- 2 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 web/src/modules/centre/components/EditEmployeeBenefitWidget.vue diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue new file mode 100644 index 00000000..cd48a1c6 --- /dev/null +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -0,0 +1,124 @@ + + + diff --git a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue index 3ce43292..a5f9af7e 100644 --- a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue @@ -1,15 +1,37 @@ + + From 3cf88e6742a7417e31878c1bb39486cba2adb3a4 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 09:39:16 -0700 Subject: [PATCH 07/21] :butterfly: Add unique constraint on fiscal year + month in fiscal periods table. Why? This make it possible to query safely from the UI and also avoids data corruption. --- ...x-to-fiscal-year-month-on-fiscal-periods-table.ts | 12 ++++++++++++ api/src/models/fiscal-period.ts | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 api/src/db/migrations/2023.12.13T16.09.55.add-unique-index-to-fiscal-year-month-on-fiscal-periods-table.ts diff --git a/api/src/db/migrations/2023.12.13T16.09.55.add-unique-index-to-fiscal-year-month-on-fiscal-periods-table.ts b/api/src/db/migrations/2023.12.13T16.09.55.add-unique-index-to-fiscal-year-month-on-fiscal-periods-table.ts new file mode 100644 index 00000000..55fb80b8 --- /dev/null +++ b/api/src/db/migrations/2023.12.13T16.09.55.add-unique-index-to-fiscal-year-month-on-fiscal-periods-table.ts @@ -0,0 +1,12 @@ +import type { Migration } from "@/db/umzug" + +export const up: Migration = async ({ context: queryInterface }) => { + await queryInterface.addIndex("fiscal_periods", { + fields: ["fiscal_year", "month"], + unique: true, + }) +} + +export const down: Migration = async ({ context: queryInterface }) => { + await queryInterface.removeIndex("fiscal_periods", ["fiscal_year", "month"]) +} diff --git a/api/src/models/fiscal-period.ts b/api/src/models/fiscal-period.ts index cf7438fe..b42ff494 100644 --- a/api/src/models/fiscal-period.ts +++ b/api/src/models/fiscal-period.ts @@ -58,6 +58,12 @@ FiscalPeriod.init( }, { sequelize, + indexes: [ + { + unique: true, + fields: ["fiscal_year", "month"], // not sure if these need to be snake_case? + }, + ], } ) From ab06c15223f5e25ca929bdabc4cd8ea50ecb848a Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 10:11:32 -0700 Subject: [PATCH 08/21] :butterfly: Fix typo in foreign key in employee benefits table. --- ...-foriegn-key-in-employee-benefits-table.ts | 64 +++++++++++++++++++ api/src/models/employee-benefit.ts | 4 +- .../components/EditEmployeeBenefitWidget.vue | 4 +- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 api/src/db/migrations/2023.12.13T16.09.56.fix-typo-in-foriegn-key-in-employee-benefits-table.ts diff --git a/api/src/db/migrations/2023.12.13T16.09.56.fix-typo-in-foriegn-key-in-employee-benefits-table.ts b/api/src/db/migrations/2023.12.13T16.09.56.fix-typo-in-foriegn-key-in-employee-benefits-table.ts new file mode 100644 index 00000000..5378b522 --- /dev/null +++ b/api/src/db/migrations/2023.12.13T16.09.56.fix-typo-in-foriegn-key-in-employee-benefits-table.ts @@ -0,0 +1,64 @@ +import type { Migration } from "@/db/umzug" + +type ForeignKeyReference = { + constraint_name: string // "FK__employee___fislc__22401542" + constraintName: string // "FK__employee___fislc__22401542" + constraintCatalog: string // "elcc_development" + constraintSchema: string // "dbo" + tableName: string // "employee_benefits" + tableSchema: string // "dbo" + tableCatalog: string // "elcc_development" + columnName: string // "fislcal_period_id" + referencedTableSchema: string // "dbo" + referencedCatalog: string // "elcc_development" + referencedTableName: string // "fiscal_periods" + referencedColumnName: string // "id" +} + +export const up: Migration = async ({ context: queryInterface }) => { + const references = (await queryInterface.getForeignKeyReferencesForTable( + "employee_benefits" + )) as ForeignKeyReference[] + + const foreignKey = references.find((reference) => reference.columnName === "fislcal_period_id") + if (foreignKey !== undefined) { + await queryInterface.removeConstraint("employee_benefits", foreignKey.constraintName) + } + + await queryInterface.renameColumn("employee_benefits", "fislcal_period_id", "fiscal_period_id") + + await queryInterface.addConstraint("employee_benefits", { + fields: ["fiscal_period_id"], + type: "foreign key", + references: { + table: "fiscal_periods", + field: "id", + }, + onDelete: "", // RESTRICT is default for MSSQL, so you can't set it; and must use and empty string + onUpdate: "", + }) +} + +export const down: Migration = async ({ context: queryInterface }) => { + const references = (await queryInterface.getForeignKeyReferencesForTable( + "employee_benefits" + )) as ForeignKeyReference[] + + const foreignKey = references.find((reference) => reference.columnName === "fiscal_period_id") + if (foreignKey !== undefined) { + await queryInterface.removeConstraint("employee_benefits", foreignKey.constraintName) + } + + await queryInterface.renameColumn("employee_benefits", "fiscal_period_id", "fislcal_period_id") + + await queryInterface.addConstraint("employee_benefits", { + fields: ["fislcal_period_id"], + type: "foreign key", + references: { + table: "fiscal_periods", + field: "id", + }, + onDelete: "", // RESTRICT is default for MSSQL, so you can't set it; and must use and empty string + onUpdate: "", + }) +} diff --git a/api/src/models/employee-benefit.ts b/api/src/models/employee-benefit.ts index 06c866c9..95a446f1 100644 --- a/api/src/models/employee-benefit.ts +++ b/api/src/models/employee-benefit.ts @@ -23,7 +23,7 @@ export class EmployeeBenefit extends Model< > { declare id: CreationOptional declare centreId: ForeignKey - declare fislcalPeriodId: ForeignKey + declare fiscalPeriodId: ForeignKey declare grossPayrollMonthlyActual: number declare grossPayrollMonthlyEstimated: number declare costCapPercentage: number @@ -79,7 +79,7 @@ EmployeeBenefit.init( key: "id", }, }, - fislcalPeriodId: { + fiscalPeriodId: { type: DataTypes.INTEGER, allowNull: false, references: { diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue index cd48a1c6..924f193b 100644 --- a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -79,7 +79,7 @@ import { ref } from "vue" type EmployeeBenefit = { id: number centreId: number - fislcalPeriodId: number + fiscalPeriodId: number grossPayrollMonthlyActual: number grossPayrollMonthlyEstimated: number costCapPercentage: number @@ -105,7 +105,7 @@ defineProps({ const fakeDate = { id: 979, centreId: 33, - fislcalPeriodId: 12, + fiscalPeriodId: 12, grossPayrollMonthlyActual: 6466, grossPayrollMonthlyEstimated: 4015, costCapPercentage: 0.08, From f256c16707d74e5e937e3f44282432dba13d3146 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 10:14:50 -0700 Subject: [PATCH 09/21] :butterfly: Add unique constraint on centre_id fiscal_period_id in employee benefits table. Why? This makes it possible to safely query using said columns and also avoids data corruption. --- ...id-fiscal-period-id-on-employee-benefits-table.ts | 12 ++++++++++++ api/src/models/employee-benefit.ts | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 api/src/db/migrations/2023.12.13T16.09.57.add-unique-index-to-centre-id-fiscal-period-id-on-employee-benefits-table.ts diff --git a/api/src/db/migrations/2023.12.13T16.09.57.add-unique-index-to-centre-id-fiscal-period-id-on-employee-benefits-table.ts b/api/src/db/migrations/2023.12.13T16.09.57.add-unique-index-to-centre-id-fiscal-period-id-on-employee-benefits-table.ts new file mode 100644 index 00000000..d70625bf --- /dev/null +++ b/api/src/db/migrations/2023.12.13T16.09.57.add-unique-index-to-centre-id-fiscal-period-id-on-employee-benefits-table.ts @@ -0,0 +1,12 @@ +import type { Migration } from "@/db/umzug" + +export const up: Migration = async ({ context: queryInterface }) => { + await queryInterface.addIndex("employee_benefits", { + fields: ["centre_id", "fiscal_period_id"], + unique: true, + }) +} + +export const down: Migration = async ({ context: queryInterface }) => { + await queryInterface.removeIndex("employee_benefits", ["centre_id", "fiscal_period_id"]) +} diff --git a/api/src/models/employee-benefit.ts b/api/src/models/employee-benefit.ts index 95a446f1..0d411679 100644 --- a/api/src/models/employee-benefit.ts +++ b/api/src/models/employee-benefit.ts @@ -128,6 +128,12 @@ EmployeeBenefit.init( }, { sequelize, + indexes: [ + { + unique: true, + fields: ["centre_id", "fiscal_period_id"], // not sure if these need to be snake_case? + }, + ], } ) From 9fa253bee6ed5a61fc90d033e9b3b9dd29c340a3 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 10:47:42 -0700 Subject: [PATCH 10/21] :label: Fix typing in funding submission line json serializer. --- .../funding-submission-line-json-serializer.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/api/src/serializers/funding-submission-line-json-serializer.ts b/api/src/serializers/funding-submission-line-json-serializer.ts index 65502dc6..dfa634c6 100644 --- a/api/src/serializers/funding-submission-line-json-serializer.ts +++ b/api/src/serializers/funding-submission-line-json-serializer.ts @@ -1,6 +1,6 @@ import { sortBy, uniq, pick, omit } from "lodash" import moment from "moment" -import { FundingSubmissionLineJson } from "@/models" +import { FundingLineValue, FundingSubmissionLineJson } from "@/models" import BaseSerializer from "@/serializers/base-serializer" @@ -45,7 +45,7 @@ export class FundingSubmissionLineJsonSerializer extends BaseSerializer() + const groups = [] const years = uniq(worksheets.map((m) => m.fiscalYear)) for (const fiscalYear of years) { @@ -60,12 +60,18 @@ export class FundingSubmissionLineJsonSerializer extends BaseSerializer month == m.dateName)[0] const sections = uniq(monthSheets.lines.map((w) => w.sectionName)) - const monthRow = { + const monthRow: { + id: number + fiscalYear: string + month: string + year: string + sections: { sectionName: string; lines: FundingLineValue[] }[] + } = { id: monthSheets.id, fiscalYear, month, year: moment.utc(monthSheets.dateStart).format("YYYY"), - sections: new Array(), + sections: [], } for (const section of sections) { From 2e0e6fef759bc3bc77a873331a4d87ca825a2435 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 11:01:15 -0700 Subject: [PATCH 11/21] :sparkles: Add minimal employee benefits api and wiring. --- .../employee-benefits-controller.ts | 94 +++++++++++++++++++ api/src/controllers/index.ts | 1 + api/src/routes/api-router.ts | 12 +++ .../employee-benefit-serializer.ts | 48 ++++++++++ api/src/serializers/index.ts | 9 +- web/src/api/employee-benefits-api.ts | 53 +++++++++++ .../components/EditEmployeeBenefitWidget.vue | 15 +-- 7 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 api/src/controllers/employee-benefits-controller.ts create mode 100644 api/src/serializers/employee-benefit-serializer.ts create mode 100644 web/src/api/employee-benefits-api.ts diff --git a/api/src/controllers/employee-benefits-controller.ts b/api/src/controllers/employee-benefits-controller.ts new file mode 100644 index 00000000..066a578b --- /dev/null +++ b/api/src/controllers/employee-benefits-controller.ts @@ -0,0 +1,94 @@ +import { isNil } from "lodash" +import { WhereOptions } from "sequelize" + +import BaseController from "./base-controller" + +import { EmployeeBenefit } from "@/models" +import { EmployeeBenefitSerializer } from "@/serializers" + +export class EmployeeBenefitsController extends BaseController { + index() { + const where = this.query.where as WhereOptions + return EmployeeBenefit.findAll({ + where, + order: ["dateStart"], + }) + .then((employeeBenefits) => { + const serializedEmployeeBenefits = EmployeeBenefitSerializer.asTable(employeeBenefits) + return this.response.json({ + employeeBenefits: serializedEmployeeBenefits, + }) + }) + .catch((error) => { + return this.response + .status(400) + .json({ message: `Invalid query for employee benefits: ${error}` }) + }) + } + + async show() { + const employeeBenefit = await this.loadEmployeeBenefit() + if (isNil(employeeBenefit)) + return this.response.status(404).json({ message: "employee benefit not found." }) + + const serializedemployeeBenefit = EmployeeBenefitSerializer.asDetailed(employeeBenefit) + return this.response.json({ + employeeBenefit: serializedemployeeBenefit, + }) + } + + async create() { + return EmployeeBenefit.create(this.request.body) + .then((employeeBenefit) => { + return this.response.status(201).json({ employeeBenefit }) + }) + .catch((error) => { + return this.response + .status(422) + .json({ message: `employee benefit creation failed: ${error}` }) + }) + } + + async update() { + const employeeBenefit = await this.loadEmployeeBenefit() + if (isNil(employeeBenefit)) + return this.response.status(404).json({ message: "employee benefit not found." }) + + return employeeBenefit + .update(this.request.body) + .then((employeeBenefit) => { + const serializedemployeeBenefit = EmployeeBenefitSerializer.asDetailed(employeeBenefit) + return this.response.json({ + employeeBenefit: serializedemployeeBenefit, + }) + }) + .catch((error) => { + return this.response + .status(422) + .json({ message: `employee benefit update failed: ${error}` }) + }) + } + + async destroy() { + const employeeBenefit = await this.loadEmployeeBenefit() + if (isNil(employeeBenefit)) + return this.response.status(404).json({ message: "employee benefit not found." }) + + return employeeBenefit + .destroy() + .then(() => { + return this.response.status(204).end() + }) + .catch((error) => { + return this.response + .status(422) + .json({ message: `employee benefit deletion failed: ${error}` }) + }) + } + + private loadEmployeeBenefit(): Promise { + return EmployeeBenefit.findByPk(this.params.employeeBenefitId) + } +} + +export default EmployeeBenefitsController diff --git a/api/src/controllers/index.ts b/api/src/controllers/index.ts index e40736cb..5553a084 100644 --- a/api/src/controllers/index.ts +++ b/api/src/controllers/index.ts @@ -1,3 +1,4 @@ +export { EmployeeBenefitsController } from "./employee-benefits-controller" export { FiscalPeriodsController } from "./fiscal-periods-controller" export { FundingSubmissionLineJsonsController } from "./funding-submission-line-jsons-controller" export { PaymentsController } from "./payments-controller" diff --git a/api/src/routes/api-router.ts b/api/src/routes/api-router.ts index 406fd1d1..ecb4733c 100644 --- a/api/src/routes/api-router.ts +++ b/api/src/routes/api-router.ts @@ -2,6 +2,7 @@ import { Router, type Request, type Response } from "express" import { checkJwt, autheticateAndLoadUser } from "@/middleware/authz.middleware" import { + EmployeeBenefitsController, FiscalPeriodsController, FundingSubmissionLineJsonsController, PaymentsController, @@ -32,8 +33,19 @@ apiRouter.delete( "/api/funding-submission-line-jsons/:fundingSubmissionLineJsonId", FundingSubmissionLineJsonsController.destroy ) + apiRouter.get("/api/fiscal-periods", FiscalPeriodsController.index) +apiRouter + .route("/api/employee-benefits") + .get(EmployeeBenefitsController.index) + .post(EmployeeBenefitsController.create) +apiRouter + .route("/api/employee-benefits/:employeeBenefitId") + .get(EmployeeBenefitsController.show) + .patch(EmployeeBenefitsController.update) + .delete(EmployeeBenefitsController.destroy) + apiRouter.use("/api", (req: Request, res: Response) => { return res.status(404).json({ error: `Api endpoint "${req.originalUrl}" not found` }) }) diff --git a/api/src/serializers/employee-benefit-serializer.ts b/api/src/serializers/employee-benefit-serializer.ts new file mode 100644 index 00000000..e78c5cbb --- /dev/null +++ b/api/src/serializers/employee-benefit-serializer.ts @@ -0,0 +1,48 @@ +import { pick } from "lodash" +import { EmployeeBenefit } from "@/models" + +import BaseSerializer from "@/serializers/base-serializer" + +export class EmployeeBenefitSerializer extends BaseSerializer { + static asTable(employeeBenefits: EmployeeBenefit[]) { + return employeeBenefits.map((employeeBenefit) => { + return { + ...pick(employeeBenefit, [ + "id", + "centreId", + "fiscalPeriodId", + "grossPayrollMonthlyActual", + "grossPayrollMonthlyEstimated", + "costCapPercentage", + "employeeCostActual", + "employeeCostEstimated", + "employerCostActual", + "employerCostEstimated", + "createdAt", + "updatedAt", + ]), + } + }) + } + + static asDetailed(employeeBenefit: EmployeeBenefit) { + return { + ...pick(employeeBenefit, [ + "id", + "centreId", + "fiscalPeriodId", + "grossPayrollMonthlyActual", + "grossPayrollMonthlyEstimated", + "costCapPercentage", + "employeeCostActual", + "employeeCostEstimated", + "employerCostActual", + "employerCostEstimated", + "createdAt", + "updatedAt", + ]), + } + } +} + +export default EmployeeBenefitSerializer diff --git a/api/src/serializers/index.ts b/api/src/serializers/index.ts index 088348e0..3e6bfdb2 100644 --- a/api/src/serializers/index.ts +++ b/api/src/serializers/index.ts @@ -1,5 +1,4 @@ -export * from "@/serializers/funding-submission-line-json-serializer" -export * from "@/serializers/funding-submission-line-serializer" -export * from "@/serializers/user-serializer" - -export default undefined +export { EmployeeBenefitSerializer } from "@/serializers/employee-benefit-serializer" +export { FundingSubmissionLineJsonSerializer } from "@/serializers/funding-submission-line-json-serializer" +export { FundingSubmissionLineSerializer } from "@/serializers/funding-submission-line-serializer" +export { UserSerializer } from "@/serializers/user-serializer" diff --git a/web/src/api/employee-benefits-api.ts b/web/src/api/employee-benefits-api.ts new file mode 100644 index 00000000..de85e804 --- /dev/null +++ b/web/src/api/employee-benefits-api.ts @@ -0,0 +1,53 @@ +import http from "@/api/http-client" + +export type EmployeeBenefit = { + id: number + centreId: number + fiscalPeriodId: number + grossPayrollMonthlyActual: number + grossPayrollMonthlyEstimated: number + costCapPercentage: number + employeeCostActual: number + employeeCostEstimated: number + employerCostActual: number + employerCostEstimated: number + createdAt: Date + updatedAt: Date +} + +export type Params = { + where?: { + centreId?: number + fiscalPeriodId?: string + } + page?: number + perPage?: number + otherParams?: any +} + +export const employeeBenefitsApi = { + list(params: Params = {}): Promise<{ + employeeBenefits: EmployeeBenefit[] + }> { + return http.get("/api/employee-benefits", { params }).then(({ data }) => data) + }, + get(employeeBenefitId: number) { + return http.get(`/api/employee-benefits/${employeeBenefitId}`).then(({ data }) => data) + }, + create(attributes: Partial): Promise<{ employeeBenefit: EmployeeBenefit }> { + return http.post("/api/employee-benefits", attributes).then(({ data }) => data) + }, + update( + employeeBenefitId: number, + attributes: any + ): Promise<{ employeeBenefit: EmployeeBenefit }> { + return http + .patch(`/api/employee-benefits/${employeeBenefitId}`, attributes) + .then(({ data }) => data) + }, + delete(employeeBenefitId: number): Promise { + return http.delete(`/api/employee-benefits/${employeeBenefitId}`).then(({ data }) => data) + }, +} + +export default employeeBenefitsApi diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue index 924f193b..eeccdbd6 100644 --- a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -76,20 +76,7 @@ diff --git a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue index a5f9af7e..d35a56c4 100644 --- a/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardEmployeesMonthlyWorksheetPage.vue @@ -67,7 +67,7 @@ async function fetchFiscalPeriods(fiscalYear: string, month: string) { }, }) - if (newFiscalPeriods.length === 0) { + if (isEmpty(newFiscalPeriods)) { throw new Error(`No fiscal periods found for ${fiscalYear} ${month}`) } else if (newFiscalPeriods.length > 1) { throw new Error(`Multiple fiscal periods found for ${fiscalYear} ${month}`) From ee6ce0b722eb5521142dc654ec4a21db1b7cac2b Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 17:07:14 -0700 Subject: [PATCH 14/21] :fire: Clean up unused import. --- .../centre/pages/CentreDashboardSummaryReconciliationPage.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/modules/centre/pages/CentreDashboardSummaryReconciliationPage.vue b/web/src/modules/centre/pages/CentreDashboardSummaryReconciliationPage.vue index 63a1af38..5bd7b58e 100644 --- a/web/src/modules/centre/pages/CentreDashboardSummaryReconciliationPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardSummaryReconciliationPage.vue @@ -87,7 +87,6 @@ import { isEmpty, sumBy } from "lodash" import useFundingSubmissionLineJsonsStore from "@/store/funding-submission-line-jsons" import usePaymentsStore from "@/store/payments" -import { useSubmissionLinesStore } from "@/modules/submission-lines/store" import { formatMoney, centsToDollars, dollarsToCents } from "@/utils/format-money" import { interleaveArrays } from "@/utils/interleave-arrays" From 2dfbe6efff501be42c65556aa885fb29a90f0416 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 17:07:30 -0700 Subject: [PATCH 15/21] :sparkles: Implement employee benefits. --- .../components/EditEmployeeBenefitWidget.vue | 190 +++++++++++++++--- 1 file changed, 165 insertions(+), 25 deletions(-) diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue index ec4516c3..9a9c2219 100644 --- a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -19,7 +19,9 @@ density="compact" variant="underlined" hide-details - @update:model-value="updateEmployeeBenefit('grossPayrollMonthlyEstimated', $event)" + @update:model-value=" + updateEmployeeBenefitCurrencyValue('grossPayrollMonthlyEstimated', $event) + " /> @@ -31,7 +33,9 @@ density="compact" variant="underlined" hide-details - @update:model-value="updateEmployeeBenefit('grossPayrollMonthlyActual', $event)" + @update:model-value=" + updateEmployeeBenefitCurrencyValue('grossPayrollMonthlyActual', $event) + " /> @@ -39,8 +43,8 @@ {{ employeeBenefit.costCapPercentage * 100 }}% of above Employee Cost - + - + Employer Cost - + - + Section Total - {{ - Math.min( - employeeBenefit.employeeCostEstimated + employeeBenefit.employerCostEstimated, - employeeBenefit.grossPayrollMonthlyEstimated * employeeBenefit.costCapPercentage - ) - }} + - {{ - Math.min( - employeeBenefit.employeeCostActual + employeeBenefit.employerCostActual, - employeeBenefit.grossPayrollMonthlyActual * employeeBenefit.costCapPercentage - ) - }} + + + + + + + + + + Save + @@ -134,18 +196,34 @@ const props = defineProps({ const employeeBenefit = ref() const isLoading = ref(true) -const estimatedMonthlyBenefitCostCap = computed(() => { +const monthlyBenefitCostCapEstimated = computed(() => { if (isUndefined(employeeBenefit.value)) return 0 return ( employeeBenefit.value.grossPayrollMonthlyEstimated * employeeBenefit.value.costCapPercentage ) }) -const actualMonthlyBenefitCostCap = computed(() => { +const monthlyBenefitCostCapActual = computed(() => { if (isUndefined(employeeBenefit.value)) return 0 return employeeBenefit.value.grossPayrollMonthlyActual * employeeBenefit.value.costCapPercentage }) +const minimumTotalCostEstimated = computed(() => { + if (isUndefined(employeeBenefit.value)) return 0 + + return Math.min( + employeeBenefit.value.employeeCostEstimated + employeeBenefit.value.employerCostEstimated, + monthlyBenefitCostCapEstimated.value + ) +}) +const minimumTotalCostActual = computed(() => { + if (isUndefined(employeeBenefit.value)) return 0 + + return Math.min( + employeeBenefit.value.employeeCostActual + employeeBenefit.value.employerCostActual, + monthlyBenefitCostCapActual.value + ) +}) function buildEmployeeBenefit(attributes: Partial): NonPersistedEmployeeBenefit { return { @@ -199,7 +277,7 @@ watch<[number, number], true>( { immediate: true } ) -function updateEmployeeBenefit( +function updateEmployeeBenefitCurrencyValue( attribute: keyof (EmployeeBenefit | NonPersistedEmployeeBenefit), newValue: any ) { @@ -212,4 +290,66 @@ function updateEmployeeBenefit( employeeBenefit.value[attribute] = newValueNumber } } + +function isPersisted( + employeeBenefit: EmployeeBenefit | NonPersistedEmployeeBenefit +): employeeBenefit is EmployeeBenefit { + return "id" in employeeBenefit && !isUndefined(employeeBenefit.id) +} + +async function save() { + if (isUndefined(employeeBenefit.value)) return + + if (isPersisted(employeeBenefit.value)) { + await update(employeeBenefit.value.id, employeeBenefit.value) + } else { + await create(employeeBenefit.value) + } +} + +async function update(employeeBenefitId: number, employeeBenefitAttributes: EmployeeBenefit) { + isLoading.value = true + try { + const { employeeBenefit: newEmployeeBenefit } = await employeeBenefitsApi.update( + employeeBenefitId, + employeeBenefitAttributes + ) + + employeeBenefit.value = newEmployeeBenefit + notificationStore.notify({ + text: "Employee benefit saved", + variant: "success", + }) + } catch (error) { + console.error(error) + notificationStore.notify({ + text: `Failed to save employee benefit: ${error}`, + variant: "error", + }) + } finally { + isLoading.value = false + } +} + +async function create(employeeBenefitAttributes: NonPersistedEmployeeBenefit) { + isLoading.value = true + try { + const { employeeBenefit: newEmployeeBenefit } = + await employeeBenefitsApi.create(employeeBenefitAttributes) + + employeeBenefit.value = newEmployeeBenefit + notificationStore.notify({ + text: "Employee benefit created", + variant: "success", + }) + } catch (error) { + console.error(error) + notificationStore.notify({ + text: `Failed to create employee benefit: ${error}`, + variant: "error", + }) + } finally { + isLoading.value = false + } +} From 05607d1a7fc260af68d744a4dacb4b51948d4fe6 Mon Sep 17 00:00:00 2001 From: Marlen Brunner Date: Wed, 13 Dec 2023 17:33:45 -0700 Subject: [PATCH 16/21] :cherry_blossom: Add tooltip about how total is calculated. --- .../components/EditEmployeeBenefitWidget.vue | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue index 9a9c2219..eb13f111 100644 --- a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -124,7 +124,22 @@ - Section Total + + Section Total + + + + Totaling the lesser of either {{ employeeBenefit.costCapPercentage * 100 }}% of Gross + Payroll or Employee Cost plus Employer Cost + + + Date: Thu, 14 Dec 2023 09:00:08 -0700 Subject: [PATCH 17/21] :sparkles: Add ability to edit the cost cap percentage. --- .../components/EditEmployeeBenefitWidget.vue | 52 +++++++++++++++++-- web/src/plugins/vuetify.ts | 1 + 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue index eb13f111..0dbb8ca2 100644 --- a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -40,7 +40,31 @@ - {{ employeeBenefit.costCapPercentage * 100 }}% of above + + {{ costCapPercentage }}% of above + + + + + - Totaling the lesser of either {{ employeeBenefit.costCapPercentage * 100 }}% of Gross - Payroll or Employee Cost plus Employer Cost + Totaling the lesser of either {{ costCapPercentage }}% of Gross Payroll or Employee + Cost plus Employer Cost @@ -186,7 +210,7 @@