diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index c0b2cd7..bbdf16a 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -195,6 +195,19 @@ export async function getMembershipStatisticsById(req: Request, res: Response): res.json(MembershipFactory.mapToBaseStatistics(member)); } +/** + * @description get member total statistics by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMembershipTotalStatisticsById(req: Request, res: Response): Promise { + const memberId = req.params.memberId; + let member = await MembershipService.getTotalStatisticsById(memberId); + + res.json(MembershipFactory.mapToSingleTotalStatistic(member)); +} + /** * @description get membership by member and record * @param req {Request} Express req object diff --git a/src/data-source.ts b/src/data-source.ts index bb96819..f436bf4 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -34,7 +34,7 @@ import { query } from "./entity/configuration/query"; import { memberView } from "./views/memberView"; import { memberExecutivePositionsView } from "./views/memberExecutivePositionView"; import { memberQualificationsView } from "./views/memberQualificationsView"; -import { membershipView } from "./views/membershipsView"; +import { membershipTotalView, membershipView } from "./views/membershipsView"; import { template } from "./entity/configuration/template"; import { templateUsage } from "./entity/configuration/templateUsage"; import { newsletter } from "./entity/club/newsletter/newsletter"; @@ -57,6 +57,7 @@ import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberC import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine"; import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set"; import { AddDateToNewsletter1748509435932 } from "./migrations/1748509435932-addDateToNewsletter"; +import { AddMembershipTotalView1748607842929 } from "./migrations/1748607842929-addMembershipTotalView"; configCheck(); @@ -109,6 +110,7 @@ const dataSource = new DataSource({ memberExecutivePositionsView, memberQualificationsView, membershipView, + membershipTotalView, webapi, webapiPermission, setting, @@ -125,6 +127,7 @@ const dataSource = new DataSource({ MemberCreatedAt1746006549262, UserLoginRoutine1746252454922, AddDateToNewsletter1748509435932, + AddMembershipTotalView1748607842929, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/factory/admin/club/member/membership.ts b/src/factory/admin/club/member/membership.ts index b71d56c..a57063e 100644 --- a/src/factory/admin/club/member/membership.ts +++ b/src/factory/admin/club/member/membership.ts @@ -1,9 +1,10 @@ import { membership } from "../../../../entity/club/member/membership"; import { MembershipStatisticsViewModel, + MembershipTotalStatisticsViewModel, MembershipViewModel, } from "../../../../viewmodel/admin/club/member/membership.models"; -import { membershipView } from "../../../../views/membershipsView"; +import { membershipTotalView, membershipView } from "../../../../views/membershipsView"; import DateMappingHelper from "./dateMappingHelper"; export default abstract class MembershipFactory { @@ -53,6 +54,25 @@ export default abstract class MembershipFactory { }; } + /** + * @description map view record to MembershipTotalStatisticsViewModel + * @param {membershipTotalView} record + * @returns {MembershipTotalStatisticsViewModel} + */ + public static mapToSingleTotalStatistic(record: membershipTotalView): MembershipTotalStatisticsViewModel { + return { + durationInDays: record.durationInDays, + durationInYears: record.durationInYears, + exactDuration: DateMappingHelper.mapDate(record.exactDuration), + memberId: record.memberId, + memberSalutation: record.memberSalutation, + memberFirstname: record.memberFirstname, + memberLastname: record.memberLastname, + memberNameaffix: record.memberNameaffix, + memberBirthdate: record.memberBirthdate, + }; + } + /** * @description map records to MembershipStatisticsViewModel * @param {Array} records diff --git a/src/helpers/dynamicQueryBuilder.ts b/src/helpers/dynamicQueryBuilder.ts index 3c6d4e6..ef9e97e 100644 --- a/src/helpers/dynamicQueryBuilder.ts +++ b/src/helpers/dynamicQueryBuilder.ts @@ -22,6 +22,7 @@ export default abstract class DynamicQueryBuilder { "memberExecutivePositionsView", "memberQualificationsView", "membershipView", + "membershipTotalView", ]; public static getTableMeta(tableName: string): TableMeta { diff --git a/src/migrations/1748607842929-addMembershipTotalView.ts b/src/migrations/1748607842929-addMembershipTotalView.ts new file mode 100644 index 0000000..97cfea0 --- /dev/null +++ b/src/migrations/1748607842929-addMembershipTotalView.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { DB_TYPE } from "../env.defaults"; +import { + membership_total_view_mysql, + membership_total_view_postgres, + membership_total_view_sqlite, +} from "./baseSchemaTables/member"; + +export class AddMembershipTotalView1748607842929 implements MigrationInterface { + name = "AddMembershipTotalView1748607842929"; + + public async up(queryRunner: QueryRunner): Promise { + if (DB_TYPE == "postgres") await queryRunner.createView(membership_total_view_postgres, true); + else if (DB_TYPE == "mysql") await queryRunner.createView(membership_total_view_mysql, true); + else if (DB_TYPE == "sqlite") await queryRunner.createView(membership_total_view_sqlite, true); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropView("membership_total_view"); + } +} diff --git a/src/migrations/baseSchemaTables/member.ts b/src/migrations/baseSchemaTables/member.ts index c5f304b..8ef428c 100644 --- a/src/migrations/baseSchemaTables/member.ts +++ b/src/migrations/baseSchemaTables/member.ts @@ -529,3 +529,76 @@ export const membership_view_sqlite = new View({ GROUP BY status.id, member.id, salutation.id `, }); + +export const membership_total_view_mysql = new View({ + name: "membership_total_view", + expression: ` + SELECT + \`member\`.\`id\` AS \`memberId\`, + \`member\`.\`firstname\` AS \`memberFirstname\`, + \`member\`.\`lastname\` AS \`memberLastname\`, + \`member\`.\`nameaffix\` AS \`memberNameaffix\`, + \`member\`.\`birthdate\` AS \`memberBirthdate\`, + \`salutation\`.\`salutation\` AS \`memberSalutation\`, + SUM(DATEDIFF(COALESCE(\`membership\`.\`end\`, CURDATE()), \`membership\`.\`start\`)) AS \`durationInDays\`, + SUM(TIMESTAMPDIFF(YEAR, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE()))) AS \`durationInYears\`, + CONCAT( + SUM(FLOOR(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())) / 365.25)), + ' years ', + SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 12))), + ' months ', + SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 30))), + ' days' + ) AS \`exactDuration\` + FROM \`membership\` \`membership\` + LEFT JOIN \`membership_status\` \`status\` ON \`status\`.\`id\`=\`membership\`.\`statusId\` + LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`membership\`.\`memberId\` + LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\` + GROUP BY \`member\`.\`id\`, \`salutation\`.\`id\` + `, +}); + +export const membership_total_view_postgres = new View({ + name: "membership_total_view", + expression: ` + SELECT + "member"."id" AS "memberId", + "member"."firstname" AS "memberFirstname", + "member"."lastname" AS "memberLastname", + "member"."nameaffix" AS "memberNameaffix", + "member"."birthdate" AS "memberBirthdate", + "salutation"."salutation" AS "memberSalutation", + SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") AS "durationInDays", + SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start"))) AS "durationInYears", + SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")) AS "exactDuration" + FROM "membership" "membership" + LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId" + LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId" + LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId" + GROUP BY "member"."id", "salutation"."id" + `, +}); + +export const membership_total_view_sqlite = new View({ + name: "membership_total_view", + expression: ` + SELECT + member.id AS memberId, + member.firstname AS memberFirstname, + member.lastname AS memberLastname, + member.nameaffix AS memberNameaffix, + member.birthdate AS memberBirthdate, + salutation.salutation AS memberSalutation, + SUM(JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) AS durationInDays, + SUM(FLOOR((JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) / 365.25)) AS durationInYears, + SUM((strftime('%Y', COALESCE(membership.end, DATE('now'))) - strftime('%Y', membership.start))) || ' years ' || + SUM((strftime('%m', COALESCE(membership.end, DATE('now'))) - strftime('%m', membership.start))) || ' months ' || + SUM((strftime('%d', COALESCE(membership.end, DATE('now'))) - strftime('%d', membership.start))) || ' days' + AS exactDuration + FROM membership membership + LEFT JOIN membership_status status ON status.id=membership.statusId + LEFT JOIN member member ON member.id=membership.memberId + LEFT JOIN salutation salutation ON salutation.id=member.salutationId + GROUP BY member.id, salutation.id + `, +}); diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index 804b595..a2b653a 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -26,6 +26,7 @@ import { getMembershipByMemberAndRecord, getMembershipsByMember, getMembershipStatisticsById, + getMembershipTotalStatisticsById, getMemberStatisticsById, getQualificationByMemberAndRecord, getQualificationsByMember, @@ -72,6 +73,10 @@ router.get("/:memberId/memberships/statistics", async (req: Request, res: Respon await getMembershipStatisticsById(req, res); }); +router.get("/:memberId/memberships/totalstatistics", async (req: Request, res: Response) => { + await getMembershipTotalStatisticsById(req, res); +}); + router.get("/:memberId/membership/:id", async (req: Request, res: Response) => { await getMembershipByMemberAndRecord(req, res); }); diff --git a/src/service/club/member/membershipService.ts b/src/service/club/member/membershipService.ts index f5e7a07..d423b13 100644 --- a/src/service/club/member/membershipService.ts +++ b/src/service/club/member/membershipService.ts @@ -2,7 +2,7 @@ import { dataSource } from "../../../data-source"; import { membership } from "../../../entity/club/member/membership"; import DatabaseActionException from "../../../exceptions/databaseActionException"; import InternalException from "../../../exceptions/internalException"; -import { membershipView } from "../../../views/membershipsView"; +import { membershipTotalView, membershipView } from "../../../views/membershipsView"; export default abstract class MembershipService { /** @@ -66,4 +66,23 @@ export default abstract class MembershipService { throw new DatabaseActionException("SELECT", "membershipView", err); }); } + + /** + * @description get membership total statistics by memberId + * @param {string} memberId + * @returns {Promise>} + */ + static async getTotalStatisticsById(memberId: string): Promise { + return await dataSource + .getRepository(membershipTotalView) + .createQueryBuilder("membershipTotalView") + .where("membershipTotalView.memberId = :memberId", { memberId: memberId }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new DatabaseActionException("SELECT", "membershipTotalView", err); + }); + } } diff --git a/src/viewmodel/admin/club/member/membership.models.ts b/src/viewmodel/admin/club/member/membership.models.ts index ace02ea..a7a2256 100644 --- a/src/viewmodel/admin/club/member/membership.models.ts +++ b/src/viewmodel/admin/club/member/membership.models.ts @@ -20,3 +20,15 @@ export interface MembershipStatisticsViewModel { memberNameaffix: string; memberBirthdate: Date; } + +export interface MembershipTotalStatisticsViewModel { + durationInDays: number; + durationInYears: number; + exactDuration: string; + memberId: string; + memberSalutation: string; + memberFirstname: string; + memberLastname: string; + memberNameaffix: string; + memberBirthdate: Date; +} diff --git a/src/views/membershipsView.ts b/src/views/membershipsView.ts index fc38c23..7807f8e 100644 --- a/src/views/membershipsView.ts +++ b/src/views/membershipsView.ts @@ -89,3 +89,52 @@ export class membershipView { @ViewColumn() memberBirthdate: Date; } + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .getRepository(membership) + .createQueryBuilder("membership") + .select("member.id", "memberId") + .addSelect("member.firstname", "memberFirstname") + .addSelect("member.lastname", "memberLastname") + .addSelect("member.nameaffix", "memberNameaffix") + .addSelect("member.birthdate", "memberBirthdate") + .addSelect("salutation.salutation", "memberSalutation") + .addSelect(durationInDays, "durationInDays") + .addSelect(durationInYears, "durationInYears") + .addSelect(exactDuration, "exactDuration") + .leftJoin("membership.status", "status") + .leftJoin("membership.member", "member") + .leftJoin("member.salutation", "salutation") + .groupBy("status.id") + .addGroupBy("member.id"), +}) +export class membershipTotalView { + @ViewColumn() + durationInDays: number; + + @ViewColumn() + durationInYears: number; + + @ViewColumn() + exactDuration: string; + + @ViewColumn() + memberId: string; + + @ViewColumn() + memberSalutation: string; + + @ViewColumn() + memberFirstname: string; + + @ViewColumn() + memberLastname: string; + + @ViewColumn() + memberNameaffix: string; + + @ViewColumn() + memberBirthdate: Date; +}