@stamhoofd/backend 2.108.0 → 2.110.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.
Files changed (38) hide show
  1. package/index.ts +2 -2
  2. package/package.json +20 -20
  3. package/src/crons/disable-auto-update-documents.test.ts +164 -0
  4. package/src/crons/disable-auto-update-documents.ts +82 -0
  5. package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -5
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +8 -7
  7. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -8
  8. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +48 -0
  9. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +95 -19
  10. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +282 -0
  11. package/src/endpoints/organization/dashboard/documents/{PatchDocumentTemplateEndpoint.ts → PatchDocumentTemplatesEndpoint.ts} +56 -3
  12. package/src/excel-loaders/members.ts +61 -7
  13. package/src/excel-loaders/registrations.ts +123 -2
  14. package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
  15. package/src/helpers/SQLTranslatedString.ts +14 -0
  16. package/src/helpers/TagHelper.test.ts +9 -9
  17. package/src/helpers/outstandingBalanceJoin.ts +49 -0
  18. package/src/seeds/1765896674-document-update-year.test.ts +179 -0
  19. package/src/seeds/1765896674-document-update-year.ts +66 -0
  20. package/src/seeds/1766150402-document-published-at.test.ts +46 -0
  21. package/src/seeds/1766150402-document-published-at.ts +20 -0
  22. package/src/services/PaymentService.ts +14 -32
  23. package/src/sql-filters/base-registration-filter-compilers.ts +51 -4
  24. package/src/sql-filters/document-templates.ts +45 -0
  25. package/src/sql-filters/documents.ts +1 -1
  26. package/src/sql-filters/events.ts +6 -6
  27. package/src/sql-filters/groups.ts +7 -6
  28. package/src/sql-filters/members.ts +31 -26
  29. package/src/sql-filters/orders.ts +16 -16
  30. package/src/sql-filters/organizations.ts +11 -11
  31. package/src/sql-filters/payments.ts +10 -10
  32. package/src/sql-filters/registrations.ts +14 -6
  33. package/src/sql-sorters/document-templates.ts +79 -0
  34. package/src/sql-sorters/documents.ts +1 -1
  35. package/src/sql-sorters/members.ts +22 -0
  36. package/src/sql-sorters/orders.ts +5 -5
  37. package/src/sql-sorters/organizations.ts +3 -3
  38. package/src/sql-sorters/registrations.ts +186 -15
