@stamhoofd/backend 2.53.0 → 2.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.53.0",
3
+ "version": "2.54.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -36,14 +36,14 @@
36
36
  "@simonbackx/simple-encoding": "2.16.6",
37
37
  "@simonbackx/simple-endpoints": "1.14.0",
38
38
  "@simonbackx/simple-logging": "^1.0.1",
39
- "@stamhoofd/backend-i18n": "2.53.0",
40
- "@stamhoofd/backend-middleware": "2.53.0",
41
- "@stamhoofd/email": "2.53.0",
42
- "@stamhoofd/models": "2.53.0",
43
- "@stamhoofd/queues": "2.53.0",
44
- "@stamhoofd/sql": "2.53.0",
45
- "@stamhoofd/structures": "2.53.0",
46
- "@stamhoofd/utility": "2.53.0",
39
+ "@stamhoofd/backend-i18n": "2.54.0",
40
+ "@stamhoofd/backend-middleware": "2.54.0",
41
+ "@stamhoofd/email": "2.54.0",
42
+ "@stamhoofd/models": "2.54.0",
43
+ "@stamhoofd/queues": "2.54.0",
44
+ "@stamhoofd/sql": "2.54.0",
45
+ "@stamhoofd/structures": "2.54.0",
46
+ "@stamhoofd/utility": "2.54.0",
47
47
  "archiver": "^7.0.1",
48
48
  "aws-sdk": "^2.885.0",
49
49
  "axios": "1.6.8",
@@ -63,5 +63,5 @@
63
63
  "publishConfig": {
64
64
  "access": "public"
65
65
  },
66
- "gitHead": "d202fb8d4a234f7369c3da54ba15f2b10b82e74e"
66
+ "gitHead": "d895bc5e468a87792398ca4e762d1216bf750baf"
67
67
  }
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { Event, Group, Platform, RegistrationPeriod } from '@stamhoofd/models';
4
- import { Event as EventStruct, GroupType, NamedObject } from '@stamhoofd/structures';
4
+ import { Event as EventStruct, GroupType, NamedObject, Group as GroupStruct } from '@stamhoofd/structures';
5
5
 
6
6
  import { SimpleError } from '@simonbackx/simple-errors';
7
7
  import { SQL, SQLWhereSign } from '@stamhoofd/sql';
@@ -218,6 +218,15 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
218
218
  event.groupId = group.id;
219
219
  }
220
220
  }
221
+ else {
222
+ if (patch.startDate || patch.endDate) {
223
+ // Correct period id if needed
224
+ const period = await RegistrationPeriod.getByDate(event.startDate);
225
+ if (event.groupId) {
226
+ await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: event.groupId }), period);
227
+ }
228
+ }
229
+ }
221
230
 
222
231
  if (type.isLocationRequired === true) {
223
232
  PatchEventsEndpoint.throwIfAddressIsMissing(event);
@@ -8,6 +8,7 @@ import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { Email } from '@stamhoofd/email';
10
10
  import { QueueHandler } from '@stamhoofd/queues';
11
+ import { SQL, SQLWhereSign } from '@stamhoofd/sql';
11
12
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
13
  import { Context } from '../../../helpers/Context';
13
14
  import { MembershipCharger } from '../../../helpers/MembershipCharger';
@@ -440,10 +441,32 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
440
441
  }
441
442
 
442
443
  // Check duplicate memberships
443
-
444
- // Check dates
445
-
446
- // Calculate prices
444
+ const existing = await MemberPlatformMembership.select()
445
+ .where('memberId', member.id)
446
+ .where('membershipTypeId', put.membershipTypeId)
447
+ .where('periodId', put.periodId)
448
+ .where(
449
+ SQL.where('startDate', SQLWhereSign.LessEqual, put.startDate)
450
+ .and('endDate', SQLWhereSign.GreaterEqual, put.startDate),
451
+ )
452
+ .orWhere(
453
+ SQL.where('startDate', SQLWhereSign.LessEqual, put.endDate)
454
+ .and('endDate', SQLWhereSign.GreaterEqual, put.endDate),
455
+ )
456
+ .orWhere(
457
+ SQL.where('startDate', SQLWhereSign.GreaterEqual, put.startDate)
458
+ .and('endDate', SQLWhereSign.LessEqual, put.endDate),
459
+ )
460
+ .first(false);
461
+
462
+ if (existing) {
463
+ throw new SimpleError({
464
+ code: 'invalid_field',
465
+ field: 'startDate',
466
+ message: 'Invalid start date',
467
+ human: 'Je kan geen aansluiting toevoegen die overlapt met een bestaande aansluiting van hetzelfde type',
468
+ });
469
+ }
447
470
 
