Skip to content

Commit 422ebe9

Browse files
Merge pull request #2334 from zetkin/release-241108
241108 Release
2 parents 20b1c9b + be8416d commit 422ebe9

File tree

127 files changed

+7808
-3161
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+7808
-3161
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import mongoose from 'mongoose';
2+
import { NextRequest, NextResponse } from 'next/server';
3+
4+
import asOrgAuthorized from 'utils/api/asOrgAuthorized';
5+
import { CanvassAssignmentModel } from 'features/canvassAssignments/models';
6+
7+
type RouteMeta = {
8+
params: {
9+
areaId: string;
10+
assignmentId: string;
11+
orgId: string;
12+
personId: number;
13+
};
14+
};
15+
16+
export async function DELETE(request: NextRequest, { params }: RouteMeta) {
17+
return asOrgAuthorized(
18+
{
19+
orgId: params.orgId,
20+
request: request,
21+
roles: ['admin'],
22+
},
23+
24+
async ({ apiClient, orgId }) => {
25+
await mongoose.connect(process.env.MONGODB_URL || '');
26+
apiClient;
27+
orgId;
28+
29+
const assignmentModel = await CanvassAssignmentModel.findOne({
30+
_id: params.assignmentId,
31+
});
32+
33+
if (!assignmentModel) {
34+
return new NextResponse(null, { status: 404 });
35+
}
36+
37+
const filteredSessions = assignmentModel.sessions.filter((session) => {
38+
return !(
39+
session.personId == params.personId && session.areaId == params.areaId
40+
);
41+
});
42+
43+
assignmentModel.sessions = filteredSessions;
44+
45+
await assignmentModel.save();
46+
47+
return new NextResponse(null, { status: 204 });
48+
}
49+
);
50+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import mongoose from 'mongoose';
2+
import { NextRequest, NextResponse } from 'next/server';
3+
4+
import { AreaModel } from 'features/areas/models';
5+
import {
6+
CanvassAssignmentModel,
7+
PlaceModel,
8+
} from 'features/canvassAssignments/models';
9+
import {
10+
AreaCardData,
11+
AreaGraphData,
12+
Household,
13+
Visit,
14+
ZetkinCanvassSession,
15+
ZetkinPlace,
16+
} from 'features/canvassAssignments/types';
17+
import getAreaData from 'features/canvassAssignments/utils/getAreaData';
18+
import isPointInsidePolygon from 'features/canvassAssignments/utils/isPointInsidePolygon';
19+
import asOrgAuthorized from 'utils/api/asOrgAuthorized';
20+
import { ZetkinPerson } from 'utils/types/zetkin';
21+
import { ZetkinArea } from 'features/areas/types';
22+
23+
type RouteMeta = {
24+
params: {
25+
canvassAssId: string;
26+
orgId: string;
27+
};
28+
};
29+
30+
export async function GET(request: NextRequest, { params }: RouteMeta) {
31+
return asOrgAuthorized(
32+
{
33+
orgId: params.orgId,
34+
request: request,
35+
roles: ['admin', 'organizer'],
36+
},
37+
async ({ apiClient, orgId }) => {
38+
await mongoose.connect(process.env.MONGODB_URL || '');
39+
40+
const assignmentModel = await CanvassAssignmentModel.findOne({
41+
_id: params.canvassAssId,
42+
});
43+
44+
if (!assignmentModel) {
45+
return new NextResponse(null, { status: 404 });
46+
}
47+
48+
const sessions: ZetkinCanvassSession[] = [];
49+
50+
for await (const sessionData of assignmentModel.sessions) {
51+
const person = await apiClient.get<ZetkinPerson>(
52+
`/api/orgs/${orgId}/people/${sessionData.personId}`
53+
);
54+
const areaModel = await AreaModel.findOne({ _id: sessionData.areaId });
55+
56+
if (areaModel && person) {
57+
sessions.push({
58+
area: {
59+
description: areaModel.description,
60+
id: areaModel._id.toString(),
61+
organization: { id: orgId },
62+
points: areaModel.points,
63+
tags: [],
64+
title: areaModel.title,
65+
},
66+
assignee: person,
67+
assignment: {
68+
campaign: { id: assignmentModel.campId },
69+
end_date: assignmentModel.end_date,
70+
id: assignmentModel._id.toString(),
71+
metrics: assignmentModel.metrics.map((m) => ({
72+
definesDone: m.definesDone,
73+
description: m.description,
74+
id: m._id,
75+
kind: m.kind,
76+
question: m.question,
77+
})),
78+
organization: { id: assignmentModel.orgId },
79+
start_date: assignmentModel.start_date,
80+
title: assignmentModel.title,
81+
},
82+
});
83+
}
84+
}
85+
86+
const areas = sessions.map((session) => session.area);
87+
const uniqueAreas = [
88+
...new Map(areas.map((area) => [area.id, area])).values(),
89+
];
90+
91+
const allPlaceModels = await PlaceModel.find({ orgId });
92+
const allPlaces: ZetkinPlace[] = allPlaceModels.map((model) => ({
93+
description: model.description,
94+
households: model.households,
95+
id: model._id.toString(),
96+
orgId: orgId,
97+
position: model.position,
98+
title: model.title,
99+
}));
100+
101+
type PlaceWithAreaId = ZetkinPlace & { areaId: ZetkinArea['id'] };
102+
103+
//Find places in the given areas
104+
const placesInAreas: PlaceWithAreaId[] = [];
105+
uniqueAreas.forEach((area) => {
106+
allPlaces.forEach((place) => {
107+
const placeIsInArea = isPointInsidePolygon(
108+
{ lat: place.position.lat, lng: place.position.lng },
109+
area.points.map((point) => ({ lat: point[0], lng: point[1] }))
110+
);
111+
112+
if (placeIsInArea) {
113+
placesInAreas.push({ ...place, areaId: area.id });
114+
}
115+
});
116+
});
117+
118+
const metricThatDefinesDone = assignmentModel.metrics
119+
.find((metric) => metric.definesDone)
120+
?._id.toString();
121+
122+
const filteredVisitsInAllAreas: Visit[] = [];
123+
let firstVisit: Date = new Date();
124+
let lastVisit: Date = new Date();
125+
126+
allPlaces.forEach((place) => {
127+
const placeVisits = place.households
128+
.flatMap((household) => household.visits)
129+
.filter((visit) => visit.canvassAssId === params.canvassAssId);
130+
131+
filteredVisitsInAllAreas.push(...placeVisits);
132+
});
133+
134+
if (filteredVisitsInAllAreas.length > 0) {
135+
// Sort filtered visits by timestamp
136+
const sortedVisits = filteredVisitsInAllAreas.sort(
137+
(a, b) =>
138+
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
139+
);
140+
141+
firstVisit = new Date(sortedVisits[0].timestamp);
142+
lastVisit = new Date(sortedVisits[sortedVisits.length - 1].timestamp);
143+
}
144+
145+
const areaData: Record<
146+
string,
147+
{ area: { id: string; title: string | null }; data: AreaGraphData[] }
148+
> = {};
149+
150+
const areasDataList: AreaCardData[] = [];
151+
152+
const addedAreaIds = new Set<string>();
153+
const householdsOutsideAreasList: Household[] = [];
154+
155+
uniqueAreas.forEach((area) => {
156+
if (!areaData[area.id]) {
157+
areaData[area.id] = {
158+
area: { id: area.id, title: area.title },
159+
data: [],
160+
};
161+
}
162+
163+
const areaVisitsData: AreaGraphData[] = [];
164+
const householdList: Household[] = [];
165+
166+
allPlaces.forEach((place) => {
167+
const placeIsInArea = isPointInsidePolygon(
168+
{ lat: place.position.lat, lng: place.position.lng },
169+
area.points.map((point) => ({ lat: point[0], lng: point[1] }))
170+
);
171+
172+
if (placeIsInArea) {
173+
const filteredHouseholds = place.households.filter((household) => {
174+
return household.visits.filter(
175+
(visit) => visit.canvassAssId === params.canvassAssId
176+
);
177+
});
178+
householdList.push(...filteredHouseholds);
179+
}
180+
});
181+
182+
const visitsData = getAreaData(
183+
lastVisit,
184+
householdList,
185+
firstVisit,
186+
metricThatDefinesDone || ''
187+
);
188+
areaVisitsData.push(...visitsData);
189+
190+
areaData[area.id].data.push(...areaVisitsData);
191+
192+
if (!addedAreaIds.has(area.id)) {
193+
areasDataList.push(areaData[area.id]);
194+
addedAreaIds.add(area.id);
195+
}
196+
});
197+
198+
//rogue visits logic
199+
const idsOfPlacesInAreas = new Set(
200+
placesInAreas.map((place) => place.id)
201+
);
202+
const placesOutsideAreas = allPlaces.filter(
203+
(place) => !idsOfPlacesInAreas.has(place.id)
204+
);
205+
206+
placesOutsideAreas.forEach((place) => {
207+
place.households.forEach((household) => {
208+
household.visits.forEach((visit) => {
209+
if (visit.canvassAssId == params.canvassAssId) {
210+
householdsOutsideAreasList.push(household);
211+
}
212+
});
213+
});
214+
});
215+
216+
if (householdsOutsideAreasList.length > 0) {
217+
const visitsData = getAreaData(
218+
lastVisit,
219+
householdsOutsideAreasList,
220+
firstVisit,
221+
metricThatDefinesDone || ''
222+
);
223+
const noAreaData = (areaData['noArea'] = {
224+
area: { id: 'noArea', title: 'noArea' },
225+
data: visitsData,
226+
});
227+
228+
areasDataList.push(noAreaData);
229+
}
230+
231+
const areasDataArray: AreaCardData[] = Object.values(areasDataList);
232+
233+
return NextResponse.json({
234+
data: areasDataArray,
235+
});
236+
}
237+
);
238+
}

0 commit comments

Comments
 (0)