@@ -0,0 +1,282 @@
1
+ import { PatchableArray } from '@simonbackx/simple-encoding';
2
+ import { Endpoint, Request } from '@simonbackx/simple-endpoints';
3
+ import { DocumentTemplate, DocumentTemplateFactory, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
4
+ import { SQL } from '@stamhoofd/sql';
5
+ import { DocumentPrivateSettings, DocumentStatus, DocumentTemplateDefinition, DocumentTemplatePrivate, PermissionLevel, Permissions } from '@stamhoofd/structures';
6
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
7
+ import { testServer } from '../../../../../tests/helpers/TestServer.js';
8
+ import { PatchDocumentTemplatesEndpoint } from './PatchDocumentTemplatesEndpoint.js';
9
+
10
+ const baseUrl = `/organization/document-templates`;
11
+ const endpoint = new PatchDocumentTemplatesEndpoint();
12
+ type EndpointType = typeof endpoint;
13
+ type Body = EndpointType extends Endpoint<any, any, infer B, any> ? B : never;
14
+
15
+ describe('Endpoint.PatchDocumentTemplatesEndpoint', () => {
16
+ let period: RegistrationPeriod;
17
+ let organization: Organization;
18
+ let user: User;
19
+ let token: Token;
20
+
21
+ beforeEach(async () => {
22
+ TestUtils.setEnvironment('userMode', 'platform');
23
+ });
24
+
25
+ beforeAll(async () => {
26
+ period = await new RegistrationPeriodFactory({
27
+ startDate: new Date(2023, 0, 1),
28
+ endDate: new Date(2023, 11, 31),
29
+ }).create();
30
+
31
+ organization = await new OrganizationFactory({ period })
32
+ .create();
33
+
34
+ user = await new UserFactory({
35
+ organization,
36
+ permissions: Permissions.create({
37
+ level: PermissionLevel.Full,
38
+ }),
39
+ }).create();
40
+
41
+ token = await Token.createToken(user);
42
+ });
43
+
44
+ describe('put fiscal document', () => {
45
+ it('should throw if already has fiscal document in year', async () => {
46
+ // create existing fiscal document in same year
47
+ await new DocumentTemplateFactory({
48
+ organizationId: organization.id,
49
+ type: 'fiscal',
50
+ groups: [],
51
+ year: 2022,
52
+ }).create();
53
+
54
+ // create new fiscal document in same year
55
+ const arr: Body = new PatchableArray();
56
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
57
+ organizationId: organization.id,
58
+ type: 'fiscal',
59
+ groups: [],
60
+ year: 2022,
61
+ }).createWithoutSave());
62
+
63
+ const newDocument = newDocumentModel.getPrivateStructure();
64
+ arr.addPut(newDocument);
65
+
66
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
67
+ request.headers.authorization = 'Bearer ' + token.accessToken;
68
+
69
+ await expect(testServer.test(endpoint, request))
70
+ .rejects
71
+ .toThrow(STExpect.errorWithCode('double_fiscal_document'));
72
+ });
73
+
74
+ it('should not throw if first fiscal document in year', async () => {
75
+ // delete existing docs from organization
76
+ await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
77
+
78
+ const arr: Body = new PatchableArray();
79
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
80
+ organizationId: organization.id,
81
+ type: 'fiscal',
82
+ groups: [],
83
+ year: 2022,
84
+ }).createWithoutSave());
85
+
86
+ const newDocument = newDocumentModel.getPrivateStructure();
87
+ arr.addPut(newDocument);
88
+
89
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
90
+ request.headers.authorization = 'Bearer ' + token.accessToken;
91
+
92
+ const result = await testServer.test(endpoint, request);
93
+
94
+ expect(result).toBeDefined();
95
+ expect(result.status).toBe(200);
96
+ });
97
+ });
98
+
99
+ describe('patch fiscal document', () => {
100
+ describe('change type to fiscal', () => {
101
+ it('should throw if already has fiscal document in year', async () => {
102
+ // create existing fiscal document in same year
103
+ await new DocumentTemplateFactory({
104
+ organizationId: organization.id,
105
+ type: 'fiscal',
106
+ groups: [],
107
+ year: 2022,
108
+ }).create();
109
+
110
+ // create new fiscal document in same year
111
+ const arr: Body = new PatchableArray();
112
+
113
+ // create new participation document in same year
114
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
115
+ organizationId: organization.id,
116
+ type: 'participation',
117
+ groups: [],
118
+ year: 2022,
119
+ }).create());
120
+
121
+ // change type to fiscal
122
+ arr.addPatch(DocumentTemplatePrivate.patch({
123
+ id: newDocumentModel.id,
124
+ privateSettings: DocumentPrivateSettings.patch({
125
+ templateDefinition: DocumentTemplateDefinition.patch({
126
+ type: 'fiscal',
127
+ }),
128
+ }),
129
+ }));
130
+
131
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
132
+ request.headers.authorization = 'Bearer ' + token.accessToken;
133
+
134
+ await expect(testServer.test(endpoint, request))
135
+ .rejects
136
+ .toThrow(STExpect.errorWithCode('double_fiscal_document'));
137
+ });
138
+
139
+ it('should not throw if first fiscal document in year', async () => {
140
+ // delete existing docs from organization
141
+ await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
142
+
143
+ // create new fiscal document in same year
144
+ const arr: Body = new PatchableArray();
145
+
146
+ // create new participation document in same year
147
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
148
+ organizationId: organization.id,
149
+ type: 'participation',
150
+ groups: [],
151
+ year: 2022,
152
+ }).create());
153
+
154
+ // change type to fiscal
155
+ arr.addPatch(DocumentTemplatePrivate.patch({
156
+ id: newDocumentModel.id,
157
+ privateSettings: DocumentPrivateSettings.patch({
158
+ templateDefinition: DocumentTemplateDefinition.patch({
159
+ type: 'fiscal',
160
+ }),
161
+ }),
162
+ }));
163
+
164
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
165
+ request.headers.authorization = 'Bearer ' + token.accessToken;
166
+
167
+ const result = await testServer.test(endpoint, request);
168
+
169
+ expect(result).toBeDefined();
170
+ expect(result.status).toBe(200);
171
+ });
172
+ });
173
+
174
+ describe('change year', () => {
175
+ it('should throw if already has fiscal document in year', async () => {
176
+ // create existing fiscal document in same year
177
+ await new DocumentTemplateFactory({
178
+ organizationId: organization.id,
179
+ type: 'fiscal',
180
+ groups: [],
181
+ year: 2022,
182
+ }).create();
183
+
184
+ // create new fiscal document in same year
185
+ const arr: Body = new PatchableArray();
186
+
187
+ // create new participation document in other year
188
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
189
+ organizationId: organization.id,
190
+ type: 'fiscal',
191
+ groups: [],
192
+ year: 2021,
193
+ }).create());
194
+
195
+ // change year to same year as other existing document
196
+ arr.addPatch(DocumentTemplatePrivate.patch({
197
+ id: newDocumentModel.id,
198
+ year: 2022,
199
+ }));
200
+
201
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
202
+ request.headers.authorization = 'Bearer ' + token.accessToken;
203
+
204
+ await expect(testServer.test(endpoint, request))
205
+ .rejects
206
+ .toThrow(STExpect.errorWithCode('double_fiscal_document'));
207
+ });
208
+
209
+ it('should not throw if first fiscal document in year', async () => {
210
+ // delete existing docs from organization
211
+ await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
212
+
213
+ // create new fiscal document in same year
214
+ const arr: Body = new PatchableArray();
215
+
216
+ // create new participation document in other year
217
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
218
+ organizationId: organization.id,
219
+ type: 'fiscal',
220
+ groups: [],
221
+ year: 2021,
222
+ }).create());
223
+
224
+ // change year to same year as other existing document
225
+ arr.addPatch(DocumentTemplatePrivate.patch({
226
+ id: newDocumentModel.id,
227
+ year: 2022,
228
+ }));
229
+
230
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
231
+ request.headers.authorization = 'Bearer ' + token.accessToken;
232
+
233
+ const result = await testServer.test(endpoint, request);
234
+
235
+ expect(result).toBeDefined();
236
+ expect(result.status).toBe(200);
237
+ });
238
+ });
239
+
240
+ describe('do not change type or year of existing fiscal document', () => {
241
+ it('should not throw if multiple fiscal documents in year', async () => {
242
+ // delete existing docs from organization
243
+ await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
244
+
245
+ // create existing fiscal document in same year
246
+ await new DocumentTemplateFactory({
247
+ organizationId: organization.id,
248
+ type: 'fiscal',
249
+ groups: [],
250
+ year: 2022,
251
+ }).create();
252
+
253
+ // create new fiscal document in same year
254
+ const arr: Body = new PatchableArray();
255
+
256
+ // create double fiscal document in same year
257
+ const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
258
+ organizationId: organization.id,
259
+ type: 'fiscal',
260
+ groups: [],
261
+ year: 2022,
262
+ }).create());
263
+
264
+ // change status to published (do not change year or type)
265
+ arr.addPatch(DocumentTemplatePrivate.patch({
266
+ id: newDocumentModel.id,
267
+ status: DocumentStatus.Published,
268
+ }));
269
+
270
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
271
+ request.headers.authorization = 'Bearer ' + token.accessToken;
272
+
273
+ const result = await testServer.test(endpoint, request);
274
+
275
+ expect(result).toBeDefined();
276
+ expect(result.status).toBe(200);
277
+ expect(result.body.length).toBe(1);
278
+ expect(result.body[0].status).toBe(DocumentStatus.Published);
279
+ });
280
+ });
281
+ });
282
+ });
@@ -1,10 +1,11 @@
1
1
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { DocumentTemplate, Token } from '@stamhoofd/models';
4
+ import { DocumentTemplate } from '@stamhoofd/models';
5
5
  import { DocumentTemplatePrivate, PermissionLevel } from '@stamhoofd/structures';
