Skip to content

Commit 5a4f383

Browse files
author
Brijesh
committed
#418 - Implement export agreements list endpoint
1 parent 2bad5df commit 5a4f383

4 files changed

Lines changed: 208 additions & 41 deletions

File tree

src/constants.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,40 @@ export const AGREEMENT_EXEMPTION_STATUS = {
142142
};
143143

144144
export const SYSTEM_USER_ID = 1115;
145+
146+
export const AGREEMENT_EXPORT_COLUMNS = {
147+
RAN: 'RAN',
148+
LICENSE_TYPE: 'License Type',
149+
RANGE_NAME: 'Range Name',
150+
LICENSEE: 'Licensee',
151+
AGREEMENT_HOLDER_1: 'Agreement Holder 1',
152+
AGREEMENT_HOLDER_1_EMAIL: 'Agreement Holder 1 Email',
153+
AGREEMENT_HOLDER_2: 'Agreement Holder 2',
154+
AGREEMENT_HOLDER_2_EMAIL: 'Agreement Holder 2 Email',
155+
AGREEMENT_HOLDER_3: 'Agreement Holder 3',
156+
AGREEMENT_HOLDER_3_EMAIL: 'Agreement Holder 3 Email',
157+
AGREEMENT_HOLDER_4: 'Agreement Holder 4',
158+
AGREEMENT_HOLDER_4_EMAIL: 'Agreement Holder 4 Email',
159+
AGREEMENT_HOLDER_5: 'Agreement Holder 5',
160+
AGREEMENT_HOLDER_5_EMAIL: 'Agreement Holder 5 Email',
161+
ZONE_AGROLOGIST: 'Zone Agrologist',
162+
ZONE: 'Zone',
163+
DISTRICT: 'District',
164+
PLAN_STATUS: 'Plan Status',
165+
PLAN_START_DATE: 'Plan Start Date',
166+
PLAN_END_DATE: 'Plan End Date',
167+
USAGE_STATUS: 'Usage Status',
168+
PERCENTAGE_USE: 'Percentage Use',
169+
HAS_CURRENT_SCHEDULE: 'Has Current Schedule',
170+
EXEMPTION_STATUS: 'Exemption Status',
171+
};
172+
173+
export const AGREEMENT_EXPORT = {
174+
FILENAME: 'agreements.csv',
175+
CONTENT_TYPE: 'text/csv',
176+
NO_PLAN_DEFAULT: 'No Plan',
177+
YES: 'Yes',
178+
NO: 'No',
179+
DATE_FORMAT: 'YYYY-MM-DD',
180+
TEST_RAN_PREFIX: 'RAN0999',
181+
};

src/libs/db2/model/agreement.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ export default class Agreement extends Model {
103103
}
104104
}
105105

106+
static getLicenseTypeText(agreement) {
107+
if (Agreement.isHayCuttingSchedule(agreement)) {
108+
return 'Hay Cutting Licence';
109+
}
110+
if (Agreement.isGrazingSchedule(agreement)) {
111+
return 'Grazing Licence';
112+
}
113+
return '';
114+
}
115+
106116
get usageStatusText() {
107117
return Agreement.getUsageStatusText(this.usage_status);
108118
}
@@ -491,6 +501,9 @@ export default class Agreement extends Model {
491501
clientTypeCode: client.clientType.code,
492502
startDate: client.licenseeStartDate,
493503
endDate: client.licenseeEndDate,
504+
email: client.email,
505+
userGivenName: client.userGivenName,
506+
userFamilyName: client.userFamilyName,
494507
};
495508

496509
return aClient;

src/libs/db2/model/client.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export default class Client extends Model {
3838
super(obj, db);
3939

4040
this.clientType = new ClientType(ClientType.extract(data), db);
41+
this.email = data.user_email || '';
42+
this.userGivenName = data.user_given_name || '';
43+
this.userFamilyName = data.user_family_name || '';
4144
}
4245

