Compare commits

..

12 commits
v1.0.2 ... main

15 changed files with 78 additions and 12 deletions

View file

@ -6,7 +6,7 @@ Administration für Feuerwehren und Vereine (Backend).
Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [ff-admin-ui](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin) zu finden. Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [ff-admin-ui](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin) zu finden.
Eine Demo zusammen mit der `ff-admin` finden Sie unter [ff-admin-demo.jk-effects.cloud](ff-admin-demo.jk-effects.cloud). Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
## Installation ## Installation

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "ff-admin-server", "name": "ff-admin-server",
"version": "1.0.2", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ff-admin-server", "name": "ff-admin-server",
"version": "1.0.2", "version": "1.1.1",
"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": "ff-admin-server", "name": "ff-admin-server",
"version": "1.0.2", "version": "1.1.1",
"description": "Feuerwehr/Verein Mitgliederverwaltung Server", "description": "Feuerwehr/Verein Mitgliederverwaltung Server",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View file

@ -6,4 +6,5 @@ export interface SynchronizeProtocolPresenceCommand {
export interface ProtocolPresenceCommand { export interface ProtocolPresenceCommand {
memberId: number; memberId: number;
absent: boolean; absent: boolean;
excused: boolean;
} }

View file

@ -74,6 +74,7 @@ export default abstract class ProtocolPresenceCommandHandler {
.update(protocolPresence) .update(protocolPresence)
.set({ .set({
absent: member.absent, absent: member.absent,
excused: member.excused,
}) })
.where("memberId = :memberId", { memberId: member.memberId }) .where("memberId = :memberId", { memberId: member.memberId })
.andWhere("protocolId = :protocolId", { protocolId }) .andWhere("protocolId = :protocolId", { protocolId })

View file

@ -61,8 +61,13 @@ export async function getAllMembers(req: Request, res: Response): Promise<any> {
let offset = parseInt((req.query.offset as string) ?? "0"); let offset = parseInt((req.query.offset as string) ?? "0");
let count = parseInt((req.query.count as string) ?? "25"); let count = parseInt((req.query.count as string) ?? "25");
let search = (req.query.search as string) ?? ""; let search = (req.query.search as string) ?? "";
let noLimit = req.query.noLimit === "true";
let ids = ((req.query.ids ?? "") as string)
.split(",")
.filter((i) => i)
.map((i) => parseInt(i));
let [members, total] = await MemberService.getAll(offset, count, search); let [members, total] = await MemberService.getAll({ offset, count, search, noLimit, ids });
res.json({ res.json({
members: MemberFactory.mapToBase(members), members: MemberFactory.mapToBase(members),

View file

@ -260,7 +260,9 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
agenda, agenda,
decisions, decisions,
presence: presence.filter((p) => !p.absent).map((p) => p.member), presence: presence.filter((p) => !p.absent).map((p) => p.member),
absent: presence.filter((p) => p.absent).map((p) => p.member), absent: presence.filter((p) => p.absent).map((p) => ({ ...p.member, excused: p.excused })),
excused_absent: presence.filter((p) => p.absent && p.excused).map((p) => p.member),
unexcused_absent: presence.filter((p) => p.absent && !p.excused).map((p) => p.member),
votings, votings,
}, },
}); });
@ -389,6 +391,7 @@ export async function synchronizeProtocolPrecenseById(req: Request, res: Respons
members: presence.map((p) => ({ members: presence.map((p) => ({
memberId: p.memberId, memberId: p.memberId,
absent: p.absent, absent: p.absent,
excused: p.excused,
})), })),
protocolId, protocolId,
}; };

View file