6
6
 
7
- import { Context } from '../../../../helpers/Context';
7
+ import { SQL, SQLWhereSign } from '@stamhoofd/sql';
8
+ import { Context } from '../../../../helpers/Context.js';
8
9
 
9
10
  type Params = Record<string, never>;
10
11
  type Query = undefined;
@@ -15,7 +16,7 @@ type ResponseBody = DocumentTemplatePrivate[];
15
16
  * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
16
17
  */
17
18
 
18
- export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ export class PatchDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
20
  bodyDecoder = new PatchableArrayDecoder(DocumentTemplatePrivate as Decoder<DocumentTemplatePrivate>, DocumentTemplatePrivate.patchType() as Decoder<AutoEncoderPatchType<DocumentTemplatePrivate>>, StringDecoder);
20
21
 
21
22
  protected doesMatch(request: Request): [true, Params] | [false] {
@@ -49,7 +50,19 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
49
50
  template.status = put.status;
50
51
  template.html = put.html;
51
52
  template.updatesEnabled = put.updatesEnabled;
53
+ template.year = put.year;
52
54
  template.organizationId = organization.id;
55
+
56
+ if (await this.doesYearAlreadyHaveFiscalDocument(template)) {
57
+ throw new SimpleError({
58
+ code: 'double_fiscal_document',
59
+ field: 'year',
60
+ message: 'This year already has a fiscal document',
61
+ human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
62
+
63
+ });
64
+ }
65
+
53
66
  await template.save();
54
67
 
55
68
  // todo: Generate documents (maybe in background)
@@ -65,7 +78,14 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
65
78
  throw Context.auth.notFoundOrNoAccess($t(`148bfab7-ca0e-4fac-8a0a-302ca7855fc8`));
66
79
  }