4346
// To match previous Agreement (sequelize) schema.
@@ -61,7 +64,13 @@ export default class Client extends Model {
6164
return [];
6265
}
6366

64-
const myFields = [...Client.fields, ...ClientType.fields.map((f) => `${f} AS ${f.replace('.', '_')}`)];
67+
const myFields = [
68+
...Client.fields,
69+
...ClientType.fields.map((f) => `${f} AS ${f.replace('.', '_')}`),
70+
'user_account.email AS user_email',
71+
'user_account.given_name AS user_given_name',
72+
'user_account.family_name AS user_family_name',
73+
];
6574

6675
const results = await db
6776
.select(myFields)
@@ -72,6 +81,12 @@ export default class Client extends Model {
7281
.join('ref_client_type', {
7382
'client_agreement.client_type_id': 'ref_client_type.id',
7483
})
84+
.leftJoin('user_client_link', {
85+
'user_client_link.client_id': 'ref_client.client_number',
86+
})
87+
.leftJoin('user_account', {
88+
'user_account.id': 'user_client_link.user_id',
89+
})
7590
.where({ 'client_agreement.agreement_id': agreement.forestFileId });
7691

7792
const clients = results.map((row) => new Client(row));

src/router/routes_v1/agreement.js

Lines changed: 142 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
import { asyncMiddleware, errorWithCode } from '@bcgov/nodejs-common-utils';
44
import { Router } from 'express';
5+
import moment from 'moment';
6+
import csv from 'csv';
57
import config from '../../config';
68
import DataManager from '../../libs/db2';
79
import UserDistricts from '../../libs/db2/model/userDistricts';
810
import ExemptionController from '../controllers_v1/ExemptionController';
911
import ExemptionStatusController from '../controllers_v1/ExemptionStatusController';
12+
import { AGREEMENT_EXPORT_COLUMNS, AGREEMENT_EXPORT } from '../../constants';
13+
1014
const router = new Router();
1115

1216
const dm2 = new DataManager(config);
@@ -38,57 +42,64 @@ const getAgreeementsForAH = async (user, filterSettings) => {
3842
});
3943
};
4044