@ -67,6 +67,7 @@ import { ProtocolAbsent1736072179716 } from "./migrations/1736072179716-protocol
import { Memberlist1736079005086 } from "./migrations/1736079005086-memberlist"; import { Memberlist1736079005086 } from "./migrations/1736079005086-memberlist";
import { ExtendViewValues1736084198860 } from "./migrations/1736084198860-extendViewValues"; import { ExtendViewValues1736084198860 } from "./migrations/1736084198860-extendViewValues";
import { FinishInternalIdTransfer1736505324488 } from "./migrations/1736505324488-finishInternalIdTransfer"; import { FinishInternalIdTransfer1736505324488 } from "./migrations/1736505324488-finishInternalIdTransfer";
import { ProtocolPresenceExcuse1737287798828 } from "./migrations/1737287798828-protocolPresenceExcuse";
const dataSource = new DataSource({ const dataSource = new DataSource({
type: DB_TYPE as any, type: DB_TYPE as any,
@ -144,6 +145,7 @@ const dataSource = new DataSource({
Memberlist1736079005086, Memberlist1736079005086,
ExtendViewValues1736084198860, ExtendViewValues1736084198860,
FinishInternalIdTransfer1736505324488, FinishInternalIdTransfer1736505324488,
ProtocolPresenceExcuse1737287798828,
], ],
migrationsRun: true, migrationsRun: true,
migrationsTransactionMode: "each", migrationsTransactionMode: "each",

View file

@ -13,6 +13,9 @@ export class protocolPresence {
@Column({ type: "boolean", default: false }) @Column({ type: "boolean", default: false })
absent: boolean; absent: boolean;
@Column({ type: "boolean", default: true })
excused: boolean;
@ManyToOne(() => member, { @ManyToOne(() => member, {
nullable: false, nullable: false,
onDelete: "CASCADE", onDelete: "CASCADE",

View file

@ -12,6 +12,7 @@ export default abstract class ProtocolPresenceFactory {
return { return {
memberId: record.member.id, memberId: record.member.id,
absent: record.absent, absent: record.absent,
excused: record.excused,
protocolId: record.protocolId, protocolId: record.protocolId,
}; };
} }

View file

@ -141,7 +141,7 @@ export abstract class NewsletterHelper {
} }
} }
let members = await MemberService.getAll(0, 1000); let members = await MemberService.getAll({ noLimit: true });
return members[0].filter((m) => queryMemberIds.includes(m.id)); return members[0].filter((m) => queryMemberIds.includes(m.id));
} }

View file

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

View file

@ -8,7 +8,19 @@ export default abstract class MemberService {
* @description get all members * @description get all members
* @returns {Promise<[Array<member>, number]>} * @returns {Promise<[Array<member>, number]>}
*/ */
static async getAll(offset: number = 0, count: number = 25, search: string = ""): Promise<[Array<member>, number]> { static async getAll({
offset = 0,
count = 25,
search = "",
noLimit = false,
ids = [],
}: {
offset?: number;
count?: number;
search?: string;
noLimit?: boolean;
ids?: Array<number>;
}): Promise<[Array<member>, number]> {
let query = dataSource let query = dataSource
.getRepository(member) .getRepository(member)
.createQueryBuilder("member") .createQueryBuilder("member")
@ -54,9 +66,15 @@ export default abstract class MemberService {
}); });
} }
if (ids.length != 0) {
query = query.where({ id: ids });
}
if (!noLimit) {
query = query.offset(offset).limit(count);
}
return await query return await query
.offset(offset)
.limit(count)
.orderBy("member.lastname") .orderBy("member.lastname")
.addOrderBy("member.firstname") .addOrderBy("member.firstname")
.addOrderBy("member.nameaffix") .addOrderBy("member.nameaffix")

View file

@ -14,9 +14,19 @@
<br /> <br />
<br /> <br />
<h2>Anwesenheit ({{presence.length}})</h2> <h2>Anwesenheit ({{presence.length}})</h2>
<p>{{#each presence}} {{this.firstname}} {{this.lastname}}{{#unless @last}}, {{/unless}} {{/each}}</p> <p>
{{#each presence}}{{this.firstname}} {{this.lastname}}{{#unless @last}}, {{/unless}}{{/each}}{{#unless
presence.length}}---{{/unless}}
</p>
<h2>Abwesenheit ({{absent.length}})</h2> <h2>Abwesenheit ({{absent.length}})</h2>
<p>{{#each absent}} {{this.firstname}} {{this.lastname}}{{#unless @last}}, {{/unless}} {{/each}}</p> <p>
entschuldigt: {{#each excused_absent}}{{this.firstname}} {{this.lastname}}{{#unless @last}},
{{/unless}}{{/each}}{{#unless excused_absent.length}}---{{/unless}}
</p>
<p>
unentschuldigt: {{#each unexcused_absent}}{{this.firstname}} {{this.lastname}}{{#unless @last}},
{{/unless}}{{/each}}{{#unless unexcused_absent.length}}---{{/unless}}
</p>
<br /> <br />
<h2>Agenda</h2> <h2>Agenda</h2>
{{#each agenda}} {{#each agenda}}

View file

@ -3,5 +3,6 @@ import { MemberViewModel } from "../member/member.models";
export interface ProtocolPresenceViewModel { export interface ProtocolPresenceViewModel {
memberId: number; memberId: number;
absent: boolean; absent: boolean;
excused: boolean;
protocolId: number; protocolId: number;
} }