67
80
 
81
+ let shouldCheckIfAlreadyHasFiscalDocument = false;
82
+
68
83
  if (patch.privateSettings) {
84
+ const patchType = patch.privateSettings.templateDefinition?.type;
85
+
86
+ // only check if type has changed and new type is fiscal
87
+ shouldCheckIfAlreadyHasFiscalDocument = patchType !== undefined && template.privateSettings.templateDefinition.type !== patchType && patchType === 'fiscal';
88
+
69
89
  template.privateSettings.patchOrPut(patch.privateSettings);
70
90
  }
71
91
 
@@ -85,6 +105,23 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
85
105
  template.html = patch.html;
86
106
  }
87
107
 
108
+ if (patch.year) {
109
+ if (shouldCheckIfAlreadyHasFiscalDocument === false) {
110
+ shouldCheckIfAlreadyHasFiscalDocument = template.year !== patch.year;
111
+ }
112
+
113
+ template.year = patch.year;
114
+ }
115
+
116
+ if (shouldCheckIfAlreadyHasFiscalDocument && await this.doesYearAlreadyHaveFiscalDocument(template)) {
117
+ throw new SimpleError({
118
+ code: 'double_fiscal_document',
119
+ field: 'year',
120
+ message: 'This year already has a fiscal document',
121
+ human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
122
+ });
123
+ }
124
+
88
125
  await template.save();
89
126
 
90
127
  // Update documents
@@ -111,4 +148,20 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
111
148
  updatedTemplates,
112
149
  );
113
150
  }