41-
// Search agreements by RAN, contact name, and client name. This is only used by Web
42-
router.get(
43-
'/search',
44-
asyncMiddleware(async (req, res) => {
45-
const { user, query } = req;
46-
const filterSettings = JSON.parse(query.filterSettings);
47-
const page = Number(filterSettings.page || 0);
48-
const limit = Number(filterSettings.limit || 10);
49-
let agreements = [];
50-
let totalItems = 0;
51-
if (user.isAgreementHolder()) {
52-
agreements = await getAgreeementsForAH(user, {
53-
...filterSettings,
54-
limit: null,
55-
page: null,
56-
});
57-
} else if (user.isAdministrator() || user.canReadAll() || user.isRangeOfficer()) {
58-
if (!(filterSettings?.zoneInfo?.selectedZones?.length === 0)) {
59-
agreements = await Agreement.findWithAllRelations(
60-
db,
61-
{ 'ref_zone.id': filterSettings.zoneInfo?.selectedZones },
62-
{
63-
...filterSettings,
64-
limit: null,
65-
page: null,
66-
},
67-
true,
68-
);
69-
}
70-
} else if (user.isDecisionMaker() || user.canReadDistrict()) {
71-
const districts = await UserDistricts.find(db, { user_id: user.id });
72-
const zones = await Zone.find(db, {
73-
district_id: districts.map((d) => d.id),
74-
});
45+
const getAgreements = async (user, filterSettings) => {
46+
let agreements = [];
47+
if (user.isAgreementHolder()) {
48+
agreements = await getAgreeementsForAH(user, {
49+
...filterSettings,
50+
limit: null,
51+
page: null,
52+
});
53+
} else if (user.isAdministrator() || user.canReadAll() || user.isRangeOfficer()) {
54+
if (!(filterSettings?.zoneInfo?.selectedZones?.length === 0)) {
7555
agreements = await Agreement.findWithAllRelations(
7656
db,
77-
{ zone_id: zones.map((z) => z.id) },
57+
{ 'ref_zone.id': filterSettings.zoneInfo?.selectedZones },
7858
{
7959
...filterSettings,
8060
limit: null,
8161
page: null,
8262
},
83-
false,
63+
true,
8464
);
85-
} else {
86-
throw errorWithCode('Unable to determine user role', 500);
8765
}
88-
totalItems = agreements.length;
66+
} else if (user.isDecisionMaker() || user.canReadDistrict()) {
67+
const districts = await UserDistricts.find(db, { user_id: user.id });
68+
const zones = await Zone.find(db, {
69+
district_id: districts.map((d) => d.id),
70+
});
71+
agreements = await Agreement.findWithAllRelations(
72+
db,
73+
{ zone_id: zones.map((z) => z.id) },
74+
{
75+
...filterSettings,
76+
limit: null,
77+
page: null,
78+
},
79+
false,
80+
);
81+
} else {
82+
throw errorWithCode('Unable to determine user role', 500);
83+
}
84+
85+
return agreements;
86+
};
87+
88+
// Search agreements by RAN, contact name, and client name. This is only used by Web
89+
router.get(
90+
'/search',
91+
asyncMiddleware(async (req, res) => {
92+
const { user, query } = req;
93+
const filterSettings = query.filterSettings ? JSON.parse(query.filterSettings) : {};
94+
const page = Number(filterSettings.page || 0);
95+
const limit = Number(filterSettings.limit || 10);
96+
97+
const agreementsAll = await getAgreements(user, filterSettings);
98+
const totalItems = agreementsAll.length;
8999
const startIndex = (page - 1) * limit;
90100
const endIndex = startIndex + limit;
91-
agreements = agreements.slice(startIndex, endIndex);
101+
const agreements = agreementsAll.slice(startIndex, endIndex);
102+
92103
agreements.map((agreement) => agreement.transformToV1());
93104
const totalPages = Math.ceil(totalItems / limit);
94105
const currentPage = page > totalPages ? totalPages : page;
@@ -103,6 +114,97 @@ router.get(
103114
}),
104115
);
105116

117+
// Export agreements as CSV
118+
router.get(
119+
'/export',
120+
asyncMiddleware(async (req, res) => {
121+
const { user, query } = req;
122+
123+
if (!user.isAdministrator() && !user.isRangeOfficer() && !user.isDecisionMaker() && !user.isReadOnly()) {
124+
throw errorWithCode('Unauthorized', 403);
125+
}
126+
127+
const filterSettings = query.filterSettings ? JSON.parse(query.filterSettings) : {};
128+
129+
let agreements = await getAgreements(user, filterSettings);
130+
// Filter out test agreements
131+
agreements = agreements.filter((agreement) => !agreement.forestFileId.startsWith(AGREEMENT_EXPORT.TEST_RAN_PREFIX));
132+
agreements.map((agreement) => agreement.transformToV1());
133+
134+
const csvData = agreements.map((agreement) => {
135+
// Sort clients: primary (A) first, then others
136+
const sortedClients = agreement.clients.sort((a, b) => {
137+
if (a.clientTypeCode === 'A') return -1;
138+
if (b.clientTypeCode === 'A') return 1;
139+
return 0;
140+
});
141+
142+
const primaryClient = sortedClients[0];
143+
const clientName = primaryClient?.name || '';
144+
145+
// Get up to 5 agreement holders (from user account names) and emails, fill remaining with empty strings
146+
const holders = Array(5)
147+
.fill('')
148+
.map((_, index) => {
149+
const client = sortedClients[index];
150+
if (!client) return '';
151+
// Use user name if available, otherwise blank
152+
if (client.userGivenName || client.userFamilyName) {
153+
return `${client.userGivenName} ${client.userFamilyName}`.trim();
154+
}
155+
return '';
156+
});
157+
const emails = Array(5)
158+
.fill('')
159+
.map((_, index) => sortedClients[index]?.email || '');
160+
161+
return {
162+
[AGREEMENT_EXPORT_COLUMNS.RAN]: agreement.forestFileId,
163+
[AGREEMENT_EXPORT_COLUMNS.LICENSE_TYPE]: Agreement.getLicenseTypeText(agreement),
164+
[AGREEMENT_EXPORT_COLUMNS.RANGE_NAME]: agreement.plan?.rangeName || '',
165+
[AGREEMENT_EXPORT_COLUMNS.LICENSEE]: clientName,
166+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_1]: holders[0],
167+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_1_EMAIL]: emails[0],
168+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_2]: holders[1],
169+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_2_EMAIL]: emails[1],
170+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_3]: holders[2],
171+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_3_EMAIL]: emails[2],
172+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_4]: holders[3],
173+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_4_EMAIL]: emails[3],
174+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_5]: holders[4],
175+
[AGREEMENT_EXPORT_COLUMNS.AGREEMENT_HOLDER_5_EMAIL]: emails[4],
176+
[AGREEMENT_EXPORT_COLUMNS.ZONE_AGROLOGIST]: agreement.zone?.user
177+
? `${agreement.zone.user.givenName} ${agreement.zone.user.familyName}`
178+
: '',
179+
[AGREEMENT_EXPORT_COLUMNS.ZONE]: agreement.zone?.code || '',
180+
[AGREEMENT_EXPORT_COLUMNS.DISTRICT]: agreement.zone?.district?.code || '',
181+
[AGREEMENT_EXPORT_COLUMNS.PLAN_STATUS]: agreement.plan?.status?.name || AGREEMENT_EXPORT.NO_PLAN_DEFAULT,
182+
[AGREEMENT_EXPORT_COLUMNS.PLAN_START_DATE]: agreement.plan?.planStartDate
183+
? moment(agreement.plan.planStartDate).format(AGREEMENT_EXPORT.DATE_FORMAT)
184+
: '',
185+
[AGREEMENT_EXPORT_COLUMNS.PLAN_END_DATE]: agreement.plan?.planEndDate
186+
? moment(agreement.plan.planEndDate).format(AGREEMENT_EXPORT.DATE_FORMAT)
187+
: '',
188+
[AGREEMENT_EXPORT_COLUMNS.USAGE_STATUS]: Agreement.getUsageStatusText(agreement.usageStatus),
189+
[AGREEMENT_EXPORT_COLUMNS.PERCENTAGE_USE]: agreement.percentageUse || 0,
190+
[AGREEMENT_EXPORT_COLUMNS.HAS_CURRENT_SCHEDULE]: agreement.hasCurrentSchedule
191+
? AGREEMENT_EXPORT.YES
192+
: AGREEMENT_EXPORT.NO,
193+
[AGREEMENT_EXPORT_COLUMNS.EXEMPTION_STATUS]: Agreement.getExemptionStatusText(agreement.exemptionStatus),
194+
};
195+
});
196+
197+
csv.stringify(csvData, { header: true }, (err, output) => {
198+
if (err) {
199+
throw err;
200+
}
201+
res.setHeader('Content-Type', AGREEMENT_EXPORT.CONTENT_TYPE);
202+
res.setHeader('Content-Disposition', `attachment; filename="${AGREEMENT_EXPORT.FILENAME}"`);
203+
res.status(200).send(output);
204+
});
205+
}),
206+
);
207+
106208
// Exemption routes nested under /agreement/:agreementId/exemption
107209
const exemptionBase = '/:agreementId/exemption';
108210
// Exemption history

0 commit comments

Comments
 (0)