#5-intelligent-groups #23

Merged
jkeffects merged 16 commits from #5-intelligent-groups into main 2024-12-19 09:50:45 +00:00
27 changed files with 285 additions and 74 deletions
Showing only changes of commit 6fd2091a7e - Show all commits

View file

@ -17,7 +17,6 @@ WORKDIR /app
COPY --from=build /app/dist /app/dist COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/package.json /app/package.json COPY --from=build /app/package.json /app/package.json
COPY --from=build /app/.env /app/.env
EXPOSE 5000 EXPOSE 5000

View file

@ -1,30 +1,87 @@
# member-administration-server # member-administration-server
Memberadministration Mitgliederverwaltung für Feuerwehren und Vereine (Backend).
Authentications is realized via JWT-Tokens. The server is able to send Mails to the members. ## Einleitung
Login is possible via Username and TOTP.
Dieses Projekt, `member-administration-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [member-administration-ui](https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui) zu finden.
Eine Demo zusammen mit der `member-administration-ui` finden Sie unter [ff-admin-demo.jk-effects.cloud](ff-admin-demo.jk-effects.cloud).
## Installation ## Installation
### Requirements ### Docker Compose Setup
1. MySql Database Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt:
2. Access to the internet for sending Mails
### Configuration ```yaml
version: "3"
1. Copy the .env.example file to .env and fill in the required information services:
2. Create a new Database in MySql named as in the .env file ff-member-administration-server:
3. Install all packages via `npm install` image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/server:latest
4. Start the application to create the database schema container_name: ff_member_administration_server
restart: unless-stopped
environment:
- DB_TYPE = mysql
- DB_HOST=ffm-db
- DB_NAME=administration
- DB_USERNAME=administration_backend
- DB_PASSWORD=<dbuserpasswd>
- JWT_SECRET=<tobemodified>
- JWT_EXPIRATION=<number[m|d] - bsp.:15m>
- REFRESH_EXPIRATION=<number[m|d] - bsp.:1d>
- MAIL_USERNAME=<mailadress|username>
- MAIL_PASSWORD=<password>
- MAIL_HOST=<url>
- MAIL_PORT=<port>
- MAIL_SECURE=<boolean>
- CLUB_NAME=<tobemodified>
volumes:
- <volume|local path>:/app/export
networks:
- ff_internal
depends_on:
- ff-db
## Testing ff-db:
image: mariadb:11.2
container_name: ff_db
restart: unless-stopped
environment:
- MYSQL_DATABASE=ffadmin
- MYSQL_USER=administration_backend
- MYSQL_PASSWORD=<dbuserpasswd>
- MYSQL_ROOT_PASSWORD=<dbrootpasswd>
volumes:
- <volume|local path>:/var/lib/mysql
networks:
- ff_internal
1. Install the database-system-package you like (e.g. mysql, mariadb, postgresql, sqlite3) networks:
2. Configure type inside src/data-source.ts to run the database-system you like. ff_internal:
3. Set migrationsRun to false and synchronize to true for rapid prototyping ```
4. Building the schema via CLI:
- Run `npm run update-database` to build the schema using the migrations without starting the application Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
- Run `npm run synchronize-database` to build the schema without using migrations without starting the application
5. Run `npm run dev` to run inside dev-environment (runs migrations if migrationsRun is set to true) ```sh
docker-compose up -d
```
### Manuelle Installation
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
```sh
git clone https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server.git
cd member-administration-server
npm install
npm run build
npm run start
```
## Fragen und Wünsche
Bei Fragen, Anregungen oder Wünschen können Sie sich gerne melden.\
Wir freuen uns über Ihr Feedback und helfen Ihnen gerne weiter.\
Schreiben Sie dafür eine Mail an julian.krauser@jk-effects.com.

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "member-administration-server", "name": "member-administration-server",
"version": "0.0.7", "version": "0.0.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "member-administration-server", "name": "member-administration-server",
"version": "0.0.7", "version": "0.0.9",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "member-administration-server", "name": "member-administration-server",
"version": "0.0.7", "version": "0.0.9",
"description": "Feuerwehr/Verein Mitgliederverwaltung Server", "description": "Feuerwehr/Verein Mitgliederverwaltung Server",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View file

@ -2,6 +2,7 @@ export interface CreateCalendarTypeCommand {
type: string; type: string;
nscdr: boolean; nscdr: boolean;
color: string; color: string;
passphrase?: string;
} }
export interface UpdateCalendarTypeCommand { export interface UpdateCalendarTypeCommand {
@ -9,6 +10,7 @@ export interface UpdateCalendarTypeCommand {
type: string; type: string;
nscdr: boolean; nscdr: boolean;
color: string; color: string;
passphrase?: string;
} }
export interface DeleteCalendarTypeCommand { export interface DeleteCalendarTypeCommand {

View file

@ -18,6 +18,7 @@ export default abstract class CalendarTypeCommandHandler {
type: createCalendarType.type, type: createCalendarType.type,
nscdr: createCalendarType.nscdr, nscdr: createCalendarType.nscdr,
color: createCalendarType.color, color: createCalendarType.color,
passphrase: createCalendarType.nscdr ? null : createCalendarType.passphrase,
}) })
.execute() .execute()
.then((result) => { .then((result) => {
@ -41,6 +42,7 @@ export default abstract class CalendarTypeCommandHandler {
type: updateCalendarType.type, type: updateCalendarType.type,
nscdr: updateCalendarType.nscdr, nscdr: updateCalendarType.nscdr,
color: updateCalendarType.color, color: updateCalendarType.color,
passphrase: updateCalendarType.nscdr ? null : updateCalendarType.passphrase,
}) })
.where("id = :id", { id: updateCalendarType.id }) .where("id = :id", { id: updateCalendarType.id })
.execute() .execute()

View file

@ -1,5 +1,6 @@
export interface CreateCommunicationCommand { export interface CreateCommunicationCommand {
preferred: boolean; preferred: boolean;
isSMSAlarming: boolean;
mobile: string; mobile: string;
email: string; email: string;
city: string; city: string;
@ -13,6 +14,7 @@ export interface CreateCommunicationCommand {
export interface UpdateCommunicationCommand { export interface UpdateCommunicationCommand {
id: number; id: number;
preferred: boolean; preferred: boolean;
isSMSAlarming: boolean;
mobile: string; mobile: string;
email: string; email: string;
city: string; city: string;

View file

@ -22,6 +22,7 @@ export default abstract class CommunicationCommandHandler {
.into(communication) .into(communication)
.values({ .values({
preferred: createCommunication.preferred, preferred: createCommunication.preferred,
isSMSAlarming: createCommunication.isSMSAlarming,
mobile: createCommunication.mobile, mobile: createCommunication.mobile,
email: createCommunication.email, email: createCommunication.email,
city: createCommunication.city, city: createCommunication.city,
@ -59,6 +60,7 @@ export default abstract class CommunicationCommandHandler {
.update(communication) .update(communication)
.set({ .set({
preferred: updateCommunication.preferred, preferred: updateCommunication.preferred,
isSMSAlarming: updateCommunication.isSMSAlarming,
mobile: updateCommunication.mobile, mobile: updateCommunication.mobile,
email: updateCommunication.email, email: updateCommunication.email,
city: updateCommunication.city, city: updateCommunication.city,

View file

@ -68,7 +68,6 @@ export default abstract class MemberCommandHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async updateNewsletter(updateMember: UpdateMemberNewsletterCommand): Promise<void> { static async updateNewsletter(updateMember: UpdateMemberNewsletterCommand): Promise<void> {
console.log(updateMember);
return await dataSource return await dataSource
.createQueryBuilder() .createQueryBuilder()
.update(member) .update(member)
@ -88,6 +87,26 @@ export default abstract class MemberCommandHandler {
}); });
} }
/**
* @description update member newsletter to unset
* @param memberId string
* @returns {Promise<void>}
*/
static async unsetNewsletter(memberId: number): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(member)
.set({
sendNewsletter: null,
})
.where("id = :id", { id: memberId })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating member", err);
});
}
/** /**
* @description delete member * @description delete member
* @param DeleteMemberCommand * @param DeleteMemberCommand

View file

@ -101,11 +101,13 @@ export async function createCalendarType(req: Request, res: Response): Promise<a
const type = req.body.type; const type = req.body.type;
const nscdr = req.body.nscdr; const nscdr = req.body.nscdr;
const color = req.body.color; const color = req.body.color;
const passphrase = req.body.passphrase;
let createType: CreateCalendarTypeCommand = { let createType: CreateCalendarTypeCommand = {
type, type,
nscdr, nscdr,
color, color,
passphrase,
}; };
let id = await CalendarTypeCommandHandler.create(createType); let id = await CalendarTypeCommandHandler.create(createType);
@ -154,12 +156,14 @@ export async function updateCalendarType(req: Request, res: Response): Promise<a
const type = req.body.type; const type = req.body.type;
const nscdr = req.body.nscdr; const nscdr = req.body.nscdr;
const color = req.body.color; const color = req.body.color;
const passphrase = req.body.passphrase;
let updateType: UpdateCalendarTypeCommand = { let updateType: UpdateCalendarTypeCommand = {
id, id,
type, type,
nscdr, nscdr,
color, color,
passphrase,
}; };
await CalendarTypeCommandHandler.update(updateType); await CalendarTypeCommandHandler.update(updateType);

View file

@ -346,6 +346,7 @@ export async function addExecutivePositionToMember(req: Request, res: Response):
export async function addCommunicationToMember(req: Request, res: Response): Promise<any> { export async function addCommunicationToMember(req: Request, res: Response): Promise<any> {
const memberId = parseInt(req.params.memberId); const memberId = parseInt(req.params.memberId);
const preferred = req.body.preferred; const preferred = req.body.preferred;
const isSMSAlarming = req.body.isSMSAlarming;
const mobile = req.body.mobile; const mobile = req.body.mobile;
const email = req.body.email; const email = req.body.email;
const city = req.body.city; const city = req.body.city;
@ -357,6 +358,7 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro
let createCommunication: CreateCommunicationCommand = { let createCommunication: CreateCommunicationCommand = {
preferred, preferred,
isSMSAlarming,
mobile, mobile,
email, email,
city, city,
@ -528,6 +530,7 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
const memberId = parseInt(req.params.memberId); const memberId = parseInt(req.params.memberId);
const recordId = parseInt(req.params.recordId); const recordId = parseInt(req.params.recordId);
const preferred = req.body.preferred; const preferred = req.body.preferred;
const isSMSAlarming = req.body.isSMSAlarming;
const mobile = req.body.mobile; const mobile = req.body.mobile;
const email = req.body.email; const email = req.body.email;
const city = req.body.city; const city = req.body.city;
@ -540,6 +543,7 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
let updateCommunication: UpdateCommunicationCommand = { let updateCommunication: UpdateCommunicationCommand = {
id: recordId, id: recordId,
preferred, preferred,
isSMSAlarming,
mobile, mobile,
email, email,
city, city,
@ -550,12 +554,16 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
}; };
await CommunicationCommandHandler.update(updateCommunication); await CommunicationCommandHandler.update(updateCommunication);
let currentUserNewsletterMain = await MemberService.getNewsletterById(memberId);
if (isNewsletterMain) { if (isNewsletterMain) {
let updateNewsletter: UpdateMemberNewsletterCommand = { let updateNewsletter: UpdateMemberNewsletterCommand = {
id: memberId, id: memberId,
communicationId: recordId, communicationId: recordId,
}; };
await MemberCommandHandler.updateNewsletter(updateNewsletter); await MemberCommandHandler.updateNewsletter(updateNewsletter);
} else if (currentUserNewsletterMain.sendNewsletter.id == recordId) {
await MemberCommandHandler.unsetNewsletter(memberId);
} }
res.sendStatus(204); res.sendStatus(204);

View file

@ -4,63 +4,96 @@ import CalendarTypeService from "../service/calendarTypeService";
import { calendar } from "../entity/calendar"; import { calendar } from "../entity/calendar";
import { createEvents } from "ics"; import { createEvents } from "ics";
import moment from "moment"; import moment from "moment";
import InternalException from "../exceptions/internalException";
import CalendarFactory from "../factory/admin/calendar";
/** /**
* @description get all calendar items by types or nscdr * @description get all calendar items by types or nscdr
* @summary passphrase is passed as value pair like `type:passphrase`
* @param req {Request} Express req object * @param req {Request} Express req object
* @param res {Response} Express res object * @param res {Response} Express res object
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
export async function getCalendarItemsByTypes(req: Request, res: Response): Promise<any> { export async function getCalendarItemsByTypes(req: Request, res: Response): Promise<any> {
let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types]; let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types];
let output = (req.query.output as "ics" | "json") ?? "ics";
if (output != "ics" && output != "json") {
throw new InternalException("set output query value to `ics` or `json` (defaults to `ics`)");
}
types = types.filter((t) => t);
let items: Array<calendar> = []; let items: Array<calendar> = [];
if (types.length == 0) { if (types.length != 0) {
let typeIds = await CalendarTypeService.getByTypes(types as Array<string>); let typeIds = await CalendarTypeService.getByTypes((types as Array<string>).map((t) => t.split(":")[0]));
typeIds = typeIds.filter(
(ti) =>
ti.passphrase == null ||
ti.passphrase == "" ||
ti.passphrase == (types as Array<string>).find((t) => t.includes(ti.type)).split(":")[1]
);
items = await CalendarService.getByTypes(typeIds.map((t) => t.id)); items = await CalendarService.getByTypes(typeIds.map((t) => t.id));
} else { } else {
items = await CalendarService.getByTypeNSCDR(); items = await CalendarService.getByTypeNSCDR();
} }
let events = createEvents( if (output == "json") {
items.map((i) => ({ res.json(CalendarFactory.mapToBase(items));
calName: process.env.CLUB_NAME, } else {
uid: i.id, let events = createEvents(
sequence: 1, items.map((i) => ({
start: moment(i.starttime) calName: process.env.CLUB_NAME,
.format("YYYY-M-D-H-m") uid: i.id,
.split("-") sequence: 1,
.map((a) => parseInt(a)) as [number, number, number, number, number], ...(i.allDay
end: moment(i.endtime) ? {
.format("YYYY-M-D-H-m") start: moment(i.starttime)
.split("-") .format("YYYY-M-D")
.map((a) => parseInt(a)) as [number, number, number, number, number], .split("-")
title: i.title, .map((a) => parseInt(a)) as [number, number, number],
description: i.content, end: moment(i.endtime)
location: i.location, .format("YYYY-M-D")
categories: [i.type.type], .split("-")
created: moment(i.createdAt) .map((a) => parseInt(a)) as [number, number, number],
.format("YYYY-M-D-H-m") }
.split("-") : {
.map((a) => parseInt(a)) as [number, number, number, number, number], start: moment(i.starttime)
lastModified: moment(i.updatedAt) .format("YYYY-M-D-H-m")
.format("YYYY-M-D-H-m") .split("-")
.split("-") .map((a) => parseInt(a)) as [number, number, number, number, number],
.map((a) => parseInt(a)) as [number, number, number, number, number], end: moment(i.endtime)
transp: "OPAQUE" as "OPAQUE", .format("YYYY-M-D-H-m")
url: "https://www.ff-merching.de", .split("-")
alarms: [ .map((a) => parseInt(a)) as [number, number, number, number, number],
{ }),
action: "display", title: i.title,
description: "Erinnerung", description: i.content,
trigger: { location: i.location,
minutes: 30, categories: [i.type.type],
before: true, created: moment(i.createdAt)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
lastModified: moment(i.updatedAt)
.format("YYYY-M-D-H-m")
.split("-")
.map((a) => parseInt(a)) as [number, number, number, number, number],
transp: "OPAQUE" as "OPAQUE",
url: "https://www.ff-merching.de",
alarms: [
{
action: "display",
description: "Erinnerung",
trigger: {
minutes: 30,
before: true,
},
}, },
}, ],
], }))
})) );
);
res.type("ics").send(events.value); res.type("ics").send(events.value);
}
} }

View file

@ -42,6 +42,8 @@ import { calendarType } from "./entity/calendarType";
import { Calendar1729947763295 } from "./migrations/1729947763295-calendar"; import { Calendar1729947763295 } from "./migrations/1729947763295-calendar";
import { reset } from "./entity/reset"; import { reset } from "./entity/reset";
import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken"; import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken";
import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming";
import { SecuringCalendarType1733249553766 } from "./migrations/1733249553766-securingCalendarType";
const dataSource = new DataSource({ const dataSource = new DataSource({
type: DB_TYPE as any, type: DB_TYPE as any,
@ -94,6 +96,8 @@ const dataSource = new DataSource({
Protocol1729347911107, Protocol1729347911107,
Calendar1729947763295, Calendar1729947763295,
ResetToken1732358596823, ResetToken1732358596823,
SMSAlarming1732696919191,
SecuringCalendarType1733249553766,
], ],
migrationsRun: true, migrationsRun: true,
migrationsTransactionMode: "each", migrationsTransactionMode: "each",

View file

@ -15,6 +15,9 @@ export class calendarType {
@Column({ type: "varchar", length: 255 }) @Column({ type: "varchar", length: 255 })
color: string; color: string;
@Column({ type: "varchar", length: 255, nullable: true, default: null })
passphrase: string | null;
@OneToMany(() => calendar, (c) => c.type, { @OneToMany(() => calendar, (c) => c.type, {
nullable: false, nullable: false,
onDelete: "RESTRICT", onDelete: "RESTRICT",

View file

@ -10,6 +10,9 @@ export class communication {
@Column({ type: "boolean", default: false }) @Column({ type: "boolean", default: false })
preferred: boolean; preferred: boolean;
@Column({ type: "boolean", default: false })
isSMSAlarming: boolean;
@Column({ type: "varchar", length: 255, nullable: true }) @Column({ type: "varchar", length: 255, nullable: true })
mobile: string; mobile: string;

View file

@ -65,4 +65,5 @@ export class member {
firstMembershipEntry?: membership; firstMembershipEntry?: membership;
lastMembershipEntry?: membership; lastMembershipEntry?: membership;
preferredCommunication?: Array<communication>; preferredCommunication?: Array<communication>;
smsAlarming?: Array<communication>;
} }

View file

@ -13,6 +13,7 @@ export default abstract class CalendarTypeFactory {
type: record.type, type: record.type,
nscdr: record.nscdr, nscdr: record.nscdr,
color: record.color, color: record.color,
passphrase: record.passphrase,
}; };
} }

View file

@ -20,6 +20,7 @@ export default abstract class CommunicationFactory {
streetNumberAddition: record.streetNumberAddition, streetNumberAddition: record.streetNumberAddition,
type: CommunicationTypeFactory.mapToSingle(record.type), type: CommunicationTypeFactory.mapToSingle(record.type),
isNewsletterMain: isMain ? isMain : record?.member?.sendNewsletter?.id == record.id, isNewsletterMain: isMain ? isMain : record?.member?.sendNewsletter?.id == record.id,
isSMSAlarming: record.isSMSAlarming,
}; };
} }

View file

@ -27,6 +27,7 @@ export default abstract class MemberFactory {
preferredCommunication: record?.preferredCommunication preferredCommunication: record?.preferredCommunication
? CommunicationFactory.mapToBase(record.preferredCommunication) ? CommunicationFactory.mapToBase(record.preferredCommunication)
: null, : null,
smsAlarming: record?.smsAlarming ? CommunicationFactory.mapToBase(record.smsAlarming) : null,
}; };
} }

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class SMSAlarming1732696919191 implements MigrationInterface {
name = "SMSAlarming1732696919191";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"communication",
new TableColumn({
name: "isSMSAlarming",
type: "tinyint",
default: 0,
isNullable: false,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("communication", "isSMSAlarming");
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class SecuringCalendarType1733249553766 implements MigrationInterface {
name = "SecuringCalendarType1733249553766";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns("calendar_type", [
new TableColumn({
name: "passphrase",
type: "varchar",
length: "255",
isNullable: true,
}),
]);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("calendar_type", "passphrase");
}
}

View file

@ -25,13 +25,13 @@ export default (app: Express) => {
app.use(cors()); app.use(cors());
app.options("*", cors()); app.options("*", cors());
app.use("/public", publicAvailable); app.use("/api/public", publicAvailable);
app.use("/setup", allowSetup, setup); app.use("/api/setup", allowSetup, setup);
app.use("/reset", reset); app.use("/api/reset", reset);
app.use("/invite", invite); app.use("/api/invite", invite);
app.use("/auth", auth); app.use("/api/auth", auth);
app.use(authenticate); app.use(authenticate);
app.use("/admin", admin); app.use("/api/admin", admin);
app.use("/user", user); app.use("/api/user", user);
app.use(errorHandler); app.use(errorHandler);
}; };

View file

@ -56,6 +56,6 @@ export default abstract class CommunicationService {
static getAvailableColumnsForCommunication(): Array<string> { static getAvailableColumnsForCommunication(): Array<string> {
let metadata = dataSource.getMetadata(communication); let metadata = dataSource.getMetadata(communication);
let columns = metadata.columns.map((c) => c.propertyName); let columns = metadata.columns.map((c) => c.propertyName);
return columns.filter((c) => !["id", "preferred", "type", "member"].includes(c)); return columns.filter((c) => !["id", "preferred", "isSMSAlarming", "type", "member"].includes(c));
} }
} }

View file

@ -35,6 +35,8 @@ export default abstract class MemberService {
"preferredCommunication.preferred = 1" "preferredCommunication.preferred = 1"
) )
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred") .leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
.leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1")
.leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming")
.offset(offset) .offset(offset)
.limit(count) .limit(count)
.orderBy("member.lastname") .orderBy("member.lastname")
@ -52,7 +54,7 @@ export default abstract class MemberService {
/** /**
* @description get member by id * @description get member by id
* @param {number} id * @param {number} id
* @returns {Promise<Array<member>>} * @returns {Promise<member>}
*/ */
static async getById(id: number): Promise<member> { static async getById(id: number): Promise<member> {
return await dataSource return await dataSource
@ -80,6 +82,9 @@ export default abstract class MemberService {
"preferredCommunication", "preferredCommunication",
"preferredCommunication.preferred = 1" "preferredCommunication.preferred = 1"
) )
.leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1")
.leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming")
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred") .leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
.where("member.id = :id", { id: id }) .where("member.id = :id", { id: id })
.getOneOrFail() .getOneOrFail()
@ -90,4 +95,24 @@ export default abstract class MemberService {
throw new InternalException("member not found by id", err); throw new InternalException("member not found by id", err);
}); });
} }
/**
* @description get newsletter by member by id
* @param {number} id
* @returns {Promise<member>}
*/
static async getNewsletterById(id: number): Promise<member> {
return await dataSource
.getRepository(member)
.createQueryBuilder("member")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.where("member.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("member not found by id", err);
});
}
} }

View file

@ -3,4 +3,5 @@ export interface CalendarTypeViewModel {
type: string; type: string;
nscdr: boolean; nscdr: boolean;
color: string; color: string;
passphrase: string | null;
} }

View file

@ -11,4 +11,5 @@ export interface CommunicationViewModel {
streetNumberAddition: string; streetNumberAddition: string;
type: CommunicationTypeViewModel; type: CommunicationTypeViewModel;
isNewsletterMain: boolean; isNewsletterMain: boolean;
isSMSAlarming: boolean;
} }

View file

@ -12,5 +12,6 @@ export interface MemberViewModel {
firstMembershipEntry?: MembershipViewModel; firstMembershipEntry?: MembershipViewModel;
lastMembershipEntry?: MembershipViewModel; lastMembershipEntry?: MembershipViewModel;
sendNewsletter?: CommunicationViewModel; sendNewsletter?: CommunicationViewModel;
smsAlarming?: Array<CommunicationViewModel>;
preferredCommunication?: Array<CommunicationViewModel>; preferredCommunication?: Array<CommunicationViewModel>;
} }