448
471
  const membership = new MemberPlatformMembership();
449
472
  membership.id = put.id;
@@ -408,7 +408,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
408
408
  balanceItem2.memberId = registration.memberId;
409
409
 
410
410
  // If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
411
- balanceItem2.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
411
+ balanceItem2.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
412
412
  await balanceItem2.save();
413
413
 
414
414
  // do not add to createdBalanceItems array because we don't want to add this to the payment if we create a payment
@@ -419,7 +419,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
419
419
  balanceItem.userId = user.id;
420
420
  }
421
421
 
422
- balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
422
+ balanceItem.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
423
423
  balanceItem.pricePaid = 0;
424
424
 
425
425
  // Connect the 'pay back' balance item to this balance item. As soon as this balance item is paid, we'll mark the other one as pending so the outstanding balance for the member increases
@@ -434,24 +434,24 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
434
434
  const { item, registration } = bundle;
435
435
  registration.reservedUntil = null;
436
436
 
437
- if (shouldMarkValid) {
437
+ /* if (shouldMarkValid) {
438
438
  await registration.markValid({ skipEmail: bundle.item.replaceRegistrations.length > 0 });
439
439
  }
440
- else {
441
- // Reserve registration for 30 minutes (if needed)
442
- const group = groups.find(g => g.id === registration.groupId);
440
+ else { */
441
+ // Reserve registration for 30 minutes (if needed)
442
+ const group = groups.find(g => g.id === registration.groupId);
443
443
 
444
- if (group && group.settings.maxMembers !== null) {
445
- registration.reservedUntil = new Date(new Date().getTime() + 1000 * 60 * 30);
446
- }
447
- await registration.save();
444
+ if (group && group.settings.maxMembers !== null) {
445
+ registration.reservedUntil = new Date(new Date().getTime() + 1000 * 60 * 30);
448
446
  }
447
+ await registration.save();
448
+ // }
449
449
 
450
450
  // Note: we should always create the balance items: even when the price is zero
451
451
  // Otherwise we don't know which registrations to activate after payment
452
452
 
453
453
  if (shouldMarkValid && item.calculatedPrice === 0) {
454
- continue;
454
+ // continue;
455
455
  }
456
456
 
457
457
  // Create balance items
@@ -538,7 +538,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
538
538
  if (oldestMember) {
539
539
  balanceItem.memberId = oldestMember.id;
540
540
  }
541
- balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
541
+ balanceItem.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
542
542
  await balanceItem.save();
543
543
  createdBalanceItems.push(balanceItem);
544
544
  }
@@ -563,7 +563,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
563
563
  }
564
564
  }
565
565
 
566
- balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
566
+ balanceItem.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
567
567
  await balanceItem.save();
568
568
 
569
569
  createdBalanceItems.push(balanceItem);
@@ -580,6 +580,16 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
580
580
  let paymentUrl: string | null = null;
581
581
  let payment: Payment | null = null;
582
582
 
583
+ // Delaying markign as valid as late as possible so any errors will prevent creating valid balance items
584
+ async function markValidIfNeeded() {
585
+ if (shouldMarkValid) {
586
+ for (const balanceItem of [...createdBalanceItems, ...unrelatedCreatedBalanceItems]) {
587
+ // Mark vlaid
588
+ await balanceItem.markPaid(payment, organization);
589
+ }
590
+ }
591
+ }
592
+
583
593
  if (whoWillPayNow !== 'nobody') {
584
594
  const mappedBalanceItems = new Map<BalanceItem, number>();
585
595
 
@@ -606,6 +616,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
606
616
  checkout: request.body,
607
617
  members,
608
618
  });