151
+
152
+ private async doesYearAlreadyHaveFiscalDocument(template: DocumentTemplate) {
153
+ let query = SQL.select().from(SQL.table(DocumentTemplate.table))
154
+ .where(SQL.column('organizationId'), template.organizationId)
155
+ .where(SQL.column('year'), template.year)
156
+ .where(SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'), 'fiscal');
157
+
158
+ // id is not set if put
159
+ if (template.id) {
160
+ query = query.where(SQL.column('id'), SQLWhereSign.NotEqual, template.id);
161
+ }
162
+
163
+ const result = await query.limit(1).count();
164
+
165
+ return result > 0;
166
+ }
114
167
  }
@@ -1,12 +1,12 @@
1
1
  import { XlsxBuiltInNumberFormat, XlsxTransformerColumn, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { Platform } from '@stamhoofd/models';
3
- import { ExcelExportType, Gender, GroupType, LimitedFilteredRequest, PlatformFamily, PlatformMember, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
3
+ import { ExcelExportType, Gender, GroupType, LimitedFilteredRequest, MembershipStatus, PlatformFamily, PlatformMember, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
4
4
  import { Formatter } from '@stamhoofd/utility';
5
- import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
6
- import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint';
7
- import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
8
- import { Context } from '../helpers/Context';
9
- import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
5
+ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
6
+ import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint.js';
7
+ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
8
+ import { Context } from '../helpers/Context.js';
9
+ import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
10
10
 
11
11
  export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
12
12
  {
@@ -150,6 +150,16 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
150
150
  value: object.details.nationalRegisterNumber?.toString() ?? '',
151
151
  }),
152
152
  },
153
+ {
154
+ id: 'membership',
155
+ name: $t(`c7d995f1-36a0-446e-9fcf-17ffb69f3f45`),
156
+ width: 20,
157
+ getValue: (member: PlatformMember) => {
158
+ return {
159
+ value: formatMembershipStatus(member.membershipStatus),
160
+ };
161
+ },
162
+ },
153
163
 
154
164
  ...XlsxTransformerColumnHelper.creatColumnsForParents(),
155
165
 
@@ -260,6 +270,35 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
260
270
  };
261
271
  },
262
272
  },
273
+ {
274
+ id: 'outstandingBalance',
275
+ name: $t(`beb45452-dee7-4a7f-956c-e6db06aac20f`),
276
+ width: 30,
277
+ getValue: (v) => {
278
+ return {
279
+ value: v.member.balances.reduce((sum, r) => sum + (r.amountOpen), 0) / 1_0000,
280
+ style: {
281
+ numberFormat: {
282
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
283
+ },
284
+ },
285
+ };
286
+ },
287
+
288
+ },
289
+ {
290
+ id: 'createdAt',
291
+ name: $t('c38e774e-e8ab-4549-b119-4eed380c626c'),
292
+ width: 20,
293
+ getValue: v => ({
294
+ value: v.member.createdAt,
295
+ style: {
296
+ numberFormat: {
297
+ id: XlsxBuiltInNumberFormat.DateSlash,
298
+ },
299
+ },
300
+ }),
301
+ },
263
302
 
264
303
  // Registration records
265
304
  {
@@ -427,10 +466,25 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
427
466
  ],
428
467
  });
429
468
 
