@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.
- package/index.ts +2 -2
- package/package.json +20 -20
- package/src/crons/disable-auto-update-documents.test.ts +164 -0
- package/src/crons/disable-auto-update-documents.ts +82 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -5
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +8 -7
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -8
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +48 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +95 -19
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +282 -0
- package/src/endpoints/organization/dashboard/documents/{PatchDocumentTemplateEndpoint.ts → PatchDocumentTemplatesEndpoint.ts} +56 -3
- package/src/excel-loaders/members.ts +61 -7
- package/src/excel-loaders/registrations.ts +123 -2
- package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
- package/src/helpers/SQLTranslatedString.ts +14 -0
- package/src/helpers/TagHelper.test.ts +9 -9
- package/src/helpers/outstandingBalanceJoin.ts +49 -0
- package/src/seeds/1765896674-document-update-year.test.ts +179 -0
- package/src/seeds/1765896674-document-update-year.ts +66 -0
- package/src/seeds/1766150402-document-published-at.test.ts +46 -0
- package/src/seeds/1766150402-document-published-at.ts +20 -0
- package/src/services/PaymentService.ts +14 -32
- package/src/sql-filters/base-registration-filter-compilers.ts +51 -4
- package/src/sql-filters/document-templates.ts +45 -0
- package/src/sql-filters/documents.ts +1 -1
- package/src/sql-filters/events.ts +6 -6
- package/src/sql-filters/groups.ts +7 -6
- package/src/sql-filters/members.ts +31 -26
- package/src/sql-filters/orders.ts +16 -16
- package/src/sql-filters/organizations.ts +11 -11
- package/src/sql-filters/payments.ts +10 -10
- package/src/sql-filters/registrations.ts +14 -6
- package/src/sql-sorters/document-templates.ts +79 -0
- package/src/sql-sorters/documents.ts +1 -1
- package/src/sql-sorters/members.ts +22 -0
- package/src/sql-sorters/orders.ts +5 -5
- package/src/sql-sorters/organizations.ts +3 -3
- 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
|
|
4
|
+
import { DocumentTemplate } from '@stamhoofd/models';
|
|
5
5
|
import { DocumentTemplatePrivate, PermissionLevel } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
|
-
import {
|
|
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
|
|
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(`
|
|
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
|
+
}
|