619
+ await markValidIfNeeded();
609
620
 
610
621
  if (response) {
611
622
  paymentUrl = response.paymentUrl;
@@ -618,6 +629,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
618
629
  }
619
630
  }
620
631
  else {
632
+ await markValidIfNeeded();
621
633
  await BalanceItem.updateOutstanding([...createdBalanceItems, ...unrelatedCreatedBalanceItems]);
622
634
  }
623
635
 
@@ -96,7 +96,6 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
96
96
  },
97
97
  XlsxTransformerColumnHelper.createAddressColumns<PlatformMember>({
98
98
  matchId: 'address',
99
- identifier: 'Adres',
100
99
  getAddress: ({ patchedMember: object }: PlatformMember) => {
101
100
  // get member address if exists
102
101
  const memberAddress = object.details.address;
@@ -227,7 +226,6 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
227
226
  },
228
227
  ...XlsxTransformerColumnHelper.createColumnsForAddresses<PlatformMember>({
229
228
  matchIdStart: 'unverifiedAddresses',
230
- identifier: 'Niet-geverifieerd adres',
231
229
  getAddresses: object => object.patchedMember.details.unverifiedAddresses,
232
230
  limit: 2,
233
231
  }),
@@ -277,6 +275,153 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
277
275
  }
278
276
  },
279
277
  },
278
+
279
+ // Registration records
280
+ {
281
+ match(id) {
282
+ if (id.startsWith('groups.')) {
283
+ const splitted = id.split('.');
284
+ if (splitted.length < 3) {
285
+ return;
286
+ }
287
+
288
+ const groupId = splitted[1];
289
+ const recordName = splitted[2];
290
+
291
+ function getRegistration(object: PlatformMember) {
292
+ return object.filterRegistrations({ groupIds: [groupId] })[0] ?? null;
293
+ }
294
+
295
+ if (recordName === 'price') {
296
+ // Tarief
297
+ return [
298
+ {
299
+ id: `groups.${groupId}.${recordName}`,
300
+ name: 'Tarief',
301
+ width: 30,
302
+ getValue: (member: PlatformMember) => {
303
+ const registration = getRegistration(member);
304
+ if (!registration) {
305
+ return {
306
+ value: '',
307
+ };
308
+ }
309
+
310
+ return {
311
+ value: registration.groupPrice.name,
312
+ };
313
+ },
314
+ },
315
+ ];
316
+ }
317
+
318
+ if (recordName === 'optionMenu') {
319
+ if (splitted.length < 4) {
320
+ return;
321
+ }
322
+
323
+ const menuId = splitted[3];
324
+
325
+ if (splitted.length > 4) {
326
+ const optionId = splitted[4];
327
+ const returnAmount = splitted.length > 5 && splitted[5] === 'amount';
328
+
329
+ // Option menu
330
+ return [
331
+ {
332
+ id: `groups.${groupId}.${recordName}.${menuId}.${optionId}${returnAmount ? '.amount' : ''}`,
333
+ name: 'Keuzemenu aantal',
334
+ width: 30,
335
+ getValue: (member: PlatformMember) => {
336
+ const registration = getRegistration(member);
337
+ if (!registration) {
338
+ return {
339
+ value: '',
340
+ };
341
+ }
342
+ const options = registration.options.filter(o => o.optionMenu.id === menuId && o.option.id === optionId);
343
+
344
+ if (!options.length) {
345
+ return {
346
+ value: '',
347
+ };
348
+ }
349
+
350
+ return {
351
+ style: options.length === 1 && returnAmount
352
+ ? {
353
+ numberFormat: {
354
+ id: XlsxBuiltInNumberFormat.Number,
355
+ },
356
+ }
357
+ : {},
358
+ value: options.length === 1 && returnAmount ? options[0].amount : options.map(option => returnAmount ? option.amount : option).join(', '),
359
+ };
360
+ },
361
+ },
362
+ ];
363
+ }
364
+
365
+ // Option menu
366
+ return [
367
+ {
368
+ id: `groups.${groupId}.${recordName}.${menuId}`,
369
+ name: 'Keuzemenu',
370
+ width: 30,
371
+ getValue: (member: PlatformMember) => {
372
+ const registration = getRegistration(member);
373
+ if (!registration) {
374
+ return {
375
+ value: '',
376
+ };
377
+ }
378
+ const options = registration.options.filter(o => o.optionMenu.id === menuId);
379
+
380
+ if (!options.length) {
381
+ return {
382
+ value: '',
383
+ };
384
+ }
385
+
386
+ return {
387
+ value: options.map(option => (option.amount > 1 ? `${option.amount}x ` : '') + option.option.name).join(', '),
388
+ };
389
+ },
390
+ },
391
+ ];
392
+ }
393
+
394
+ if (recordName === 'recordAnswers') {
395
+ if (splitted.length < 4) {
396
+ return;
397
+ }
398
+
399
+ const recordId = splitted[3];
400
+ return [
401
+ {
402
+ id: `groups.${groupId}.${recordName}.${recordId}`,
403
+ name: 'Vraag',
404
+ width: 35,
405
+ getValue: (member: PlatformMember) => {
406
+ const registration = getRegistration(member);
407
+ if (!registration) {
408
+ return {
409
+ value: '',
410
+ };
411
+ }
412
+
413
+ return {
414
+ value: registration.recordAnswers.get(recordId)?.excelValues[0]?.value ?? '',
415
+ };
416
+ },
417
+ },
418
+ ];
419
+ }
420
+
421
+ return;
422
+ }
423
+ },
424
+ },
280
425
  ],