430
- function formatGender(gender: Gender) {
469
+ function formatGender(gender: Gender): string {
431
470
  switch (gender) {
432
471
  case Gender.Male: return $t(`f972abd4-de1e-484b-b7da-ad4c75d37808`);
433
472
  case Gender.Female: return $t(`e21f499d-1078-4044-be5d-6693d2636699`);
434
473
  default: return $t(`60f13ba4-c6c9-4388-9add-43a996bf6bee`);
435
474
  }
436
475
  }
476
+
477
+ function formatMembershipStatus(status: MembershipStatus): string {
478
+ switch (status) {
479
+ case MembershipStatus.Trial:
480
+ return $t(`47c7c3c4-9246-40b7-b1e0-2cb408d5f79e`);
481
+ case MembershipStatus.Active:
482
+ return $t(`b56351e9-4847-4a0c-9eec-348d75c794c4`);
483
+ case MembershipStatus.Expiring:
484
+ return $t(`d9858110-37d9-4b4a-8bfb-d76b3cc5ef27`);
485
+ case MembershipStatus.Temporary:
486
+ return $t(`75e62d3c-f348-4104-8a1e-e11e6e7fbe32`);
487
+ case MembershipStatus.Inactive:
488
+ return $t(`1f8620fa-e8a5-4665-99c8-c1907a5b5768`);
489
+ }
490
+ }
@@ -1,6 +1,6 @@
1
1
  import { XlsxBuiltInNumberFormat, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { Platform } from '@stamhoofd/models';
3
- import { ExcelExportType, LimitedFilteredRequest, PlatformMember, PlatformRegistration, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
3
+ import { ExcelExportType, getGroupTypeName, LimitedFilteredRequest, PlatformMember, PlatformRegistration, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
4
4
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
5
5
  import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint.js';
6
6
  import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
@@ -46,9 +46,41 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
46
46
  };
47
47
  },
48
48
  },
49
+ {
50
+ id: 'toPay',
51
+ width: 30,
52
+ name: $t(`3a97e6cb-012d-4007-9c54-49d3e5b72909`),
53
+ getValue: (registration: PlatformRegistration) => {
54
+ return {
55
+ value: registration.balances.reduce((sum, r) => sum + (r.amountOpen + r.amountPending), 0) / 1_0000,
56
+ style: {
57
+ numberFormat: {
58
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
59
+ },
60
+ },
61
+ };
62
+ },
63
+
64
+ },
65
+ {
66
+ id: 'outstandingBalance',
67
+ name: $t(`beb45452-dee7-4a7f-956c-e6db06aac20f`),
68
+ width: 30,
69
+ getValue: (v) => {
70
+ return {
71
+ value: v.member.member.balances.reduce((sum, r) => sum + (r.amountOpen), 0) / 1_0000,
72
+ style: {
73
+ numberFormat: {
74
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
75
+ },
76
+ },
77
+ };
78
+ },
79
+
80
+ },
49
81
  {
50
82
  id: 'registeredAt',
51
- name: $t(`Inschrijvingsdatum`),
83
+ name: $t(`8bec8990-4632-40b0-93f3-f27a3f2ddbdb`),
52
84
  width: 20,
53
85
  getValue: (registration: PlatformRegistration) => ({
54
86
  value: registration.registeredAt,
@@ -59,6 +91,45 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
59
91
  },
60
92
  }),
61
93
  },
94
+ {
95
+ id: 'startDate',
96
+ name: $t(`bbe0af99-b574-4719-a505-ca2285fa86e4`),
97
+ width: 20,
98
+ getValue: (registration: PlatformRegistration) => ({
99
+ value: registration.startDate,
100
+ style: {
101
+ numberFormat: {
102
+ id: XlsxBuiltInNumberFormat.DateSlash,
103
+ },
104
+ },
105
+ }),
106
+ },
107
+ {
108
+ id: 'endDate',
109
+ name: $t(`aef10d71-39c4-4cdb-8252-5fd31781abd8`),
110
+ width: 20,
111
+ getValue: (registration: PlatformRegistration) => ({
112
+ value: registration.endDate,
113
+ style: {
114
+ numberFormat: {
115
+ id: XlsxBuiltInNumberFormat.DateSlash,
116
+ },
117
+ },
118
+ }),
119
+ },
120
+ {
121
+ id: 'createdAt',
122
+ name: $t('63a86cdf-8a76-4e8c-9073-4f0b8970e808'),
123
+ width: 20,
124
+ getValue: (registration: PlatformRegistration) => ({
125
+ value: registration.member.member.createdAt,
126
+ style: {
127
+ numberFormat: {
128
+ id: XlsxBuiltInNumberFormat.DateSlash,
129
+ },
130
+ },
131
+ }),
132
+ },
62
133
  {
63
134
  id: 'organization',
64
135
  name: $t('2f325358-6e2f-418c-9fea-31a14abbc17a'),
@@ -81,6 +152,46 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
81
152
  });
82
153
  },
83
154
  },
155
+ {
156
+ id: 'groupRegistration',
157
+ name: $t('7289b10e-a284-40ea-bc57-8287c6566a82'),
158
+ width: 40,
159
+ getValue: (registration: PlatformRegistration) => {
160
+ let value: string;
161
+
162
+ if (registration.payingOrganizationId) {
163
+ const organization = registration.member.organizations.find(o => o.id === registration.payingOrganizationId);
164
+ value = organization ? organization.name : $t(`bd1e59c8-3d4c-4097-ab35-0ce7b20d0e50`);
165
+ }
166
+ else {
167
+ value = $t(`b8b730fb-f1a3-4c13-8ec4-0aebe08a1449`);
168
+ }
169
+
170
+ return ({
171
+ value,
172
+ });
173
+ },
174
+ },
175
+ {
176
+ id: 'trialUntil',
177
+ name: $t(`1f2e9d09-717b-4c17-9bbe-dce3f3dcbff0`),
178
+ width: 40,
179
+ getValue: (registration: PlatformRegistration) => {
180
+ let value: Date | null = null;
181
+ if (registration.trialUntil && registration.trialUntil > new Date()) {
182
+ value = new Date(registration.trialUntil.getTime());
183
+ }
184
+
185
+ return {
186
+ value,
187
+ style: {
188
+ numberFormat: {
189
+ id: XlsxBuiltInNumberFormat.DateSlash,
190
+ },
191
+ },
192
+ };
193
+ },
194
+ },
84
195
  // option menu
85
196
  {
86
197
  match(id) {
@@ -228,6 +339,16 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
228
339
  };
229
340
  },
230
341
  },
342
+ {
343
+ id: 'group.type',
344
+ name: $t('4fda497f-b2d8-43ef-b08c-a3e4e0b472b4'),
345
+ width: 20,
346
+ getValue: (registration: PlatformRegistration) => {
347
+ return {
348
+ value: getGroupTypeName(registration.group.type),
349
+ };
350
+ },
351
+ },
231
352
  ],
232
353
  };
233
354
 
@@ -46,4 +46,28 @@ export class LimitedFilteredRequestHelper {
46
46
 
47
47
  return next;
48
48
  }
49
+
50
+ static fixInfiniteLoadingLoopWithTransform<R, T>({ request, results, sorters, transformer }: { request: LimitedFilteredRequest; results: R[]; sorters: SQLSortDefinitions<T>; transformer: (r: R) => T }): LimitedFilteredRequest | undefined {
51
+ let next: LimitedFilteredRequest | undefined;
52
+
53
+ if (results.length >= request.limit) {
54
+ const lastObject = transformer(results[results.length - 1]);
55
+ const nextFilter = getSortFilter(lastObject, sorters, request.sort);
56
+
57
+ next = new LimitedFilteredRequest({
58
+ filter: request.filter,
59
+ pageFilter: nextFilter,
60
+ sort: request.sort,
61
+ limit: request.limit,
62
+ search: request.search,
63
+ });
64
+
65
+ if (JSON.stringify(nextFilter) === JSON.stringify(request.pageFilter)) {
66
+ console.error('Found infinite loading loop for', request);
67
+ next = undefined;
68
+ }
69
+ }
70
+
71
+ return next;
72
+ }
49
73
  }
@@ -0,0 +1,14 @@
1
+ import { SQLColumnExpression, SQLExpression, SQLExpressionOptions, SQLQuery, SQLTranslatedStringHelper } from '@stamhoofd/sql';
2
+ import { Language } from '@stamhoofd/structures';
3
+
4
+ export class SQLTranslatedString implements SQLExpression {
5
+ private helper: SQLTranslatedStringHelper;
6
+
7
+ constructor(columnExpression: SQLColumnExpression, path: string) {
8
+ this.helper = new SQLTranslatedStringHelper(columnExpression, path, () => Language.English);
9
+ }
10
+
11
+ getSQL(options?: SQLExpressionOptions): SQLQuery {
12
+ return this.helper.getSQL(options);
13
+ }
14
+ }