281
426
  };
282
427
 
@@ -1,18 +1,40 @@
1
1
  import { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
- import { ExcelExportType, LimitedFilteredRequest, Organization as OrganizationStruct } from '@stamhoofd/structures';
2
+ import { Platform as PlatformStruct, ExcelExportType, LimitedFilteredRequest, Organization as OrganizationStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, PaginatedResponse, MemberWithRegistrationsBlob, Premise } from '@stamhoofd/structures';
3
3
  import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint';
4
4
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
5
+ import { XlsxTransformerColumnHelper } from '../helpers/xlsxAddressTransformerColumnFactory';
6
+ import { Group, Member, MemberResponsibilityRecord } from '@stamhoofd/models';
7
+ import { Formatter, Sorter } from '@stamhoofd/utility';
8
+ import { ArrayDecoder, field } from '@simonbackx/simple-encoding';
9
+ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
10
+
11
+ class MemberResponsibilityRecordWithMember extends MemberResponsibilityRecordStruct {
12
+ @field({ decoder: MemberWithRegistrationsBlob })
13
+ member: MemberWithRegistrationsBlob;
14
+ }
15
+
16
+ class OrganizationWithResponsibilities extends OrganizationStruct {
17
+ @field({ decoder: new ArrayDecoder(MemberResponsibilityRecordWithMember) })
18
+ responsibilities: MemberResponsibilityRecordWithMember[];
19
+ }
20
+
21
+ class MemberResponsibilityRecordWithMemberAndOrganization extends MemberResponsibilityRecordWithMember {
22
+ @field({ decoder: OrganizationWithResponsibilities })
23
+ organization: OrganizationWithResponsibilities;
24
+ }
25
+
26
+ type Object = OrganizationWithResponsibilities;
5
27
 
6
28
  // Assign to a typed variable to assure we have correct type checking in place
7
- const sheet: XlsxTransformerSheet<OrganizationStruct, OrganizationStruct> = {
29
+ const sheet: XlsxTransformerSheet<Object, Object> = {
8
30
  id: 'organizations',
9
- name: 'Leden',
31
+ name: 'Groepen',
10
32
  columns: [
11
33
  {
12
34
  id: 'id',
13
35
  name: 'ID',
14
- width: 20,
15
- getValue: (object: OrganizationStruct) => ({
36
+ width: 35,
37
+ getValue: (object: Object) => ({
16
38
  value: object.id,
17
39
  }),
18
40
  },
@@ -20,7 +42,7 @@ const sheet: XlsxTransformerSheet<OrganizationStruct, OrganizationStruct> = {
20
42
  id: 'uri',
21
43
  name: 'Groepsnummer',
22
44
  width: 20,
23
- getValue: (object: OrganizationStruct) => ({
45
+ getValue: (object: Object) => ({
24
46
  value: object.uri,
25
47
  }),
26
48
  },
@@ -28,20 +50,241 @@ const sheet: XlsxTransformerSheet<OrganizationStruct, OrganizationStruct> = {
28
50
  id: 'name',
29
51
  name: 'Naam',
30
52
  width: 50,
31
- getValue: (object: OrganizationStruct) => ({
53
+ getValue: (object: Object) => ({
32
54
  value: object.name,
33
55
  }),
34
56
  },
57
+ {
58
+ id: 'tags',
59
+ name: 'Tags',
60
+ width: 50,
61
+ getValue: (object: Object) => {
62
+ const platform = PlatformStruct.shared;
63
+
64
+ return {
65
+ value: object.meta.tags.map(tag => platform.config.tags.find(t => t.id === tag)?.name ?? 'Onbekend').join(', '),
66
+ };
67
+ },
68
+ },
69
+ XlsxTransformerColumnHelper.createAddressColumns<OrganizationStruct>({
70
+ matchId: 'address',
71
+ getAddress: object => object.address,
72
+ }),
73
+ ],
74
+ };
75
+
76
+ const responsibilities: XlsxTransformerSheet<Object, MemberResponsibilityRecordWithMemberAndOrganization> = {
77
+ id: 'responsibilities',
78
+ name: 'Functies',
79
+ transform(organization) {
80
+ return organization.responsibilities.map(r => MemberResponsibilityRecordWithMemberAndOrganization.create({
81
+ ...r,
82
+ organization,
83
+ }));
84
+ },
85
+ columns: [
86
+ {
87
+ id: 'organization.id',
88
+ name: 'ID',
89
+ width: 35,
90
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => ({
91
+ value: object.organization.id,
92
+ }),
93
+ },
94
+ {
95
+ id: 'organization.uri',
96
+ name: 'Groepsnummer',
97
+ width: 20,
98
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => ({
99
+ value: object.organization.uri,
100
+ }),
101
+ },
102
+ {
103
+ id: 'organization.name',
104
+ name: 'Groepsnaam',
105
+ width: 50,
106
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => ({
107
+ value: object.organization.name,
108
+ }),
109
+ },
110
+ {
111
+ id: 'responsibility.name',
112
+ name: 'Functie',
113
+ width: 50,
114
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => {
115
+ const platform = PlatformStruct.shared;
116
+ const responsibility = platform.config.responsibilities.find(r => r.id === object.responsibilityId) ?? object.organization.privateMeta?.responsibilities.find(r => r.id === object.responsibilityId);
117
+
118
+ if (!responsibility) {
119
+ return {
120
+ value: 'Onbekende functie',
121
+ };
122
+ }
123
+
124
+ return {
125
+ value: responsibility.name + (responsibility.isGroupBased ? ' van ' + (object.group?.settings.name ?? 'Onbekende groep') : ''),
126
+ };
127
+ },
128
+ },
129
+ {
130
+ id: 'responsibility.member.firstName',
131
+ name: 'Voornaam',
132
+ width: 30,
133
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => ({
134
+ value: object.member.firstName,
135
+ }),
136
+ },
137
+ {
138
+ id: 'responsibility.member.lastName',
139
+ name: 'Achternaam',
140
+ width: 30,
141
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => ({
142
+ value: object.member.details.lastName,
143
+ }),
144
+ },
145
+ {
146
+ id: 'responsibility.member.email',
147
+ name: 'E-mailadres lid',
148
+ width: 50,
149
+ getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => ({
150
+ value: object.member.details.email,
151
+ }),
152
+ },
153
+ XlsxTransformerColumnHelper.createAddressColumns<MemberResponsibilityRecordWithMemberAndOrganization>({
154
+ matchId: 'responsibility.member.address',
155
+ getAddress: object => object.member.details.address ?? object.member.details.parents[0]?.address ?? object.member.details.parents[1]?.address ?? null,
156
+ }),
157
+ ],
158
+ };
159
+
160
+ type PremiseWithOrganization = { organization: Object; premise: Premise };
161
+ const premises: XlsxTransformerSheet<Object, PremiseWithOrganization> = {
162
+ id: 'premises',
163
+ name: 'Lokalen',
164
+ transform(organization) {
165
+ return organization.privateMeta?.premises.map(r => ({
166
+ organization,
167
+ premise: r,
168
+ })) ?? [];
169
+ },
170
+ columns: [
171
+ {
172
+ id: 'organization.id',
173
+ name: 'ID',
174
+ width: 35,
175
+ getValue: (object: PremiseWithOrganization) => ({
176
+ value: object.organization.id,
177
+ }),
178
+ },
179
+ {
180
+ id: 'organization.uri',
181
+ name: 'Groepsnummer',
182
+ width: 20,
183
+ getValue: (object: PremiseWithOrganization) => ({
184
+ value: object.organization.uri,
185
+ }),
186
+ },
187
+ {
188
+ id: 'organization.name',
189
+ name: 'Groepsnaam',
190
+ width: 50,
191
+ getValue: (object: PremiseWithOrganization) => ({
192
+ value: object.organization.name,
193
+ }),
194
+ },
195
+ {
196
+ id: 'premise.name',
197
+ name: 'Naam',
198
+ width: 20,
199
+ getValue: (object: PremiseWithOrganization) => ({
200
+ value: object.premise.name,
201
+ }),
202
+ },
203
+ {
204
+ id: 'premise.type',
205
+ name: 'Type',
206
+ width: 20,
207
+ getValue: (object: PremiseWithOrganization) => {
208
+ const ids = object.premise.premiseTypeIds;
209
+ const platform = PlatformStruct.shared;
210
+ return {
211
+ value: ids.map(id => platform.config.premiseTypes.find(t => t.id === id)?.name ?? 'Onbekend').join(', '),
212
+ };
213
+ },
214
+ },
215
+ XlsxTransformerColumnHelper.createAddressColumns<PremiseWithOrganization>({
216
+ matchId: 'premise.address',
217
+ getAddress: object => object.premise.address,
218
+ }),
35
219
  ],
36
220
  };
37
221
 
38
222
  ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
39
223
  fetch: async (query: LimitedFilteredRequest) => {
40
- const result = await GetOrganizationsEndpoint.buildData(query);
224
+ const organizations = await GetOrganizationsEndpoint.buildData(query);
225
+
226
+ // Now load all responsibilities with members with an active responsibility for theses organizations
227
+ const organizationIds = organizations.results.map(o => o.id);
228
+ const responsibilities = organizationIds.length
229
+ ? await MemberResponsibilityRecord.select()
230
+ .where('organizationId', organizationIds)
231
+ .where('endDate', null)
232
+ .fetch()
233
+ : [];
234
+
235
+ // Load groups and members
236
+ const groupIds = Formatter.uniqueArray(responsibilities.map(o => o.groupId).filter(g => g !== null));
237
+ const memberIds = Formatter.uniqueArray(responsibilities.map(o => o.memberId).filter(m => m !== null));
238
+
239
+ const members = await Member.getBlobByIds(...memberIds);
240
+ const groups = await Group.getByIDs(...groupIds);
241
+ const memberStructs = await AuthenticatedStructures.members(members);
242
+ const groupStructs = await AuthenticatedStructures.groups(groups);
243
+ const platform = PlatformStruct.shared;
244
+
245
+ const mappedOrganizations = organizations.results.map((o) => {
246
+ const resp = responsibilities.filter(r => r.organizationId === o.id);
247
+
248
+ const mappedResponsibilities = resp.map((r) => {
249
+ const member = memberStructs.find(m => m.id === r.memberId);
250
+ const group = groupStructs.find(g => g.id === r.groupId);
251
+
252
+ return MemberResponsibilityRecordWithMember.create({
253
+ ...r,
254
+ member: member,
255
+ group: group ? group : null,
256
+ });
257
+ });
258
+
259
+ // Sort responsibilites by index in platform config
260
+ // and, if the same, sort by order of default age group id if it has a group
261
+ mappedResponsibilities.sort((a, b) => {
262
+ const aIndex = platform.config.responsibilities.findIndex(r => r.id === a.responsibilityId);
263
+ const bIndex = platform.config.responsibilities.findIndex(r => r.id === b.responsibilityId);
264
+
265
+ const groupAIndex = platform.config.defaultAgeGroups.findIndex(g => g.id === a.group?.defaultAgeGroupId);
266
+ const groupBIndex = platform.config.defaultAgeGroups.findIndex(g => g.id === b.group?.defaultAgeGroupId);
267
+
268
+ return Sorter.stack(
269
+ aIndex - bIndex,
270
+ groupAIndex - groupBIndex,
271
+ );
272
+ });
273
+
274
+ return OrganizationWithResponsibilities.create({
275
+ ...o,
276
+ responsibilities: mappedResponsibilities,
277
+ });
278
+ });
41
279
 
42
- return result;
280
+ return new PaginatedResponse({
281
+ results: mappedOrganizations,
282
+ next: organizations.next,
283
+ });
43
284
  },
44
285
  sheets: [
45
286
  sheet,
287
+ responsibilities,
288
+ premises,
46
289
  ],
47
290
  });
@@ -479,7 +479,6 @@ function getPayingOrganizationColumns(): XlsxTransformerColumn<PaymentGeneral>[]
479
479
  XlsxTransformerColumnHelper.createAddressColumns<PaymentGeneralWithStripeAccount>({
480
480
  matchId: 'payingOrganization.address',
481
481
  getAddress: object => object.payingOrganization?.address,
482
- identifier: 'Adres betalende groep',
483
482
  }),
484
483
  ];
485
484
  }
@@ -539,7 +538,6 @@ function getInvoiceColumns(): XlsxTransformerColumn<PaymentGeneral>[] {
539
538
  XlsxTransformerColumnHelper.createAddressColumns<PaymentGeneralWithStripeAccount>({
540
539
  matchId: 'customer.company.address',
541
540
  getAddress: object => object.customer?.company?.address,
542
- identifier: 'Adres',
543
541
  }),
544
542
  {
545
543
  id: 'customer.company.administrationEmail',
@@ -131,7 +131,10 @@ export class AdminPermissionChecker {
131
131
  if (organizationId) {
132
132
  // If request is scoped to a different organization
133
133
  if (this.organization && organizationId !== this.organization.id) {
134
- return false;
134
+ if (STAMHOOFD.userMode === 'organization') {
135
+ return false;
136
+ }
137
+ // Otherwise allow for convenience
135
138
  }
136
139
 
137
140
  // If user is limited to scope
@@ -315,6 +315,10 @@ export class AuthenticatedStructures {
315
315
  return structs;
316
316
  }
317
317
 
318
+ static async members(members: MemberWithRegistrations[]): Promise<MemberWithRegistrationsBlob[]> {
319
+ return (await this.membersBlob(members, false)).members;
320
+ }
321
+
318
322
  static async membersBlob(members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User): Promise<MembersBlob> {
319
323
  if (members.length === 0 && !includeUser) {
320
324
  return MembersBlob.create({ members: [], organizations: [] });
@@ -1,5 +1,5 @@
1
1
  import { XlsxTransformerColumn } from '@stamhoofd/excel-writer';
2
- import { Address, CountryHelper, MemberWithRegistrationsBlob, Parent, ParentTypeHelper, PlatformMember } from '@stamhoofd/structures';
2
+ import { Address, CountryHelper, Parent, ParentTypeHelper, PlatformMember } from '@stamhoofd/structures';
3
3
 
4
4
  export class XlsxTransformerColumnHelper {
5
5
  static formatBoolean(value: boolean | undefined | null): string {
@@ -21,14 +21,13 @@ export class XlsxTransformerColumnHelper {
21
21
  ];
22
22
  }
23
23
 
24
- static createColumnsForAddresses<T>({ limit, getAddresses, matchIdStart, identifier }: { limit: number; getAddresses: (object: T) => Address[]; matchIdStart: string; identifier: string }): XlsxTransformerColumn<T>[] {
24
+ static createColumnsForAddresses<T>({ limit, getAddresses, matchIdStart }: { limit: number; getAddresses: (object: T) => Address[]; matchIdStart: string }): XlsxTransformerColumn<T>[] {
25
25
  const result: XlsxTransformerColumn<unknown>[] = [];
26
26
 
27
27
  for (let i = 0; i <= limit; i++) {
28
28
  const column = this.createAddressColumns({
29
29
  matchId: `${matchIdStart}.${i}`,
30
30
  getAddress: (object: T) => getAddresses(object)[i],
31
- identifier: `${identifier} ${i + 1}`,
32
31
  });
33
32
 
34
33
  result.push(column);
@@ -94,18 +93,12 @@ export class XlsxTransformerColumnHelper {
94
93
  XlsxTransformerColumnHelper.createAddressColumns<PlatformMember>({
95
94
  matchId: getId('address'),
96
95
  getAddress: member => getParent(member)?.address,
97
- identifier: getName('Adres'),
98
96
  }),
99
97
  ];
100
98
  }
101
99
 
102
- static createAddressColumns<T>({ matchId, identifier, getAddress }: { matchId: string; identifier?: string; getAddress: (object: T) => Address | null | undefined }): XlsxTransformerColumn<T> {
100
+ static createAddressColumns<T>({ matchId, getAddress }: { matchId: string; getAddress: (object: T) => Address | null | undefined }): XlsxTransformerColumn<T> {
103
101
  const getId = (value: string) => matchId + '.' + value;
104
- const identifierText = identifier ? `${identifier} - ` : '';
105
- const getName = (value: string) => {
106
- const name = `${identifierText}${value}`;
107
- return name[0].toUpperCase() + name.slice(1);
108
- };
109
102
 
110
103
  return {
111
104
  match: (id) => {
@@ -113,7 +106,7 @@ export class XlsxTransformerColumnHelper {
113
106
  return [
114
107
  {
115
108
  id: getId('street'),
116
- name: getName(`Straat`),
109
+ name: `Straat`,
117
110
  width: 30,
118
111
  getValue: (object: T) => {
119
112
  const address = getAddress(object);
@@ -124,7 +117,7 @@ export class XlsxTransformerColumnHelper {
124
117
  },
125
118
  {
126
119
  id: getId('number'),
127
- name: getName('Nummer'),
120
+ name: 'Nummer',
128
121
  width: 20,
129
122
  getValue: (object: T) => {
130
123
  const address = getAddress(object);
@@ -135,7 +128,7 @@ export class XlsxTransformerColumnHelper {
135
128
  },
136
129
  {
137
130
  id: getId('postalCode'),
138
- name: getName('Postcode'),
131
+ name: 'Postcode',
139
132
  width: 20,
140
133
  getValue: (object: T) => {
141
134
  const address = getAddress(object);
@@ -146,7 +139,7 @@ export class XlsxTransformerColumnHelper {
146
139
  },
147
140
  {
148
141
  id: getId('city'),
149
- name: getName('Stad'),
142
+ name: 'Stad',
150
143
  width: 20,
151
144
  getValue: (object: T) => {
152
145
  const address = getAddress(object);
@@ -157,7 +150,7 @@ export class XlsxTransformerColumnHelper {
157
150
  },
158
151
  {
159
152
  id: getId('country'),
160
- name: getName('Land'),
153
+ name: 'Land',
161
154
  width: 20,
162
155
  getValue: (object: T) => {
163
156
  const address = getAddress(object);