From 355e9cbf7370979d741deb84990dab38d348a21a Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:15:37 +0200 Subject: [PATCH 01/44] remove rating route, add new properties to Visit type, update models and routes, hide and remove unnecessary elements in the UI --- .../households/[householdId]/ratings/route.ts | 63 ------------------- .../households/[householdId]/visits/route.ts | 4 +- src/features/areas/models.ts | 4 +- src/features/areas/types.ts | 4 +- 4 files changed, 9 insertions(+), 66 deletions(-) delete mode 100644 src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/ratings/route.ts diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/ratings/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/ratings/route.ts deleted file mode 100644 index f05f1dd9d..000000000 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/ratings/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import mongoose from 'mongoose'; -import { NextRequest, NextResponse } from 'next/server'; - -import { PlaceModel } from 'features/areas/models'; -import asOrgAuthorized from 'utils/api/asOrgAuthorized'; - -type RouteMeta = { - params: { - householdId: string; - orgId: string; - placeId: string; - }; -}; - -export async function POST(request: NextRequest, { params }: RouteMeta) { - return asOrgAuthorized( - { - orgId: params.orgId, - request: request, - roles: ['admin'], - }, - async ({ orgId }) => { - await mongoose.connect(process.env.MONGODB_URL || ''); - - const payload = await request.json(); - - const model = await PlaceModel.findOneAndUpdate( - { _id: params.placeId, orgId }, - { - $push: { - 'households.$[elem].ratings': { - id: new mongoose.Types.ObjectId().toString(), - rate: payload.rate, - timestamp: payload.timestamp, - }, - }, - }, - { - arrayFilters: [{ 'elem.id': { $eq: params.householdId } }], - new: true, - } - ); - - if (!model) { - return new NextResponse(null, { status: 404 }); - } - - await model.save(); - - return NextResponse.json({ - data: { - description: model.description, - households: model.households, - id: model._id.toString(), - orgId: orgId, - position: model.position, - title: model.title, - type: model.type, - }, - }); - } - ); -} diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts index 2cf131a8d..a33e576ae 100644 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts +++ b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts @@ -30,8 +30,10 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { $push: { 'households.$[elem].visits': { canvassAssId: payload.canvassAssId, + doorWasOpened: payload.doorWasOpened, id: new mongoose.Types.ObjectId().toString(), - rating: payload.rating, + missionAccomplished: payload.missionAccomplished, + noteToOfficial: payload.noteToOfficial, timestamp: payload.timestamp, }, }, diff --git a/src/features/areas/models.ts b/src/features/areas/models.ts index e61419479..77f8cce90 100644 --- a/src/features/areas/models.ts +++ b/src/features/areas/models.ts @@ -56,8 +56,10 @@ const placeSchema = new mongoose.Schema({ { _id: false, canvassAssId: String, + doorWasOpened: Boolean, id: String, - rating: String, + missionAccomplished: Boolean, + noteToOfficial: String, timestamp: String, }, ], diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index 6af250d74..c08538f78 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -15,8 +15,10 @@ export type PointData = [number, number]; export type Visit = { canvassAssId: string | null; + doorWasOpened: boolean; id: string; - rating?: 'good' | 'bad'; + missionAccomplished: boolean; + noteToOfficial: string | null; timestamp: string; }; From 3fb5817fd4d7057e3ed7f37b8118b34d83336ce7 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:16:17 +0200 Subject: [PATCH 02/44] work in the ui, implement wizard step --- src/features/areas/components/PlaceDialog.tsx | 231 ++++++++---------- .../areas/components/PublicAreaMap.tsx | 10 +- 2 files changed, 101 insertions(+), 140 deletions(-) diff --git a/src/features/areas/components/PlaceDialog.tsx b/src/features/areas/components/PlaceDialog.tsx index 3cd2c0dc1..6ad1b1dbc 100644 --- a/src/features/areas/components/PlaceDialog.tsx +++ b/src/features/areas/components/PlaceDialog.tsx @@ -1,16 +1,9 @@ -import { - ArrowBackIos, - Check, - Edit, - ThumbDown, - ThumbUp, -} from '@mui/icons-material'; +import { ArrowBackIos, Check, Close, Edit } from '@mui/icons-material'; import { FC, useState } from 'react'; import { Box, Button, ButtonGroup, - CircularProgress, Dialog, Divider, FormControl, @@ -26,39 +19,35 @@ import { isWithinLast24Hours } from '../utils/isWithinLast24Hours'; import messageIds from '../l10n/messageIds'; import usePlaceMutations from '../hooks/usePlaceMutations'; import { ZetkinPlace } from '../types'; -import ZUIDateTime from 'zui/ZUIDateTime'; import { Msg, useMessages } from 'core/i18n'; type PlaceDialogProps = { - canvassAssId: string | null; dialogStep: 'place' | 'edit' | 'household'; onClose: () => void; onEdit: () => void; onSelectHousehold: () => void; onUpdateDone: () => void; + onWizard: () => void; open: boolean; orgId: number; place: ZetkinPlace; }; const PlaceDialog: FC = ({ - canvassAssId, dialogStep, onClose, onEdit, onUpdateDone, onSelectHousehold, + onWizard, open, orgId, place, }) => { - const { - addHousehold, - addVisit, - updateHousehold, - updatePlace, - isAddVisitLoading, - } = usePlaceMutations(orgId, place.id); + const { addHousehold, updateHousehold, updatePlace } = usePlaceMutations( + orgId, + place.id + ); const messages = useMessages(messageIds); const [selectedHouseholdId, setSelectedHouseholdId] = useState( @@ -72,6 +61,8 @@ const PlaceDialog: FC = ({ const [editingHouseholdTitle, setEditingHouseholdTitle] = useState(false); const [householdTitle, setHousholdTitle] = useState(''); + const [wizardStep, setWizardStep] = useState<1 | 2 | 3>(1); + const handleChange = (event: SelectChangeEvent) => { setType(event.target.value as 'address' | 'misc'); }; @@ -80,7 +71,7 @@ const PlaceDialog: FC = ({ (household) => household.id == selectedHouseholdId ); - const sortedVisits = + /*const sortedVisits = selectedHousehold?.visits.toSorted((a, b) => { const dateA = new Date(a.timestamp); const dateB = new Date(b.timestamp); @@ -91,7 +82,7 @@ const PlaceDialog: FC = ({ } else { return 0; } - }) || []; + }) || [];*/ const nothingHasBeenEdited = dialogStep == 'edit' && @@ -134,29 +125,41 @@ const PlaceDialog: FC = ({ )} {selectedHousehold && dialogStep == 'household' && ( - - + + { + onUpdateDone(); + }} + sx={{ marginRight: 2 }} + /> + {selectedHousehold.title || ( + + )} + + + + sx={{ marginRight: 1 }} + variant="outlined" + > + + + + )} {dialogStep === 'place' && ( + )} @@ -262,7 +277,7 @@ const PlaceDialog: FC = ({ const visitedRecently = isWithinLast24Hours( household.visits.map((t) => t.timestamp) ); - const mostRecentVisit = household.visits.toSorted( + /*const mostRecentVisit = household.visits.toSorted( (a, b) => { const dateA = new Date(a.timestamp); const dateB = new Date(b.timestamp); @@ -274,7 +289,7 @@ const PlaceDialog: FC = ({ return 0; } } - )[0]; + )[0];*/ return ( = ({ )} - {visitedRecently ? ( - - {mostRecentVisit.rating && ( - - {mostRecentVisit.rating == 'good' ? ( - - ) : ( - - )} - - )} - - - ) : ( - '' - )} + {visitedRecently ? : ''} ); })} @@ -317,6 +317,16 @@ const PlaceDialog: FC = ({ )} + {wizardStep === 1 && dialogStep === 'wizard' && ( + + Did they open the door? + + + + + + )} + {selectedHousehold && dialogStep == 'household' && ( = ({ flexGrow={1} overflow="hidden" > - { + onWizard(), setWizardStep(1); + }} + variant="contained" > - t.timestamp) - )} - fullWidth - sx={{ alignSelf: 'center', display: 'flex' }} - variant="outlined" - > - {!isAddVisitLoading && ( - <> - - - - )} - {isAddVisitLoading && ( - - )} - - - + + {/* = ({ }} > - {visit.rating && ( - - {visit.rating === 'good' ? ( - - ) : ( - - )} - - )} ))} - + */} )} @@ -465,24 +427,23 @@ const PlaceDialog: FC = ({ )} - + {dialogStep !== 'household' && dialogStep !== 'wizard' && ( + + )} {dialogStep == 'edit' && ( + )} {dialogStep === 'edit' && ( = ({ /> )} - {dialogStep === 'wizard' && ( - - )} + + + {/** BODY BEGINS HERE */} + {dialogStep === 'edit' && ( = ({ )} - {wizardStep === 1 && dialogStep === 'wizard' && ( - - Did they open the door? - - - - - - )} - {selectedHousehold && dialogStep == 'household' && ( = ({ + + + )} + {step == 2 && ( + + + + + )} + {step == 3 && ( + + )} + + + ); +}; + +export default VisitWizard; From 350f2d3d24591e10db70cb0a510ab067339fb6a4 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Wed, 9 Oct 2024 16:27:20 +0200 Subject: [PATCH 04/44] Add VisitWizard component and move logic into there & add new data into POST of new Visit. --- src/features/areas/components/PlaceDialog.tsx | 69 +++++----- .../areas/components/PublicAreaMap.tsx | 3 +- src/features/areas/components/VisitWizard.tsx | 125 ++++++++++++++++++ 3 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 src/features/areas/components/VisitWizard.tsx diff --git a/src/features/areas/components/PlaceDialog.tsx b/src/features/areas/components/PlaceDialog.tsx index 6ad1b1dbc..41e9d220d 100644 --- a/src/features/areas/components/PlaceDialog.tsx +++ b/src/features/areas/components/PlaceDialog.tsx @@ -3,7 +3,6 @@ import { FC, useState } from 'react'; import { Box, Button, - ButtonGroup, Dialog, Divider, FormControl, @@ -20,9 +19,11 @@ import messageIds from '../l10n/messageIds'; import usePlaceMutations from '../hooks/usePlaceMutations'; import { ZetkinPlace } from '../types'; import { Msg, useMessages } from 'core/i18n'; +import VisitWizard, { WizardStep } from './VisitWizard'; type PlaceDialogProps = { - dialogStep: 'place' | 'edit' | 'household'; + canvassAssId: string | null; + dialogStep: 'place' | 'edit' | 'household' | 'wizard'; onClose: () => void; onEdit: () => void; onSelectHousehold: () => void; @@ -34,6 +35,7 @@ type PlaceDialogProps = { }; const PlaceDialog: FC = ({ + canvassAssId, dialogStep, onClose, onEdit, @@ -44,10 +46,9 @@ const PlaceDialog: FC = ({ orgId, place, }) => { - const { addHousehold, updateHousehold, updatePlace } = usePlaceMutations( - orgId, - place.id - ); + const { addVisit, addHousehold, updateHousehold, updatePlace } = + usePlaceMutations(orgId, place.id); + const messages = useMessages(messageIds); const [selectedHouseholdId, setSelectedHouseholdId] = useState( @@ -61,7 +62,7 @@ const PlaceDialog: FC = ({ const [editingHouseholdTitle, setEditingHouseholdTitle] = useState(false); const [householdTitle, setHousholdTitle] = useState(''); - const [wizardStep, setWizardStep] = useState<1 | 2 | 3>(1); + const [wizardStep, setWizardStep] = useState(null); const handleChange = (event: SelectChangeEvent) => { setType(event.target.value as 'address' | 'misc'); @@ -106,6 +107,9 @@ const PlaceDialog: FC = ({ } }; + const showWizard = + selectedHousehold && dialogStep == 'wizard' && !!wizardStep; + return ( @@ -168,6 +172,11 @@ const PlaceDialog: FC = ({ )} )} + {dialogStep == 'wizard' && ( + + )} {dialogStep === 'edit' && ( = ({ /> )} - {dialogStep === 'wizard' && ( - - )} + + + {/** BODY BEGINS HERE */} + {dialogStep === 'edit' && ( = ({ )} - {wizardStep === 1 && dialogStep === 'wizard' && ( - - Did they open the door? - - - - - - )} - {selectedHousehold && dialogStep == 'household' && ( = ({ + + + )} + {step == 2 && ( + + + + + )} + {step == 3 && ( + + )} + + + ); +}; + +export default VisitWizard; From 81364284dd0d26ff3ee9317679b61c4817e6bf1b Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Thu, 10 Oct 2024 09:44:01 +0200 Subject: [PATCH 05/44] Create "previous"-button. --- src/features/areas/components/VisitWizard.tsx | 113 ++++++++++++------ 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/src/features/areas/components/VisitWizard.tsx b/src/features/areas/components/VisitWizard.tsx index b080ed4fd..cb6154baf 100644 --- a/src/features/areas/components/VisitWizard.tsx +++ b/src/features/areas/components/VisitWizard.tsx @@ -1,4 +1,11 @@ -import { Box, Button, ButtonGroup, TextField, Typography } from '@mui/material'; +import { + Box, + Button, + TextField, + ToggleButton, + ToggleButtonGroup, + Typography, +} from '@mui/material'; import { FC, useState } from 'react'; import { Visit } from '../types'; @@ -29,6 +36,8 @@ const VisitWizard: FC = ({ >(null); const [doorWasOpened, setDoorWasOpened] = useState(null); + const showPreviousButton = step && step != 1; + return ( = ({ height="100%" justifyContent="center" > + {showPreviousButton && ( + + )} <> @@ -59,44 +91,55 @@ const VisitWizard: FC = ({ )} {step == 1 && ( - - - - + { + if (value != null) { + setDoorWasOpened(value); + + if (value) { + onStepChange(2); + } else { + onStepChange(3); + } + } + + //If user has come back and clicks the same option again + if (typeof doorWasOpened == 'boolean' && value == null) { + if (doorWasOpened) { + onStepChange(2); + } else { + onStepChange(3); + } + } + }} + value={doorWasOpened} + > + Yes + No + )} {step == 2 && ( - - - - + } + }} + value={missionAccomplished} + > + Yes + No + )} {step == 3 && ( - - - )} - {dialogStep === 'place' && ( - - )} - - )} - {dialogStep == 'wizard' && ( - - )} - {dialogStep === 'edit' && ( - - - - )} - - - - - {/** BODY BEGINS HERE */} - - - {dialogStep === 'edit' && ( - - setTitle(ev.target.value)} - /> - - - - - - - setDescription(ev.target.value)} - rows={5} - /> - - )} - {place && dialogStep == 'place' && ( - - - - - - - {place.description || ( - - )} - - - - - - - - {place.households.map((household) => { - const visitedRecently = isWithinLast24Hours( - household.visits.map((t) => t.timestamp) - ); - /*const mostRecentVisit = household.visits.toSorted( - (a, b) => { - const dateA = new Date(a.timestamp); - const dateB = new Date(b.timestamp); - if (dateA > dateB) { - return -1; - } else if (dateB > dateA) { - return 1; - } else { - return 0; - } - } - )[0];*/ - - return ( - { - setSelectedHouseholdId(household.id); - onSelectHousehold(); - }} - width="100%" - > - - {household.title || ( - - )} - - {visitedRecently ? : ''} - - ); - })} - - - - )} - {selectedHousehold && dialogStep == 'household' && ( - - - {editingHouseholdTitle && ( - - setHousholdTitle(ev.target.value)} - placeholder="Household title" - value={householdTitle} - /> - - - - )} - - - - - {/* - - - - {sortedVisits.length} - - - - {sortedVisits.length == 0 && ( - - )} - {sortedVisits.map((visit) => ( - - {messages.place.visitLog()} - - - - - ))} - */} - - - )} - {showWizard && ( - - addVisit(selectedHousehold.id, { - ...report, - canvassAssId, - timestamp: new Date().toISOString(), - }) - } - onStepChange={setWizardStep} - step={wizardStep} - /> - )} - - - {/**FOOTER BEGINS HERE */} - - - {dialogStep === 'place' && ( - - )} - {dialogStep !== 'household' && dialogStep !== 'wizard' && ( - - )} - {dialogStep == 'edit' && ( - - )} - - - - ); -}; - -export default PlaceDialog; diff --git a/src/features/areas/components/PlaceDialog/EditPlace.tsx b/src/features/areas/components/PlaceDialog/EditPlace.tsx new file mode 100644 index 000000000..b9b540ef7 --- /dev/null +++ b/src/features/areas/components/PlaceDialog/EditPlace.tsx @@ -0,0 +1,78 @@ +import { + Box, + FormControl, + InputLabel, + MenuItem, + Select, + TextField, +} from '@mui/material'; +import { FC } from 'react'; + +import { Msg, useMessages } from 'core/i18n'; +import messageIds from 'features/areas/l10n/messageIds'; +import { PlaceType } from '.'; + +type EditPlaceProps = { + description: string; + onDescriptionChange: (newDescription: string) => void; + onTitleChange: (newTitle: string) => void; + onTypeChange: (newType: PlaceType) => void; + title: string; + type: PlaceType; +}; + +const EditPlace: FC = ({ + description, + onDescriptionChange, + onTitleChange, + onTypeChange, + title, + type, +}) => { + const messages = useMessages(messageIds); + return ( + + onTitleChange(ev.target.value)} + /> + + + + + + + onDescriptionChange(ev.target.value)} + rows={5} + /> + + ); +}; + +export default EditPlace; diff --git a/src/features/areas/components/PlaceDialog/Household.tsx b/src/features/areas/components/PlaceDialog/Household.tsx new file mode 100644 index 000000000..a132bf7f5 --- /dev/null +++ b/src/features/areas/components/PlaceDialog/Household.tsx @@ -0,0 +1,104 @@ +import { Box, Button, TextField } from '@mui/material'; +import { FC } from 'react'; + +import { HouseholdPatchBody } from 'features/areas/types'; + +type HouseholdProps = { + editingHouseholdTitle: boolean; + householdTitle: string; + onEditHouseholdTitleEnd: () => void; + onHouseholdTitleChange: (newTitle: string) => void; + onHouseholdUpdate: (data: HouseholdPatchBody) => void; + onWizardStart: () => void; +}; + +const Household: FC = ({ + householdTitle, + onHouseholdTitleChange, + editingHouseholdTitle, + onEditHouseholdTitleEnd, + onHouseholdUpdate, + onWizardStart, +}) => { + return ( + + + {editingHouseholdTitle && ( + + onHouseholdTitleChange(ev.target.value)} + placeholder="Household title" + value={householdTitle} + /> + + + + )} + + + + + {/* + + + + {sortedVisits.length} + + + + {sortedVisits.length == 0 && ( + + )} + {sortedVisits.map((visit) => ( + + {messages.place.visitLog()} + + + + + ))} + */} + + + ); +}; + +export default Household; diff --git a/src/features/areas/components/PlaceDialog/Place.tsx b/src/features/areas/components/PlaceDialog/Place.tsx new file mode 100644 index 000000000..93b73dcbc --- /dev/null +++ b/src/features/areas/components/PlaceDialog/Place.tsx @@ -0,0 +1,92 @@ +import { Check } from '@mui/icons-material'; +import { Box, Divider, Typography } from '@mui/material'; +import { FC } from 'react'; + +import { Msg } from 'core/i18n'; +import { ZetkinPlace } from 'features/areas/types'; +import messageIds from 'features/areas/l10n/messageIds'; +import { isWithinLast24Hours } from 'features/areas/utils/isWithinLast24Hours'; + +type PlaceProps = { + onSelectHousehold: (householdId: string) => void; + place: ZetkinPlace; +}; + +const Place: FC = ({ onSelectHousehold, place }) => { + return ( + + + + + + + {place.description || } + + + + + + + + {place.households.map((household) => { + const visitedRecently = isWithinLast24Hours( + household.visits.map((t) => t.timestamp) + ); + /*const mostRecentVisit = household.visits.toSorted( + (a, b) => { + const dateA = new Date(a.timestamp); + const dateB = new Date(b.timestamp); + if (dateA > dateB) { + return -1; + } else if (dateB > dateA) { + return 1; + } else { + return 0; + } + } + )[0];*/ + + return ( + { + onSelectHousehold(household.id); + }} + width="100%" + > + + {household.title || ( + + )} + + {visitedRecently ? : ''} + + ); + })} + + + + ); +}; + +export default Place; diff --git a/src/features/areas/components/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx similarity index 99% rename from src/features/areas/components/VisitWizard.tsx rename to src/features/areas/components/PlaceDialog/VisitWizard.tsx index cb6154baf..915862b88 100644 --- a/src/features/areas/components/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -8,7 +8,7 @@ import { } from '@mui/material'; import { FC, useState } from 'react'; -import { Visit } from '../types'; +import { Visit } from '../../types'; export type WizardStep = 1 | 2 | 3; diff --git a/src/features/areas/components/PlaceDialog/index.tsx b/src/features/areas/components/PlaceDialog/index.tsx new file mode 100644 index 000000000..e4b7e233a --- /dev/null +++ b/src/features/areas/components/PlaceDialog/index.tsx @@ -0,0 +1,272 @@ +import { ArrowBackIos, Close, Edit } from '@mui/icons-material'; +import { FC, useState } from 'react'; +import { + Box, + Button, + Dialog, + Divider, + IconButton, + Typography, +} from '@mui/material'; + +import messageIds from '../../l10n/messageIds'; +import usePlaceMutations from '../../hooks/usePlaceMutations'; +import { HouseholdPatchBody, ZetkinPlace } from '../../types'; +import { Msg, useMessages } from 'core/i18n'; +import VisitWizard, { WizardStep } from './VisitWizard'; +import EditPlace from './EditPlace'; +import Place from './Place'; +import Household from './Household'; + +export type PlaceType = 'address' | 'misc'; + +type PlaceDialogProps = { + canvassAssId: string | null; + dialogStep: 'place' | 'edit' | 'household' | 'wizard'; + onClose: () => void; + onEdit: () => void; + onSelectHousehold: () => void; + onUpdateDone: () => void; + onWizard: () => void; + open: boolean; + orgId: number; + place: ZetkinPlace; +}; + +const PlaceDialog: FC = ({ + canvassAssId, + dialogStep, + onClose, + onEdit, + onUpdateDone, + onSelectHousehold, + onWizard, + open, + orgId, + place, +}) => { + const { addVisit, addHousehold, updateHousehold, updatePlace } = + usePlaceMutations(orgId, place.id); + + const messages = useMessages(messageIds); + + const [selectedHouseholdId, setSelectedHouseholdId] = useState( + null + ); + const [description, setDescription] = useState( + place.description ?? '' + ); + const [title, setTitle] = useState(place.title ?? ''); + const [type, setType] = useState(place.type); + const [editingHouseholdTitle, setEditingHouseholdTitle] = useState(false); + const [householdTitle, setHousholdTitle] = useState(''); + + const [wizardStep, setWizardStep] = useState(null); + + const selectedHousehold = place.households.find( + (household) => household.id == selectedHouseholdId + ); + + /*const sortedVisits = + selectedHousehold?.visits.toSorted((a, b) => { + const dateA = new Date(a.timestamp); + const dateB = new Date(b.timestamp); + if (dateA > dateB) { + return -1; + } else if (dateB > dateA) { + return 1; + } else { + return 0; + } + }) || [];*/ + + const nothingHasBeenEdited = + dialogStep == 'edit' && + title == place.title && + type == place.type && + (description == place.description || (!description && !place.description)); + + const saveButtonDisabled = nothingHasBeenEdited; + + const showWizard = + selectedHousehold && dialogStep == 'wizard' && !!wizardStep; + + return ( + + + + {dialogStep == 'place' && ( + <> + + {place?.title || } + + + + + + + + + + + )} + {dialogStep == 'edit' && ( + <> + + + + + + + + )} + {dialogStep == 'household' && selectedHousehold && ( + + + + { + onUpdateDone(); + }} + /> + + + {selectedHousehold.title || ( + + )} + + + + { + setHousholdTitle(selectedHousehold.title); + setEditingHouseholdTitle(true); + }} + sx={{ marginRight: 1 }} + > + + + + + )} + {dialogStep == 'wizard' && selectedHousehold && ( + + { + setWizardStep(null); + onSelectHousehold(); + }} + > + + + + {selectedHousehold.title || ( + + )} + + + )} + + + + {place && dialogStep == 'place' && ( + { + setSelectedHouseholdId(householdId); + onSelectHousehold(); + }} + place={place} + /> + )} + {dialogStep === 'edit' && ( + + setDescription(newDescription) + } + onTitleChange={(newTitle) => setTitle(newTitle)} + onTypeChange={(newType) => setType(newType)} + title={title} + type={type} + /> + )} + {selectedHousehold && dialogStep == 'household' && ( + setEditingHouseholdTitle(false)} + onHouseholdTitleChange={(newTitle) => setHousholdTitle(newTitle)} + onHouseholdUpdate={(data: HouseholdPatchBody) => + updateHousehold(selectedHousehold.id, data) + } + onWizardStart={() => { + onWizard(); + setWizardStep(1); + }} + /> + )} + {showWizard && ( + + addVisit(selectedHousehold.id, { + ...report, + canvassAssId, + timestamp: new Date().toISOString(), + }) + } + onStepChange={setWizardStep} + step={wizardStep} + /> + )} + + + {dialogStep === 'place' && ( + + )} + {dialogStep == 'edit' && ( + + )} + + + + ); +}; + +export default PlaceDialog; diff --git a/src/features/areas/components/PublicAreaMap.tsx b/src/features/areas/components/PublicAreaMap.tsx index f95278571..a160af7c9 100644 --- a/src/features/areas/components/PublicAreaMap.tsx +++ b/src/features/areas/components/PublicAreaMap.tsx @@ -22,11 +22,11 @@ import { Msg } from 'core/i18n'; import messageIds from '../l10n/messageIds'; import { CreatePlaceCard } from './CreatePlaceCard'; import { DivIconMarker } from 'features/events/components/LocationModal/DivIconMarker'; -import PlaceDialog from './PlaceDialog'; import useCreatePlace from '../hooks/useCreatePlace'; import usePlaces from '../hooks/usePlaces'; import getCrosshairPositionOnMap from '../utils/getCrosshairPositionOnMap'; import MarkerIcon from '../utils/markerIcon'; +import PlaceDialog from './PlaceDialog'; const useStyles = makeStyles((theme) => ({ '@keyframes ghostMarkerBounce': { From eb21bc4e63a2fd11991278d2e38b71d18b57b402 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Thu, 10 Oct 2024 11:53:00 +0200 Subject: [PATCH 07/44] First logic for showing previous desicion with "change" buttons. --- .../components/PlaceDialog/VisitWizard.tsx | 117 +++++++++++++++--- 1 file changed, 97 insertions(+), 20 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index 915862b88..81911113a 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -47,26 +47,103 @@ const VisitWizard: FC = ({ justifyContent="center" > {showPreviousButton && ( - + + {(step == 2 || step == 3) && doorWasOpened && ( + + the door was opened + + + )} + {step == 3 && doorWasOpened && missionAccomplished && ( + + mission was accomplished! + + + )} + {step == 3 && doorWasOpened && !missionAccomplished && ( + + mission was not accomplished + + + )} + {step == 3 && !doorWasOpened && ( + + the door was not opened + + + )} + )} <> Date: Thu, 10 Oct 2024 12:29:31 +0200 Subject: [PATCH 08/44] Refactor logic of display of previous choices. --- .../components/PlaceDialog/VisitWizard.tsx | 319 ++++++++---------- 1 file changed, 136 insertions(+), 183 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index 81911113a..06cba26e5 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -17,6 +17,20 @@ type Report = Pick< 'missionAccomplished' | 'doorWasOpened' | 'noteToOfficial' >; +type PreviousMessageProps = { + message: string; + onClick: () => void; +}; + +const PreviousMessage: FC = ({ message, onClick }) => { + return ( + + {message} + + + ); +}; + type VisitWizardProps = { onExit: () => void; onRecordVisit: (report: Report) => void; @@ -30,13 +44,13 @@ const VisitWizard: FC = ({ onStepChange, step, }) => { - const [noteToOfficial, setNoteToOfficial] = useState(null); + const [noteToOfficial, setNoteToOfficial] = useState(''); const [missionAccomplished, setMissionAccomplished] = useState< boolean | null >(null); const [doorWasOpened, setDoorWasOpened] = useState(null); - const showPreviousButton = step && step != 1; + const showPrevious = step && step != 1; return ( = ({ height="100%" justifyContent="center" > - {showPreviousButton && ( + {showPrevious && ( - {(step == 2 || step == 3) && doorWasOpened && ( - - the door was opened - - - )} - {step == 3 && doorWasOpened && missionAccomplished && ( - - mission was accomplished! - - - )} - {step == 3 && doorWasOpened && !missionAccomplished && ( - - mission was not accomplished - - - )} - {step == 3 && !doorWasOpened && ( - - the door was not opened - - - )} - - )} - <> - - {step == 1 && Did they open the door?} - {step == 2 && Did you have a conversation?} - {step == 3 && ( - - - Did something happen that you need to report to an official? - - setNoteToOfficial(ev.target.value)} - value={noteToOfficial} - /> - - )} - - {step == 1 && ( - { - if (value != null) { - setDoorWasOpened(value); - - if (value) { - onStepChange(2); - } else { - onStepChange(3); - } + {(step == 2 || step == 3) && ( + { + if (step == 2) { + onStepChange(1); + setMissionAccomplished(null); + } else if ( + step == 3 && + (doorWasOpened == true || doorWasOpened == null) + ) { + onStepChange(1); + setMissionAccomplished(null); + setNoteToOfficial(''); } else { - onStepChange(3); + onStepChange(1); + setNoteToOfficial(''); } + }} + /> + )} + {step == 3 && doorWasOpened && ( + - Yes - No - + onClick={() => { + onStepChange(2); + setNoteToOfficial(''); + }} + /> + )} + + )} + + {step == 1 && Did they open the door?} + {step == 2 && Did you have a conversation?} + {step == 3 && ( + + + Did something happen that you need to report to an official? + + setNoteToOfficial(ev.target.value)} + value={noteToOfficial} + /> + )} - {step == 2 && ( - { - if (value != null) { - setMissionAccomplished(value); + + {step == 1 && ( + { + if (value != null) { + setDoorWasOpened(value); + + if (value) { + onStepChange(2); + } else { onStepChange(3); } + } - //If user has come back and clicks the same option again - if (typeof missionAccomplished == 'boolean' && value == null) { + //If user has come back and clicks the same option again + if (typeof doorWasOpened == 'boolean' && value == null) { + if (doorWasOpened) { + onStepChange(2); + } else { onStepChange(3); } - }} - value={missionAccomplished} - > - Yes - No - - )} - {step == 3 && ( - - )} - + } + }} + value={doorWasOpened} + > + Yes + No + + )} + {step == 2 && ( + { + if (value != null) { + setMissionAccomplished(value); + onStepChange(3); + } + + //If user has come back and clicks the same option again + if (typeof missionAccomplished == 'boolean' && value == null) { + onStepChange(3); + } + }} + value={missionAccomplished} + > + Yes + No + + )} + {step == 3 && ( + + )} ); }; From 7baa1dd3c8713ef77ad10885fcd4b04409540fcd Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Thu, 10 Oct 2024 13:09:43 +0200 Subject: [PATCH 09/44] Disable "record visit" button and show message if household was visited within the past 24 hours. --- .../areas/components/PlaceDialog/Household.tsx | 10 +++++++++- .../areas/components/PlaceDialog/index.tsx | 16 ++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/Household.tsx b/src/features/areas/components/PlaceDialog/Household.tsx index a132bf7f5..874172b43 100644 --- a/src/features/areas/components/PlaceDialog/Household.tsx +++ b/src/features/areas/components/PlaceDialog/Household.tsx @@ -10,6 +10,7 @@ type HouseholdProps = { onHouseholdTitleChange: (newTitle: string) => void; onHouseholdUpdate: (data: HouseholdPatchBody) => void; onWizardStart: () => void; + visitedRecently: boolean; }; const Household: FC = ({ @@ -19,6 +20,7 @@ const Household: FC = ({ onEditHouseholdTitleEnd, onHouseholdUpdate, onWizardStart, + visitedRecently, }) => { return ( = ({ justifyContent="flex-end" overflow="hidden" > - diff --git a/src/features/areas/components/PlaceDialog/index.tsx b/src/features/areas/components/PlaceDialog/index.tsx index e4b7e233a..012349bbb 100644 --- a/src/features/areas/components/PlaceDialog/index.tsx +++ b/src/features/areas/components/PlaceDialog/index.tsx @@ -17,6 +17,7 @@ import VisitWizard, { WizardStep } from './VisitWizard'; import EditPlace from './EditPlace'; import Place from './Place'; import Household from './Household'; +import { isWithinLast24Hours } from 'features/areas/utils/isWithinLast24Hours'; export type PlaceType = 'address' | 'misc'; @@ -135,12 +136,12 @@ const PlaceDialog: FC = ({ {dialogStep == 'household' && selectedHousehold && ( - - { - onUpdateDone(); - }} - /> + { + onUpdateDone(); + }} + > + {selectedHousehold.title || ( @@ -215,6 +216,9 @@ const PlaceDialog: FC = ({ onWizard(); setWizardStep(1); }} + visitedRecently={isWithinLast24Hours( + selectedHousehold.visits.map((t) => t.timestamp) + )} /> )} {showWizard && ( From 8aeb377cd94d93b4163f9c6f74f0e03e4969ff58 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Thu, 10 Oct 2024 14:23:21 +0200 Subject: [PATCH 10/44] Add some test styling to the "previous" items. --- .../areas/components/PlaceDialog/VisitWizard.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index 06cba26e5..f1ffddf69 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -24,7 +24,15 @@ type PreviousMessageProps = { const PreviousMessage: FC = ({ message, onClick }) => { return ( - + {message} From a0bda370889672f5a4a2e2c528233a2fc29f6f9b Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Thu, 10 Oct 2024 14:47:08 +0200 Subject: [PATCH 11/44] Add logic and UI to log visit directly from "place" screen. --- .../areas/components/PlaceDialog/index.tsx | 88 ++++++++++++++++++- .../areas/components/PublicAreaMap.tsx | 12 ++- src/features/areas/l10n/messageIds.ts | 1 + 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/index.tsx b/src/features/areas/components/PlaceDialog/index.tsx index 012349bbb..c10a4ef66 100644 --- a/src/features/areas/components/PlaceDialog/index.tsx +++ b/src/features/areas/components/PlaceDialog/index.tsx @@ -1,4 +1,4 @@ -import { ArrowBackIos, Close, Edit } from '@mui/icons-material'; +import { ArrowBackIos, Check, Close, Edit } from '@mui/icons-material'; import { FC, useState } from 'react'; import { Box, @@ -18,14 +18,16 @@ import EditPlace from './EditPlace'; import Place from './Place'; import Household from './Household'; import { isWithinLast24Hours } from 'features/areas/utils/isWithinLast24Hours'; +import { PlaceDialogStep } from '../PublicAreaMap'; export type PlaceType = 'address' | 'misc'; type PlaceDialogProps = { canvassAssId: string | null; - dialogStep: 'place' | 'edit' | 'household' | 'wizard'; + dialogStep: PlaceDialogStep; onClose: () => void; onEdit: () => void; + onPickHousehold: () => void; onSelectHousehold: () => void; onUpdateDone: () => void; onWizard: () => void; @@ -39,6 +41,7 @@ const PlaceDialog: FC = ({ dialogStep, onClose, onEdit, + onPickHousehold, onUpdateDone, onSelectHousehold, onWizard, @@ -118,6 +121,23 @@ const PlaceDialog: FC = ({ )} + {dialogStep == 'pickHousehold' && ( + + + { + onUpdateDone(); + }} + > + + + Log visit + + + + + + )} {dialogStep == 'edit' && ( <> @@ -191,6 +211,42 @@ const PlaceDialog: FC = ({ place={place} /> )} + {dialogStep == 'pickHousehold' && ( + + Choose household + {place.households.map((household) => { + const visitedRecently = isWithinLast24Hours( + household.visits.map((t) => t.timestamp) + ); + return ( + { + if (!visitedRecently) { + setSelectedHouseholdId(household.id); + setWizardStep(1); + onWizard(); + } + }} + width="100%" + > + + + {household.title || ( + + )} + + + {visitedRecently ? : ''} + + ); + })} + + )} {dialogStep === 'edit' && ( = ({ )} - {dialogStep === 'place' && ( + {dialogStep === 'place' && place.households.length == 0 && ( )} + {dialogStep == 'place' && place.households.length == 1 && ( + + )} + {dialogStep == 'place' && place.households.length > 1 && ( + + )} + {dialogStep == 'edit' && ( )} {dialogStep == 'place' && place.households.length == 1 && ( - + + + + )} {dialogStep == 'place' && place.households.length > 1 && ( - + + + + )} {dialogStep == 'edit' && ( @@ -349,6 +373,31 @@ const PlaceDialog: FC = ({ )} + setAnchorEl(null)} + open={!!anchorEl} + transformOrigin={{ + horizontal: 'center', + vertical: 'bottom', + }} + > + { + const newlyAddedHousehold = await addHousehold(); + setSelectedHouseholdId(newlyAddedHousehold.id); + onSelectHousehold(); + setEditingHouseholdTitle(true); + setAnchorEl(null); + }} + > + Add household + + From 55da3eea804d313d336784099ecd611eb0791577 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Thu, 10 Oct 2024 15:17:31 +0200 Subject: [PATCH 13/44] Remove unused messages and change title on button to match. --- src/features/areas/components/PlaceDialog/Household.tsx | 2 +- src/features/areas/l10n/messageIds.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/Household.tsx b/src/features/areas/components/PlaceDialog/Household.tsx index 874172b43..540512d17 100644 --- a/src/features/areas/components/PlaceDialog/Household.tsx +++ b/src/features/areas/components/PlaceDialog/Household.tsx @@ -67,7 +67,7 @@ const Household: FC = ({ onClick={onWizardStart} variant="contained" > - Record visit + Log visit {/* Date: Fri, 11 Oct 2024 10:28:39 +0200 Subject: [PATCH 14/44] Agument types with new fields for configurable metrics --- .../areas/components/PlaceDialog/index.tsx | 1 + src/features/areas/models.ts | 24 +++++++++++++++++-- src/features/areas/types.ts | 4 ++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/index.tsx b/src/features/areas/components/PlaceDialog/index.tsx index ea0f26305..601b27dcc 100644 --- a/src/features/areas/components/PlaceDialog/index.tsx +++ b/src/features/areas/components/PlaceDialog/index.tsx @@ -294,6 +294,7 @@ const PlaceDialog: FC = ({ addVisit(selectedHousehold.id, { ...report, canvassAssId, + responses: [], timestamp: new Date().toISOString(), }) } diff --git a/src/features/areas/models.ts b/src/features/areas/models.ts index 77f8cce90..8cc815e65 100644 --- a/src/features/areas/models.ts +++ b/src/features/areas/models.ts @@ -27,6 +27,13 @@ type ZetkinPlaceModelType = { type ZetkinCanvassAssignmentModelType = { campId: number; id: number; + metrics: { + definesDone: boolean; + description: string; + id: string; + kind: 'boolean' | 'scale5'; + question: string; + }[]; orgId: number; sessions: { areaId: string; @@ -56,10 +63,14 @@ const placeSchema = new mongoose.Schema({ { _id: false, canvassAssId: String, - doorWasOpened: Boolean, id: String, - missionAccomplished: Boolean, noteToOfficial: String, + responses: [ + { + metricId: String, + response: String, + }, + ], timestamp: String, }, ], @@ -74,6 +85,15 @@ const placeSchema = new mongoose.Schema({ const canvassAssignmentSchema = new mongoose.Schema({ campId: Number, + metrics: [ + { + definesDone: Boolean, + description: String, + id: String, + kind: String, + question: String, + }, + ], orgId: { required: true, type: Number }, sessions: [ { diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index c08538f78..5f0b06768 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -19,6 +19,10 @@ export type Visit = { id: string; missionAccomplished: boolean; noteToOfficial: string | null; + responses: { + metricId: string; + response: string; + }[]; timestamp: string; }; From f5fdcaa6fe05167ea814701f37bae37d0f118385 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Fri, 11 Oct 2024 11:06:34 +0200 Subject: [PATCH 15/44] Update ZetkinCanvassAssignment type --- .../[orgId]/canvassassignments/[canvassAssId]/route.ts | 1 + src/features/areas/types.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index 073731dca..8e49a6232 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -34,6 +34,7 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { const canvassAssignment: ZetkinCanvassAssignment = { campaign: { id: canvassAssignmentModel.campId }, id: canvassAssignmentModel._id.toString(), + metrics: [], organization: { id: orgId }, title: canvassAssignmentModel.title, }; diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index 5f0b06768..189f0a6c1 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -58,6 +58,13 @@ export type ZetkinCanvassAssignment = { id: number; }; id: string; + metrics: { + definesDone: boolean; + description: string; + id: string; + kind: 'boolean' | 'scale5'; + question: string; + }[]; organization: { id: number; }; From 8b147523ce2fc32aa882fabd0451f58175f2fd9e Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Fri, 11 Oct 2024 11:52:03 +0200 Subject: [PATCH 16/44] Add metrics and responses to canvassing APIs --- .../canvassassignments/[canvassAssId]/route.ts | 16 +++++++++++++++- .../orgs/[orgId]/canvassassignments/route.ts | 15 +++++++++++++++ .../households/[householdId]/visits/route.ts | 1 + src/features/areas/models.ts | 2 +- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index 8e49a6232..9f699f167 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -34,7 +34,13 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { const canvassAssignment: ZetkinCanvassAssignment = { campaign: { id: canvassAssignmentModel.campId }, id: canvassAssignmentModel._id.toString(), - metrics: [], + metrics: (canvassAssignmentModel.metrics || []).map((metric) => ({ + definesDone: metric.definesDone || false, + description: metric.description || '', + id: metric._id, + kind: metric.kind, + question: metric.question, + })), organization: { id: orgId }, title: canvassAssignmentModel.title, }; @@ -59,6 +65,7 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { const model = await CanvassAssignmentModel.findOneAndUpdate( { _id: params.canvassAssId }, { + metrics: payload.metrics, title: payload.title, }, { new: true } @@ -72,6 +79,13 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { data: { campaign: { id: model.campId }, id: model._id.toString(), + metrics: (model.metrics || []).map((metric) => ({ + definesDone: metric.definesDone || false, + description: metric.description || '', + id: metric._id, + kind: metric.kind, + question: metric.question, + })), organization: { id: orgId }, title: model.title, }, diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/route.ts index 63a87d709..e897841fe 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/route.ts @@ -28,6 +28,13 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { id: assignment.campId, }, id: assignment._id.toString(), + metrics: (assignment.metrics || []).map((metric) => ({ + definesDone: metric.definesDone || false, + description: metric.description || '', + id: metric._id, + kind: metric.kind, + question: metric.question, + })), organization: { id: orgId, }, @@ -52,6 +59,7 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { const model = new CanvassAssignmentModel({ campId: payload.campaign_id, + metrics: payload.metrics || [], orgId: orgId, title: payload.title, }); @@ -62,6 +70,13 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { data: { campaign: { id: model.campId }, id: model._id.toString(), + metrics: model.metrics.map((metric) => ({ + definesDone: metric.definesDone || false, + description: metric.description || '', + id: metric._id, + kind: metric.kind, + question: metric.question, + })), organization: { id: orgId }, title: model.title, }, diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts index a33e576ae..801150a2c 100644 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts +++ b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts @@ -34,6 +34,7 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { id: new mongoose.Types.ObjectId().toString(), missionAccomplished: payload.missionAccomplished, noteToOfficial: payload.noteToOfficial, + responses: payload.responses || [], timestamp: payload.timestamp, }, }, diff --git a/src/features/areas/models.ts b/src/features/areas/models.ts index 8cc815e65..dd558f575 100644 --- a/src/features/areas/models.ts +++ b/src/features/areas/models.ts @@ -28,9 +28,9 @@ type ZetkinCanvassAssignmentModelType = { campId: number; id: number; metrics: { + _id: string; definesDone: boolean; description: string; - id: string; kind: 'boolean' | 'scale5'; question: string; }[]; From f91bb0a41872f4a905c5c2c6c475e552ea452f3f Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Fri, 11 Oct 2024 13:30:37 +0200 Subject: [PATCH 17/44] First version of stepper rendering metrics from the outside. --- .../components/PlaceDialog/VisitWizard.tsx | 241 +++++------------- src/features/areas/types.ts | 2 - 2 files changed, 66 insertions(+), 177 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index f1ffddf69..fbf104980 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -8,196 +8,87 @@ import { } from '@mui/material'; import { FC, useState } from 'react'; -import { Visit } from '../../types'; - -export type WizardStep = 1 | 2 | 3; - -type Report = Pick< - Visit, - 'missionAccomplished' | 'doorWasOpened' | 'noteToOfficial' ->; - -type PreviousMessageProps = { - message: string; - onClick: () => void; -}; - -const PreviousMessage: FC = ({ message, onClick }) => { - return ( - - {message} - - - ); -}; - -type VisitWizardProps = { - onExit: () => void; - onRecordVisit: (report: Report) => void; - onStepChange: (newStep: WizardStep | null) => void; - step: WizardStep | null; -}; - -const VisitWizard: FC = ({ - onExit, - onRecordVisit, - onStepChange, - step, -}) => { - const [noteToOfficial, setNoteToOfficial] = useState(''); - const [missionAccomplished, setMissionAccomplished] = useState< - boolean | null - >(null); - const [doorWasOpened, setDoorWasOpened] = useState(null); - - const showPrevious = step && step != 1; +import { Visit, ZetkinCanvassAssignment } from 'features/areas/types'; +const BooleanQuestion: FC<{ + description: string; + onChange: (newValue: boolean) => void; + question: string; +}> = ({ description, onChange, question }) => { return ( - - {showPrevious && ( - - {(step == 2 || step == 3) && ( - { - if (step == 2) { - onStepChange(1); - setMissionAccomplished(null); - } else if ( - step == 3 && - (doorWasOpened == true || doorWasOpened == null) - ) { - onStepChange(1); - setMissionAccomplished(null); - setNoteToOfficial(''); - } else { - onStepChange(1); - setNoteToOfficial(''); - } - }} - /> - )} - {step == 3 && doorWasOpened && ( - { - onStepChange(2); - setNoteToOfficial(''); - }} - /> - )} - - )} + - {step == 1 && Did they open the door?} - {step == 2 && Did you have a conversation?} - {step == 3 && ( - - - Did something happen that you need to report to an official? - - setNoteToOfficial(ev.target.value)} - value={noteToOfficial} - /> - - )} + {question} + {description} - {step == 1 && ( - { - if (value != null) { - setDoorWasOpened(value); + { + if (typeof newValue == 'boolean') { + onChange(newValue); + } + }} + > + Yes + No + + + ); +}; - if (value) { - onStepChange(2); - } else { - onStepChange(3); - } - } +type VisitWizardProps = { + metrics: ZetkinCanvassAssignment['metrics']; + onLogVisit: (noteToOfficial: string, responses: Visit['responses']) => void; +}; - //If user has come back and clicks the same option again - if (typeof doorWasOpened == 'boolean' && value == null) { - if (doorWasOpened) { - onStepChange(2); - } else { - onStepChange(3); - } - } - }} - value={doorWasOpened} - > - Yes - No - - )} - {step == 2 && ( - { - if (value != null) { - setMissionAccomplished(value); - onStepChange(3); - } +const VisitWizard: FC = ({ metrics, onLogVisit }) => { + const [responses, setResponses] = useState([]); + const [step, setStep] = useState(0); + const [noteToOfficial, setNoteToOfficial] = useState(''); - //If user has come back and clicks the same option again - if (typeof missionAccomplished == 'boolean' && value == null) { - onStepChange(3); - } + const currentMetric = metrics[step] ? metrics[step] : null; + return ( + + {currentMetric && ( + { + setResponses([ + ...responses, + { metricId: currentMetric.id, response: newValue.toString() }, + ]); + setStep(step + 1); }} - value={missionAccomplished} - > - Yes - No - + question={currentMetric.question} + /> )} - {step == 3 && ( - + {!currentMetric && ( + + + Did something happen that you need an official to know? + + setNoteToOfficial(ev.target.value)} + value={noteToOfficial} + /> + + )} ); diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index 189f0a6c1..eb7a46271 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -15,9 +15,7 @@ export type PointData = [number, number]; export type Visit = { canvassAssId: string | null; - doorWasOpened: boolean; id: string; - missionAccomplished: boolean; noteToOfficial: string | null; responses: { metricId: string; From aca681164106df9c466ea1c89c7d9c6a61374028 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Fri, 11 Oct 2024 13:46:52 +0200 Subject: [PATCH 18/44] Show previous response. --- .../components/PlaceDialog/VisitWizard.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index fbf104980..f216af3a9 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -44,6 +44,27 @@ const BooleanQuestion: FC<{ ); }; +type PreviousMessageProps = { + question: string; + response: string; +}; + +const PreviousMessage: FC = ({ question, response }) => { + return ( + + {`${question}: ${response}`} + + ); +}; + type VisitWizardProps = { metrics: ZetkinCanvassAssignment['metrics']; onLogVisit: (noteToOfficial: string, responses: Visit['responses']) => void; @@ -57,6 +78,23 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { const currentMetric = metrics[step] ? metrics[step] : null; return ( + + {metrics.map((metric) => { + const response = responses.find( + (response) => response.metricId == metric.id + ); + + if (response) { + return ( + + ); + } + })} + {currentMetric && ( Date: Fri, 11 Oct 2024 14:20:53 +0200 Subject: [PATCH 19/44] Go back to the previous step you click on. --- .../components/PlaceDialog/VisitWizard.tsx | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index f216af3a9..ca85083f3 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -16,7 +16,7 @@ const BooleanQuestion: FC<{ question: string; }> = ({ description, onChange, question }) => { return ( - + <> Yes No - + ); }; type PreviousMessageProps = { + onClick: () => void; question: string; response: string; }; -const PreviousMessage: FC = ({ question, response }) => { +const PreviousMessage: FC = ({ + onClick, + question, + response, +}) => { return ( = ({ metrics, onLogVisit }) => { const currentMetric = metrics[step] ? metrics[step] : null; return ( - + - {metrics.map((metric) => { - const response = responses.find( - (response) => response.metricId == metric.id + {responses.map((response, index) => { + const metric = metrics.find( + (metric) => metric.id == response.metricId ); - if (response) { + if (metric) { return ( { + setStep(index); + setResponses(responses.slice(0, index)); + }} question={metric.question} response={response.response} /> @@ -96,33 +106,43 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { })} {currentMetric && ( - { - setResponses([ - ...responses, - { metricId: currentMetric.id, response: newValue.toString() }, - ]); - setStep(step + 1); - }} - question={currentMetric.question} - /> + + { + setResponses([ + ...responses, + { metricId: currentMetric.id, response: newValue.toString() }, + ]); + setStep(step + 1); + }} + question={currentMetric.question} + /> + )} {!currentMetric && ( - - - Did something happen that you need an official to know? - - setNoteToOfficial(ev.target.value)} - value={noteToOfficial} - /> + + + + Did something happen that you need an official to know? + + setNoteToOfficial(ev.target.value)} + value={noteToOfficial} + /> + From c8eff53fe6492b6a6c3a05f06da717a27ffb4410 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Fri, 11 Oct 2024 15:39:11 +0200 Subject: [PATCH 20/44] Refactor logic to go back and forward + add save button at the end. --- .../components/PlaceDialog/VisitWizard.tsx | 144 ++++++++++-------- 1 file changed, 78 insertions(+), 66 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index ca85083f3..c3c7540d9 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -1,7 +1,6 @@ import { Box, Button, - TextField, ToggleButton, ToggleButtonGroup, Typography, @@ -9,12 +8,14 @@ import { import { FC, useState } from 'react'; import { Visit, ZetkinCanvassAssignment } from 'features/areas/types'; +import { stringToBool } from 'utils/stringUtils'; const BooleanQuestion: FC<{ description: string; onChange: (newValue: boolean) => void; question: string; -}> = ({ description, onChange, question }) => { + value?: string; +}> = ({ description, onChange, question, value }) => { return ( <> { - if (typeof newValue == 'boolean') { - onChange(newValue); - } + onChange(newValue); }} + value={value ? stringToBool(value) : null} > Yes No @@ -73,81 +73,93 @@ const PreviousMessage: FC = ({ type VisitWizardProps = { metrics: ZetkinCanvassAssignment['metrics']; - onLogVisit: (noteToOfficial: string, responses: Visit['responses']) => void; + onLogVisit: (responses: Visit['responses']) => void; }; const VisitWizard: FC = ({ metrics, onLogVisit }) => { const [responses, setResponses] = useState([]); const [step, setStep] = useState(0); - const [noteToOfficial, setNoteToOfficial] = useState(''); - const currentMetric = metrics[step] ? metrics[step] : null; return ( - - {responses.map((response, index) => { - const metric = metrics.find( - (metric) => metric.id == response.metricId - ); - - if (metric) { - return ( + {metrics.map((metric, index) => { + if (index < step) { + return ( + <> { setStep(index); - setResponses(responses.slice(0, index)); + setResponses(responses.slice(0, index + 1)); }} question={metric.question} - response={response.response} + response={responses[index].response} /> - ); - } - })} - - {currentMetric && ( - - { - setResponses([ - ...responses, - { metricId: currentMetric.id, response: newValue.toString() }, - ]); - setStep(step + 1); - }} - question={currentMetric.question} - /> - - )} - {!currentMetric && ( - - - - Did something happen that you need an official to know? - - setNoteToOfficial(ev.target.value)} - value={noteToOfficial} - /> - - - - )} + {index == metrics.length - 1 && ( + + + + )} + + ); + } + + if (index == step) { + return ( + + { + if (newValue == null) { + //User is returning and selects the same response + setStep(step + 1); + } else { + if (responses[index]) { + //User is returning and selects new response + setResponses([ + ...responses.slice( + 0, + responses.indexOf(responses[index]) + ), + { ...responses[index], response: newValue.toString() }, + ]); + } else { + //User is responding to this question for the first time + setResponses([ + ...responses, + { + metricId: metric.id, + response: newValue.toString(), + }, + ]); + } + setStep(step + 1); + } + }} + question={metric.question} + value={responses[index]?.response} + /> + + ); + } + + if (index > step) { + return null; + } + })} ); }; From 709574add482a64e89f01734b8f83e8a46f9ae96 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Fri, 11 Oct 2024 15:47:00 +0200 Subject: [PATCH 21/44] Add metrics to stats API --- .../[canvassAssId]/stats/route.ts | 43 +++++++++++++++++++ src/features/areas/store.ts | 12 +----- src/features/areas/types.ts | 4 ++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts index 62104fad8..741b36014 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts @@ -10,6 +10,7 @@ import { Household, Visit, ZetkinArea, + ZetkinCanvassAssignmentStats, ZetkinCanvassSession, ZetkinPlace, } from 'features/areas/types'; @@ -120,6 +121,47 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { const visitedAreas: string[] = []; const householdsInAreas: Household[] = []; + const configuredMetrics = model.metrics; + const accumulatedMetrics: ZetkinCanvassAssignmentStats['metrics'] = + configuredMetrics.map((metric) => ({ + metric: { + definesDone: metric.definesDone, + description: metric.description, + id: metric._id, + kind: metric.kind, + question: metric.question, + }, + values: metric.kind == 'boolean' ? [0] : [0, 0, 0, 0, 0], + })); + + allPlaces.forEach((place) => { + place.households.forEach((household) => { + household.visits.forEach((visit) => { + if (visit.canvassAssId == params.canvassAssId) { + visit.responses.forEach((response) => { + const configuredMetric = configuredMetrics.find( + (candidate) => candidate._id == response.metricId + ); + + const accumulatedMetric = accumulatedMetrics.find( + (accum) => accum.metric.id == response.metricId + ); + + if (accumulatedMetric && configuredMetric) { + if (response.response == 'true') { + accumulatedMetric.values[0]++; + } else if (configuredMetric.kind == 'scale5') { + const rating = parseInt(response.response); + const index = rating - 1; + accumulatedMetric.values[index]++; + } + } + }); + } + }); + }); + }); + uniquePlacesInAreas.forEach((place) => { householdsInAreas.push(...place.households); place.households.forEach((household) => { @@ -156,6 +198,7 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { return Response.json({ data: { + metrics: accumulatedMetrics, num_areas: uniqueAreas.length, num_households: householdsInAreas.length, num_places: uniquePlacesInAreas.length, diff --git a/src/features/areas/store.ts b/src/features/areas/store.ts index d7c637e87..0c58065ce 100644 --- a/src/features/areas/store.ts +++ b/src/features/areas/store.ts @@ -331,17 +331,7 @@ const areasSlice = createSlice({ const statsItem = state.statsByCanvassAssId[canvassAssId]; state.statsByCanvassAssId[canvassAssId] = remoteItem(canvassAssId, { - data: statsItem?.data || { - id: canvassAssId, - num_areas: 0, - num_households: 0, - num_places: 0, - num_visited_areas: 0, - num_visited_households: 0, - num_visited_households_outside_areas: 0, - num_visited_places: 0, - num_visited_places_outside_areas: 0, - }, + data: statsItem?.data || null, isLoading: true, }); }, diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index 189f0a6c1..8aef50734 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -1,6 +1,10 @@ import { ZetkinPerson, ZetkinTag } from 'utils/types/zetkin'; export type ZetkinCanvassAssignmentStats = { + metrics: { + metric: ZetkinCanvassAssignment['metrics'][0]; + values: number[]; + }[]; num_areas: number; num_households: number; num_places: number; From 9ea71d66e1248235b1fe6394444ec3247e5fab8f Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Fri, 11 Oct 2024 15:47:51 +0200 Subject: [PATCH 22/44] Add bar chart with metrics on canvass assignment overview page --- .../components/AssignmentMetricsChart.tsx | 70 +++++++++++++++++++ .../[canvassAssId]/index.tsx | 4 ++ 2 files changed, 74 insertions(+) create mode 100644 src/features/areas/components/AssignmentMetricsChart.tsx diff --git a/src/features/areas/components/AssignmentMetricsChart.tsx b/src/features/areas/components/AssignmentMetricsChart.tsx new file mode 100644 index 000000000..f3b783c73 --- /dev/null +++ b/src/features/areas/components/AssignmentMetricsChart.tsx @@ -0,0 +1,70 @@ +import { FC } from 'react'; +import { Box, lighten, useTheme } from '@mui/material'; +import { ResponsiveBar } from '@nivo/bar'; + +import { ZetkinCanvassAssignmentStats } from '../types'; + +type Props = { + stats: ZetkinCanvassAssignmentStats; +}; + +const AssignmentMetricsChart: FC = ({ stats }) => { + const theme = useTheme(); + + const metricBars = stats.metrics.map((response) => { + if (response.metric.kind == 'boolean') { + return { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + question: response.metric.question, + yes: response.values[0], + }; + } else { + return { + 1: response.values[0], + 2: response.values[1], + 3: response.values[2], + 4: response.values[3], + 5: response.values[4], + question: response.metric.question, + yes: 0, + }; + } + }); + + return ( + + + + ); +}; + +export default AssignmentMetricsChart; diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx index f0087637c..114c434e9 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx @@ -16,6 +16,7 @@ import useCanvassAssignmentStats from 'features/areas/hooks/useCanvassAssignment import ZUIStackedStatusBar from 'zui/ZUIStackedStatusBar'; import { getContrastColor } from 'utils/colorUtils'; import { AREAS } from 'utils/featureFlags'; +import AssignmentMetricsChart from 'features/areas/components/AssignmentMetricsChart'; const scaffoldOptions = { authLevelRequired: 2, @@ -135,6 +136,9 @@ const CanvassAssignmentPage: PageWithLayout = ({ )} + + {stats.metrics && } + Progress From 6a41fe31cdb3526da54ab5939bec8751a2d43f80 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Sun, 13 Oct 2024 07:18:14 +0200 Subject: [PATCH 23/44] Display questions for scale5 metrics. --- .../components/PlaceDialog/VisitWizard.tsx | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index c3c7540d9..556b6c3f5 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -8,14 +8,14 @@ import { import { FC, useState } from 'react'; import { Visit, ZetkinCanvassAssignment } from 'features/areas/types'; -import { stringToBool } from 'utils/stringUtils'; -const BooleanQuestion: FC<{ +const Question: FC<{ description: string; onChange: (newValue: boolean) => void; + options: { label: string | number; value: string }[]; question: string; value?: string; -}> = ({ description, onChange, question, value }) => { +}> = ({ description, onChange, options, question, value }) => { return ( <> { onChange(newValue); }} - value={value ? stringToBool(value) : null} + value={value} > - Yes - No + {options.map((option) => ( + + {option.label} + + ))} ); @@ -120,7 +123,8 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { if (index == step) { return ( - { if (newValue == null) { @@ -134,7 +138,10 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { 0, responses.indexOf(responses[index]) ), - { ...responses[index], response: newValue.toString() }, + { + ...responses[index], + response: newValue.toString(), + }, ]); } else { //User is responding to this question for the first time @@ -149,6 +156,20 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { setStep(step + 1); } }} + options={ + metric.kind == 'boolean' + ? [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ] + : [ + { label: 1, value: '1' }, + { label: 2, value: '2' }, + { label: 3, value: '3' }, + { label: 4, value: '4' }, + { label: 5, value: '5' }, + ] + } question={metric.question} value={responses[index]?.response} /> From e7a11441056c316152dcac1d5442a96c5195eaf5 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Sun, 13 Oct 2024 07:32:16 +0200 Subject: [PATCH 24/44] Add textfield for note to official. --- .../components/PlaceDialog/VisitWizard.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index 556b6c3f5..6fe3966e6 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -1,6 +1,7 @@ import { Box, Button, + TextField, ToggleButton, ToggleButtonGroup, Typography, @@ -76,12 +77,16 @@ const PreviousMessage: FC = ({ type VisitWizardProps = { metrics: ZetkinCanvassAssignment['metrics']; - onLogVisit: (responses: Visit['responses']) => void; + onLogVisit: ( + responses: Visit['responses'], + noteToOfficial: Visit['noteToOfficial'] + ) => void; }; const VisitWizard: FC = ({ metrics, onLogVisit }) => { const [responses, setResponses] = useState([]); const [step, setStep] = useState(0); + const [noteToOfficial, setNoteToOfficial] = useState(''); return ( @@ -105,14 +110,28 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { flexGrow={1} justifyContent="flex-end" > + + + Did anything happen that an official needs to know about? + + setNoteToOfficial(ev.target.value)} + value={noteToOfficial} + /> + )} From 5b9ed201b33cb8d7cba9bbaa45f7f6a96070a6fe Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Sun, 13 Oct 2024 13:10:41 +0200 Subject: [PATCH 25/44] Get metric data from the canvass assignment. --- .../areas/components/PlaceDialog/index.tsx | 580 +++++++++--------- .../areas/components/PublicAreaMap.tsx | 2 +- 2 files changed, 294 insertions(+), 288 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/index.tsx b/src/features/areas/components/PlaceDialog/index.tsx index 601b27dcc..0045f975a 100644 --- a/src/features/areas/components/PlaceDialog/index.tsx +++ b/src/features/areas/components/PlaceDialog/index.tsx @@ -22,17 +22,19 @@ import messageIds from '../../l10n/messageIds'; import usePlaceMutations from '../../hooks/usePlaceMutations'; import { HouseholdPatchBody, ZetkinPlace } from '../../types'; import { Msg, useMessages } from 'core/i18n'; -import VisitWizard, { WizardStep } from './VisitWizard'; +import VisitWizard from './VisitWizard'; import EditPlace from './EditPlace'; import Place from './Place'; import Household from './Household'; import { isWithinLast24Hours } from 'features/areas/utils/isWithinLast24Hours'; import { PlaceDialogStep } from '../PublicAreaMap'; +import useCanvassAssignment from 'features/areas/hooks/useCanvassAssignment'; +import ZUIFuture from 'zui/ZUIFuture'; export type PlaceType = 'address' | 'misc'; type PlaceDialogProps = { - canvassAssId: string | null; + canvassAssId: string; dialogStep: PlaceDialogStep; onClose: () => void; onEdit: () => void; @@ -58,10 +60,10 @@ const PlaceDialog: FC = ({ orgId, place, }) => { + const messages = useMessages(messageIds); const { addVisit, addHousehold, updateHousehold, updatePlace } = usePlaceMutations(orgId, place.id); - - const messages = useMessages(messageIds); + const assignmentFuture = useCanvassAssignment(orgId, canvassAssId); const [selectedHouseholdId, setSelectedHouseholdId] = useState( null @@ -75,8 +77,6 @@ const PlaceDialog: FC = ({ const [householdTitle, setHousholdTitle] = useState(''); const [anchorEl, setAnchorEl] = useState(null); - const [wizardStep, setWizardStep] = useState(null); - const selectedHousehold = place.households.find( (household) => household.id == selectedHouseholdId ); @@ -102,305 +102,311 @@ const PlaceDialog: FC = ({ const saveButtonDisabled = nothingHasBeenEdited; - const showWizard = - selectedHousehold && dialogStep == 'wizard' && !!wizardStep; + const showWizard = selectedHousehold && dialogStep == 'wizard'; return ( - - - {dialogStep == 'place' && ( - <> - - {place?.title || } - - - - - - - - - - - )} - {dialogStep == 'pickHousehold' && ( - - - { - onUpdateDone(); - }} - > - - - Log visit - - - - + + {(assignment) => ( + + + {dialogStep == 'place' && ( + <> + + {place?.title || } + + + + + + + + + + + )} + {dialogStep == 'pickHousehold' && ( + + + { + onUpdateDone(); + }} + > + + + Log visit + + + + + + )} + {dialogStep == 'edit' && ( + <> + + + + + + + + )} + {dialogStep == 'household' && selectedHousehold && ( + + + { + onUpdateDone(); + }} + > + + + + {selectedHousehold.title || ( + + )} + + + + { + setHousholdTitle(selectedHousehold.title); + setEditingHouseholdTitle(true); + }} + sx={{ marginRight: 1 }} + > + + + + + )} + {dialogStep == 'wizard' && selectedHousehold && ( + + { + onSelectHousehold(); + }} + > + + + + {selectedHousehold.title || ( + + )} + + + )} - )} - {dialogStep == 'edit' && ( - <> - - + + {place && dialogStep == 'place' && ( + { + setSelectedHouseholdId(householdId); + onSelectHousehold(); }} + place={place} /> - - - - - - )} - {dialogStep == 'household' && selectedHousehold && ( - - - { - onUpdateDone(); + )} + {dialogStep == 'pickHousehold' && ( + + Choose household + {place.households.map((household) => { + const visitedRecently = isWithinLast24Hours( + household.visits.map((t) => t.timestamp) + ); + return ( + { + if (!visitedRecently) { + setSelectedHouseholdId(household.id); + onWizard(); + } + }} + width="100%" + > + + + {household.title || ( + + )} + + + {visitedRecently ? : ''} + + ); + })} + + )} + {dialogStep === 'edit' && ( + + setDescription(newDescription) + } + onTitleChange={(newTitle) => setTitle(newTitle)} + onTypeChange={(newType) => setType(newType)} + title={title} + type={type} + /> + )} + {selectedHousehold && dialogStep == 'household' && ( + + setEditingHouseholdTitle(false) + } + onHouseholdTitleChange={(newTitle) => + setHousholdTitle(newTitle) + } + onHouseholdUpdate={(data: HouseholdPatchBody) => + updateHousehold(selectedHousehold.id, data) + } + onWizardStart={() => { + onWizard(); }} - > - - - - {selectedHousehold.title || ( - + visitedRecently={isWithinLast24Hours( + selectedHousehold.visits.map((t) => t.timestamp) )} - - - - { - setHousholdTitle(selectedHousehold.title); + /> + )} + {showWizard && ( + { + addVisit(selectedHousehold.id, { + canvassAssId: assignment.id, + noteToOfficial, + responses, + timestamp: new Date().toISOString(), + }); + onUpdateDone(); + }} + /> + )} + + + {dialogStep === 'place' && place.households.length == 0 && ( + + )} + {dialogStep == 'place' && place.households.length == 1 && ( + + + + )} - /> - )} - {showWizard && ( - - addVisit(selectedHousehold.id, { - ...report, - canvassAssId, - responses: [], - timestamp: new Date().toISOString(), - }) - } - onStepChange={setWizardStep} - step={wizardStep} - /> - )} - - - {dialogStep === 'place' && place.households.length == 0 && ( - - )} - {dialogStep == 'place' && place.households.length == 1 && ( - - + + + )} + + {dialogStep == 'edit' && ( + + )} + - - - - - )} - {dialogStep == 'place' && place.households.length > 1 && ( - - - - - )} - - {dialogStep == 'edit' && ( - - )} - setAnchorEl(null)} - open={!!anchorEl} - transformOrigin={{ - horizontal: 'center', - vertical: 'bottom', - }} - > - { - const newlyAddedHousehold = await addHousehold(); - setSelectedHouseholdId(newlyAddedHousehold.id); - onSelectHousehold(); - setEditingHouseholdTitle(true); - setAnchorEl(null); - }} - > - Add household - - - - + { + const newlyAddedHousehold = await addHousehold(); + setSelectedHouseholdId(newlyAddedHousehold.id); + onSelectHousehold(); + setEditingHouseholdTitle(true); + setAnchorEl(null); + }} + > + Add household + + + + + )} + ); }; diff --git a/src/features/areas/components/PublicAreaMap.tsx b/src/features/areas/components/PublicAreaMap.tsx index 39c693cd5..817515052 100644 --- a/src/features/areas/components/PublicAreaMap.tsx +++ b/src/features/areas/components/PublicAreaMap.tsx @@ -318,7 +318,7 @@ const PublicAreaMap: FC = ({ canvassAssId, area }) => { })} - {selectedPlace && ( + {selectedPlace && canvassAssId && ( Date: Mon, 14 Oct 2024 10:24:58 +0200 Subject: [PATCH 26/44] add new editor page, add logic to create and save a boolean question, add push to save metric in array --- .../[canvassAssId]/route.ts | 4 +- .../areas/layouts/CanvassAssignmentLayout.tsx | 1 + .../[canvassAssId]/editor.tsx | 156 ++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index 9f699f167..79f2ec13a 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -65,7 +65,9 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { const model = await CanvassAssignmentModel.findOneAndUpdate( { _id: params.canvassAssId }, { - metrics: payload.metrics, + $push: { + metrics: payload.metrics, + }, title: payload.title, }, { new: true } diff --git a/src/features/areas/layouts/CanvassAssignmentLayout.tsx b/src/features/areas/layouts/CanvassAssignmentLayout.tsx index 75b1a8e19..eb0fd7459 100644 --- a/src/features/areas/layouts/CanvassAssignmentLayout.tsx +++ b/src/features/areas/layouts/CanvassAssignmentLayout.tsx @@ -44,6 +44,7 @@ const CanvassAssignmentLayout: FC = ({ { href: '/', label: messages.canvassAssignment.tabs.overview() }, { href: '/plan', label: 'Plan' }, { href: '/canvassers', label: 'Canvassers' }, + { href: '/editor', label: 'Editor' }, ]} title={ { + const { orgId, campId, canvassAssId } = ctx.params!; + return { + props: { campId, canvassAssId, orgId }, + }; +}, scaffoldOptions); + +interface CanvassAssignmentEditorProps { + orgId: string; + canvassAssId: string; +} + +export type ZetkinAssignmentMetrics = Omit< + ZetkinCanvassAssignment, + Exclude +>; + +const CanvassAssignmentEditorPage: PageWithLayout< + CanvassAssignmentEditorProps +> = ({ orgId, canvassAssId }) => { + const updateCanvassAssignment = useCanvassAssignmentMutations( + parseInt(orgId), + canvassAssId + ); + const canvassFuture = useCanvassAssignment(parseInt(orgId), canvassAssId); + + const [booleanTitle, setBooleanTitle] = useState(''); + const [booleanDescription, setBooleanDescription] = useState(''); + const [booleanQuestion, setBooleanQuestion] = useState(false); + + const handleAddQuestion = () => { + setBooleanQuestion(true); + }; + + return ( + <> + + Here you can configure the questions for your canvass assignment + + + + + + {/** BOOLEAN QUESTION */} + {booleanQuestion && ( + + + + Yes/No Question + + + setBooleanTitle(ev.target.value)} + sx={{ marginBottom: 1 }} + variant="outlined" + /> + setBooleanDescription(ev.target.value)} + sx={{ marginBottom: 1 }} + variant="outlined" + /> + + + Answering this question defines the goal of the assignment? + + + + + + + + + )} + + {(data) => ( + + Your list of questions: + {data.metrics.map((metric) => ( + + + + {metric.question || 'Untitled question'} + + + {metric.description || 'No description'} + + + + + + ))} + + )} + + + ); +}; + +CanvassAssignmentEditorPage.getLayout = function getLayout(page) { + return ( + {page} + ); +}; + +export default CanvassAssignmentEditorPage; From 59f1b8ed216b14977c43a24a1dd9f41c2e39ac51 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:45:09 +0200 Subject: [PATCH 27/44] add logic to delete metrics in route, extract in a new ZetkinMetric type the metrics of a canvassAssignment --- .../[canvassAssId]/route.ts | 67 ++++++++++++++++--- src/features/areas/types.ts | 16 +++-- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index 79f2ec13a..c6defea73 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; import { CanvassAssignmentModel } from 'features/areas/models'; -import { ZetkinCanvassAssignment } from 'features/areas/types'; +import { ZetkinCanvassAssignment, ZetkinMetric } from 'features/areas/types'; type RouteMeta = { params: { @@ -61,17 +61,66 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { await mongoose.connect(process.env.MONGODB_URL || ''); const payload = await request.json(); + const { metrics: newMetrics, title } = payload; - const model = await CanvassAssignmentModel.findOneAndUpdate( + // Find existing metrics to remove + const existingMetrics = await CanvassAssignmentModel.findById( + params.canvassAssId + ).select('metrics'); + + if (!existingMetrics) { + return new NextResponse(null, { status: 404 }); + } + + const existingMetricsIds = existingMetrics.metrics.map((metric) => + metric._id.toString() + ); + + // Identify metrics to be deleted + const metricsToDelete = existingMetricsIds.filter( + (id) => !newMetrics.some((metric: ZetkinMetric) => metric.id === id) + ); + + // Remove metrics that are no longer included + if (metricsToDelete.length > 0) { + await CanvassAssignmentModel.updateOne( + { _id: params.canvassAssId }, + { $pull: { metrics: { _id: { $in: metricsToDelete } } } } + ); + } + + for (const metric of newMetrics) { + if (metric.id) { + // If the metric has an ID, update it + await CanvassAssignmentModel.updateOne( + { _id: params.canvassAssId, 'metrics._id': metric.id }, + { + $set: { + 'metrics.$.definesDone': metric.definesDone, + 'metrics.$.description': metric.description, + 'metrics.$.kind': metric.kind, + 'metrics.$.question': metric.question, + }, + } + ); + } else { + // If no ID exists, push it as a new metric + await CanvassAssignmentModel.updateOne( + { _id: params.canvassAssId }, + { + $push: { metrics: metric }, + } + ); + } + } + + await CanvassAssignmentModel.updateOne( { _id: params.canvassAssId }, - { - $push: { - metrics: payload.metrics, - }, - title: payload.title, - }, - { new: true } + { title } ); + const model = await CanvassAssignmentModel.findById( + params.canvassAssId + ).populate('metrics'); if (!model) { return new NextResponse(null, { status: 404 }); diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index 9989039bd..3e9659820 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -55,18 +55,20 @@ export type ZetkinPlace = { type: 'address' | 'misc'; }; +export type ZetkinMetric = { + definesDone: boolean; + description: string; + id: string; + kind: 'boolean' | 'scale5'; + question: string; +}; + export type ZetkinCanvassAssignment = { campaign: { id: number; }; id: string; - metrics: { - definesDone: boolean; - description: string; - id: string; - kind: 'boolean' | 'scale5'; - question: string; - }[]; + metrics: ZetkinMetric[]; organization: { id: number; }; From 0cb78a0be379ffa421103424c3c24e9ca96949a1 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:39:04 +0200 Subject: [PATCH 28/44] create new component MetricCard to handle Card logic, refactor editor page, add logic to edit and delete card --- .../areas/components/Metrics/MetricCard.tsx | 91 ++++++++++ .../[canvassAssId]/editor.tsx | 161 ++++++++---------- 2 files changed, 162 insertions(+), 90 deletions(-) create mode 100644 src/features/areas/components/Metrics/MetricCard.tsx diff --git a/src/features/areas/components/Metrics/MetricCard.tsx b/src/features/areas/components/Metrics/MetricCard.tsx new file mode 100644 index 000000000..8e631409f --- /dev/null +++ b/src/features/areas/components/Metrics/MetricCard.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { + Card, + CardContent, + Typography, + Box, + TextField, + Checkbox, + Button, +} from '@mui/material'; + +import { ZetkinMetric } from 'features/areas/types'; + +const MetricCard = ({ + metric, + onDelete, + + onSave, +}: { + metric: ZetkinMetric; + onDelete: () => void; + onSave: (metric: ZetkinMetric) => void; +}) => { + const [question, setQuestion] = useState(metric.question || ''); + const [description, setDescription] = useState( + metric.description || '' + ); + const [definesDone, setDefinesDone] = useState( + metric.definesDone || false + ); + + const isEditing = !!metric?.id; + + return ( + + + + {metric.kind === 'boolean' ? 'Yes/No Question' : 'Metric'}{' '} + + {metric.kind === 'boolean' && ( + + setQuestion(ev.target.value)} + sx={{ marginBottom: 1 }} + value={question} + variant="outlined" + /> + setDescription(ev.target.value)} + sx={{ marginBottom: 1 }} + value={description} + variant="outlined" + /> + + + Answering this question defines the goal of the assignment? + + setDefinesDone(ev.target.checked)} + /> + + + {isEditing && ( + + )} + + )} + {metric.kind === 'scale5' && SCALE5} + + + ); +}; + +export default MetricCard; diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index 33a555c74..9835a3b0e 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -1,14 +1,6 @@ import { GetServerSideProps } from 'next'; import { useState } from 'react'; -import { - Box, - Button, - Card, - CardContent, - Checkbox, - TextField, - Typography, -} from '@mui/material'; +import { Box, Button, Card, CardContent, Typography } from '@mui/material'; import { AREAS } from 'utils/featureFlags'; import CanvassAssignmentLayout from 'features/areas/layouts/CanvassAssignmentLayout'; @@ -16,8 +8,9 @@ import { PageWithLayout } from 'utils/types'; import { scaffold } from 'utils/next'; import useCanvassAssignmentMutations from 'features/areas/hooks/useCanvassAssignmentMutations'; import useCanvassAssignment from 'features/areas/hooks/useCanvassAssignment'; -import { ZetkinCanvassAssignment } from 'utils/types/zetkin'; import ZUIFuture from 'zui/ZUIFuture'; +import { ZetkinMetric } from 'features/areas/types'; +import MetricCard from 'features/areas/components/Metrics/MetricCard'; const scaffoldOptions = { authLevelRequired: 2, @@ -36,11 +29,6 @@ interface CanvassAssignmentEditorProps { canvassAssId: string; } -export type ZetkinAssignmentMetrics = Omit< - ZetkinCanvassAssignment, - Exclude ->; - const CanvassAssignmentEditorPage: PageWithLayout< CanvassAssignmentEditorProps > = ({ orgId, canvassAssId }) => { @@ -50,80 +38,69 @@ const CanvassAssignmentEditorPage: PageWithLayout< ); const canvassFuture = useCanvassAssignment(parseInt(orgId), canvassAssId); - const [booleanTitle, setBooleanTitle] = useState(''); - const [booleanDescription, setBooleanDescription] = useState(''); - const [booleanQuestion, setBooleanQuestion] = useState(false); + const [editingMetric, setEditingMetric] = useState(null); + + const handleSaveMetric = async (metric: ZetkinMetric) => { + if (canvassFuture.data) { + await updateCanvassAssignment({ + metrics: canvassFuture.data.metrics + .map((m) => (m.id === metric.id ? metric : m)) + .concat(metric.id ? [] : [metric]), + }); + } + setEditingMetric(null); + }; + + const handleDeleteMetric = async (id: string) => { + if (canvassFuture.data) { + await updateCanvassAssignment({ + metrics: canvassFuture.data.metrics.filter((m) => m.id !== id), + }); + } + setEditingMetric(null); + }; - const handleAddQuestion = () => { - setBooleanQuestion(true); + const handleAddNewMetric = (kind: 'boolean' | 'scale5') => { + setEditingMetric({ + definesDone: false, + description: '', + id: '', + kind: kind, + question: '', + }); }; return ( - <> - - Here you can configure the questions for your canvass assignment - - - - - - {/** BOOLEAN QUESTION */} - {booleanQuestion && ( - - - - Yes/No Question - - - setBooleanTitle(ev.target.value)} - sx={{ marginBottom: 1 }} - variant="outlined" - /> - setBooleanDescription(ev.target.value)} - sx={{ marginBottom: 1 }} - variant="outlined" - /> - - - Answering this question defines the goal of the assignment? - - - - - - - - - )} - - {(data) => ( + + {(data) => ( + <> + + Here you can configure the questions for your canvass assignment + + + + + + + {editingMetric && ( + handleDeleteMetric(editingMetric.id)} + onSave={handleSaveMetric} + /> + )} + Your list of questions: {data.metrics.map((metric) => ( @@ -136,14 +113,18 @@ const CanvassAssignmentEditorPage: PageWithLayout< {metric.description || 'No description'} - - + + + + ))} - )} - - + + )} + ); }; From 9b8bb01cdf2f04860df390b752385ac4c99880b5 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:39:15 +0200 Subject: [PATCH 29/44] add scale5 metric type logic, add Close button in metricCard --- .../areas/components/Metrics/MetricCard.tsx | 59 ++++++++++++++++--- .../[canvassAssId]/editor.tsx | 1 + 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/features/areas/components/Metrics/MetricCard.tsx b/src/features/areas/components/Metrics/MetricCard.tsx index 8e631409f..3737c04db 100644 --- a/src/features/areas/components/Metrics/MetricCard.tsx +++ b/src/features/areas/components/Metrics/MetricCard.tsx @@ -1,3 +1,4 @@ +import { Close } from '@mui/icons-material'; import React, { useState } from 'react'; import { Card, @@ -13,11 +14,12 @@ import { ZetkinMetric } from 'features/areas/types'; const MetricCard = ({ metric, + onClose, onDelete, - onSave, }: { metric: ZetkinMetric; + onClose: () => void; onDelete: () => void; onSave: (metric: ZetkinMetric) => void; }) => { @@ -34,22 +36,26 @@ const MetricCard = ({ return ( - - {metric.kind === 'boolean' ? 'Yes/No Question' : 'Metric'}{' '} - + + + {metric.kind === 'boolean' ? 'Yes/No Question' : 'Rating Question'} + + + + {metric.kind === 'boolean' && ( setQuestion(ev.target.value)} - sx={{ marginBottom: 1 }} + sx={{ marginBottom: 2, marginTop: 2 }} value={question} variant="outlined" /> setDescription(ev.target.value)} - sx={{ marginBottom: 1 }} + sx={{ marginBottom: 2 }} value={description} variant="outlined" /> @@ -82,7 +88,46 @@ const MetricCard = ({ )} )} - {metric.kind === 'scale5' && SCALE5} + {metric.kind === 'scale5' && ( + + setQuestion(ev.target.value)} + sx={{ marginBottom: 1, marginTop: 2 }} + value={question} + variant="outlined" + /> + setDescription(ev.target.value)} + sx={{ marginBottom: 4, marginTop: 1 }} + value={description} + variant="outlined" + /> + + Users will rate using a 1 to 5 scale system + + + + {isEditing && ( + + )} + + )} ); diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index 9835a3b0e..31615630c 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -96,6 +96,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< {editingMetric && ( setEditingMetric(null)} onDelete={() => handleDeleteMetric(editingMetric.id)} onSave={handleSaveMetric} /> From 52690b4282c88e915f50de4455cee8261ac00a92 Mon Sep 17 00:00:00 2001 From: Rebe R <36491300+rebecarubio@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:58:28 +0200 Subject: [PATCH 30/44] add useEffect to change values when editting and new metric is oppened, make list of questions conditional --- src/features/areas/components/Metrics/MetricCard.tsx | 8 +++++++- .../[campId]/canvassassignments/[canvassAssId]/editor.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/areas/components/Metrics/MetricCard.tsx b/src/features/areas/components/Metrics/MetricCard.tsx index 3737c04db..c033617e6 100644 --- a/src/features/areas/components/Metrics/MetricCard.tsx +++ b/src/features/areas/components/Metrics/MetricCard.tsx @@ -1,5 +1,5 @@ import { Close } from '@mui/icons-material'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Card, CardContent, @@ -33,6 +33,12 @@ const MetricCard = ({ const isEditing = !!metric?.id; + useEffect(() => { + setQuestion(metric.question || ''); + setDescription(metric.description || ''); + setDefinesDone(metric.definesDone || false); + }, [metric]); + return ( diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index 31615630c..929e395fc 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -103,7 +103,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< )} - Your list of questions: + {data.metrics.length > 0 ? 'Your list of questions:' : ''} {data.metrics.map((metric) => ( From 29ee7a3f4a157df8f1c5859446ab30c61a9c6767 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Mon, 14 Oct 2024 17:25:01 +0200 Subject: [PATCH 31/44] Fix bug causing canvass assignments to show up in all projects --- src/features/campaigns/hooks/useActivityOverview.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/campaigns/hooks/useActivityOverview.ts b/src/features/campaigns/hooks/useActivityOverview.ts index 1862b776a..fb84edbbf 100644 --- a/src/features/campaigns/hooks/useActivityOverview.ts +++ b/src/features/campaigns/hooks/useActivityOverview.ts @@ -34,8 +34,10 @@ export default function useActivitiyOverview( campId ); const emailActivitiesFuture = useEmailActivities(orgId, campId); - const canvassAssignmentAcitivitiesFuture = - useCanvassAssignmentActivities(orgId); + const canvassAssignmentAcitivitiesFuture = useCanvassAssignmentActivities( + orgId, + campId + ); if ( callAssignmentActivitiesFuture.isLoading || From 1f31be22d0e0f9b9c3e25cc52b59b5acbda293b5 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Mon, 14 Oct 2024 17:50:19 +0200 Subject: [PATCH 32/44] Fix bug preventing PATCH of canvass assignments without including metrics --- .../[canvassAssId]/route.ts | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index c6defea73..3809508c7 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -63,55 +63,57 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { const payload = await request.json(); const { metrics: newMetrics, title } = payload; - // Find existing metrics to remove - const existingMetrics = await CanvassAssignmentModel.findById( - params.canvassAssId - ).select('metrics'); - - if (!existingMetrics) { - return new NextResponse(null, { status: 404 }); - } - - const existingMetricsIds = existingMetrics.metrics.map((metric) => - metric._id.toString() - ); + if (newMetrics) { + // Find existing metrics to remove + const assignment = await CanvassAssignmentModel.findById( + params.canvassAssId + ).select('metrics'); + + if (!assignment) { + return new NextResponse(null, { status: 404 }); + } - // Identify metrics to be deleted - const metricsToDelete = existingMetricsIds.filter( - (id) => !newMetrics.some((metric: ZetkinMetric) => metric.id === id) - ); + const existingMetricsIds = assignment.metrics.map((metric) => + metric._id.toString() + ); - // Remove metrics that are no longer included - if (metricsToDelete.length > 0) { - await CanvassAssignmentModel.updateOne( - { _id: params.canvassAssId }, - { $pull: { metrics: { _id: { $in: metricsToDelete } } } } + // Identify metrics to be deleted + const metricsToDelete = existingMetricsIds.filter( + (id) => !newMetrics.some((metric: ZetkinMetric) => metric.id === id) ); - } - for (const metric of newMetrics) { - if (metric.id) { - // If the metric has an ID, update it - await CanvassAssignmentModel.updateOne( - { _id: params.canvassAssId, 'metrics._id': metric.id }, - { - $set: { - 'metrics.$.definesDone': metric.definesDone, - 'metrics.$.description': metric.description, - 'metrics.$.kind': metric.kind, - 'metrics.$.question': metric.question, - }, - } - ); - } else { - // If no ID exists, push it as a new metric + // Remove metrics that are no longer included + if (metricsToDelete.length > 0) { await CanvassAssignmentModel.updateOne( { _id: params.canvassAssId }, - { - $push: { metrics: metric }, - } + { $pull: { metrics: { _id: { $in: metricsToDelete } } } } ); } + + for (const metric of newMetrics) { + if (metric.id) { + // If the metric has an ID, update it + await CanvassAssignmentModel.updateOne( + { _id: params.canvassAssId, 'metrics._id': metric.id }, + { + $set: { + 'metrics.$.definesDone': metric.definesDone, + 'metrics.$.description': metric.description, + 'metrics.$.kind': metric.kind, + 'metrics.$.question': metric.question, + }, + } + ); + } else { + // If no ID exists, push it as a new metric + await CanvassAssignmentModel.updateOne( + { _id: params.canvassAssId }, + { + $push: { metrics: metric }, + } + ); + } + } } await CanvassAssignmentModel.updateOne( From 1a6013b04341137def42d9f07bfdb3ff86a7d459 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Mon, 14 Oct 2024 17:51:10 +0200 Subject: [PATCH 33/44] Consistently use "yes"/"no" as the value for boolean metrics --- .../[orgId]/canvassassignments/[canvassAssId]/stats/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts index 741b36014..fd50b7151 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts @@ -148,7 +148,7 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { ); if (accumulatedMetric && configuredMetric) { - if (response.response == 'true') { + if (response.response == 'yes') { accumulatedMetric.values[0]++; } else if (configuredMetric.kind == 'scale5') { const rating = parseInt(response.response); From b8c5f749936ec31e736e7ab69a33eff850ea97dd Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Mon, 14 Oct 2024 18:04:52 +0200 Subject: [PATCH 34/44] Add default metric when creating a canvass assignment. --- src/features/areas/hooks/useCreateCanvassAssignment.ts | 8 ++++---- src/features/areas/types.ts | 3 ++- .../campaigns/components/CampaignActionButtons.tsx | 8 ++++++++ src/features/campaigns/l10n/messageIds.ts | 3 +++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/features/areas/hooks/useCreateCanvassAssignment.ts b/src/features/areas/hooks/useCreateCanvassAssignment.ts index c3b8c419d..866ba9502 100644 --- a/src/features/areas/hooks/useCreateCanvassAssignment.ts +++ b/src/features/areas/hooks/useCreateCanvassAssignment.ts @@ -10,10 +10,10 @@ export default function useCreateCanvassAssignment(orgId: number) { const dispatch = useAppDispatch(); return async (data: ZetkinCanvassAssignmentPostBody) => { - const created = await apiClient.post( - `/beta/orgs/${orgId}/canvassassignments`, - data - ); + const created = await apiClient.post< + ZetkinCanvassAssignment, + ZetkinCanvassAssignmentPostBody + >(`/beta/orgs/${orgId}/canvassassignments`, data); dispatch(canvassAssignmentCreated(created)); }; } diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index 3e9659820..b5e918c99 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -101,9 +101,10 @@ export type ZetkinPlacePatchBody = Partial< { visits?: Partial>[] }[]; }; export type ZetkinCanvassAssignmentPostBody = Partial< - Omit + Omit > & { campaign_id: number; + metrics: Omit[]; }; export type ZetkinCanvassAssignmentPatchbody = Partial< Omit diff --git a/src/features/campaigns/components/CampaignActionButtons.tsx b/src/features/campaigns/components/CampaignActionButtons.tsx index ac24a0c10..586ea785f 100644 --- a/src/features/campaigns/components/CampaignActionButtons.tsx +++ b/src/features/campaigns/components/CampaignActionButtons.tsx @@ -126,6 +126,14 @@ const CampaignActionButtons: React.FunctionComponent< onClick: () => createCanvassAssignment({ campaign_id: campaign.id, + metrics: [ + { + definesDone: true, + description: '', + kind: 'boolean', + question: messages.form.createCanvassAssignment.defaultQuestion(), + }, + ], title: null, }), }); diff --git a/src/features/campaigns/l10n/messageIds.ts b/src/features/campaigns/l10n/messageIds.ts index 866eaab70..5fa616d46 100644 --- a/src/features/campaigns/l10n/messageIds.ts +++ b/src/features/campaigns/l10n/messageIds.ts @@ -76,6 +76,9 @@ export default makeMessages('feat.campaigns', { error: m('There was an error creating the project'), newCampaign: m('My project'), }, + createCanvassAssignment: { + defaultQuestion: m('Did you complete the mission?'), + }, createEmail: { newEmail: m('Untitled email'), }, From 3822377e1231d1f026fefb4ea2295bf9b64984b8 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Mon, 14 Oct 2024 19:18:39 +0200 Subject: [PATCH 35/44] Add button to skip scale questions --- .../components/PlaceDialog/VisitWizard.tsx | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index 6fe3966e6..cffcd2e62 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -8,15 +8,31 @@ import { } from '@mui/material'; import { FC, useState } from 'react'; -import { Visit, ZetkinCanvassAssignment } from 'features/areas/types'; +import { + Visit, + ZetkinCanvassAssignment, + ZetkinMetric, +} from 'features/areas/types'; const Question: FC<{ - description: string; - onChange: (newValue: boolean) => void; - options: { label: string | number; value: string }[]; - question: string; + metric: ZetkinMetric; + onChange: (newValue: string | null) => void; value?: string; -}> = ({ description, onChange, options, question, value }) => { +}> = ({ onChange, metric, value }) => { + const options = + metric.kind == 'boolean' + ? [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ] + : [ + { label: 1, value: '1' }, + { label: 2, value: '2' }, + { label: 3, value: '3' }, + { label: 4, value: '4' }, + { label: 5, value: '5' }, + ]; + return ( <> - {question} - {description} + {metric.question} + {metric.description} + {metric.kind == 'scale5' && ( + + )} = ({ padding: 1, }} > - {`${question}: ${response}`} + {!response && ( + + {`${question}:`} + + {'Skipped'} + + + )} + {response && ( + {`${question}: ${response}`} + )} ); }; @@ -144,7 +173,7 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { { if (newValue == null) { //User is returning and selects the same response @@ -175,21 +204,6 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { setStep(step + 1); } }} - options={ - metric.kind == 'boolean' - ? [ - { label: 'Yes', value: 'yes' }, - { label: 'No', value: 'no' }, - ] - : [ - { label: 1, value: '1' }, - { label: 2, value: '2' }, - { label: 3, value: '3' }, - { label: 4, value: '4' }, - { label: 5, value: '5' }, - ] - } - question={metric.question} value={responses[index]?.response} /> From 186c4f9e5770494b9df781b9aa2fed67ab292ce7 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Mon, 14 Oct 2024 19:29:56 +0200 Subject: [PATCH 36/44] Filter out skipped responses from what is sent to the server. --- src/features/areas/components/PlaceDialog/VisitWizard.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index cffcd2e62..1ef027658 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -156,7 +156,10 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { + )} + {isEditing && ( - )} - - )} - {metric.kind === 'scale5' && ( - - setQuestion(ev.target.value)} - sx={{ marginBottom: 1, marginTop: 2 }} - value={question} - variant="outlined" - /> - setDescription(ev.target.value)} - sx={{ marginBottom: 4, marginTop: 1 }} - value={description} - variant="outlined" - /> - - Users will rate using a 1 to 5 scale system - - - {isEditing && ( - - )} - )} + ); diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index 929e395fc..ee6af71c8 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -1,6 +1,13 @@ import { GetServerSideProps } from 'next'; import { useState } from 'react'; -import { Box, Button, Card, CardContent, Typography } from '@mui/material'; +import { + Box, + Button, + Card, + CardActions, + CardContent, + Typography, +} from '@mui/material'; import { AREAS } from 'utils/featureFlags'; import CanvassAssignmentLayout from 'features/areas/layouts/CanvassAssignmentLayout'; @@ -36,14 +43,17 @@ const CanvassAssignmentEditorPage: PageWithLayout< parseInt(orgId), canvassAssId ); - const canvassFuture = useCanvassAssignment(parseInt(orgId), canvassAssId); + const canvassAssignmentFuture = useCanvassAssignment( + parseInt(orgId), + canvassAssId + ); const [editingMetric, setEditingMetric] = useState(null); const handleSaveMetric = async (metric: ZetkinMetric) => { - if (canvassFuture.data) { + if (canvassAssignmentFuture.data) { await updateCanvassAssignment({ - metrics: canvassFuture.data.metrics + metrics: canvassAssignmentFuture.data.metrics .map((m) => (m.id === metric.id ? metric : m)) .concat(metric.id ? [] : [metric]), }); @@ -52,9 +62,11 @@ const CanvassAssignmentEditorPage: PageWithLayout< }; const handleDeleteMetric = async (id: string) => { - if (canvassFuture.data) { + if (canvassAssignmentFuture.data) { await updateCanvassAssignment({ - metrics: canvassFuture.data.metrics.filter((m) => m.id !== id), + metrics: canvassAssignmentFuture.data.metrics.filter( + (m) => m.id !== id + ), }); } setEditingMetric(null); @@ -71,61 +83,85 @@ const CanvassAssignmentEditorPage: PageWithLayout< }; return ( - - {(data) => ( - <> - - Here you can configure the questions for your canvass assignment - - - - - - - {editingMetric && ( - setEditingMetric(null)} - onDelete={() => handleDeleteMetric(editingMetric.id)} - onSave={handleSaveMetric} - /> - )} - - - {data.metrics.length > 0 ? 'Your list of questions:' : ''} - {data.metrics.map((metric) => ( - - - - {metric.question || 'Untitled question'} - - - {metric.description || 'No description'} - - - - - - - - ))} - - - )} - + + + {(assignment) => ( + <> + + Here you can configure the questions for your canvass assignment + + + + + + {editingMetric && ( + metric.definesDone + )} + metric={editingMetric} + onClose={() => setEditingMetric(null)} + onDelete={() => handleDeleteMetric(editingMetric.id)} + onSave={handleSaveMetric} + /> + )} + + {assignment.metrics.length > 0 ? 'Your list of questions:' : ''} + {assignment.metrics.map((metric) => ( + + + + + {metric.definesDone && ( + + This question defines if the mission was successful + + )} + + {metric.question || 'Untitled question'} + + + {metric.description || 'No description'} + + + + + {metric.kind == 'boolean' ? 'Yes/no' : 'Scale'} + + + + + + + + + + ))} + + + )} + + ); }; From 1b91000824a1c271b45bea298218e8d8105d3794 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Mon, 14 Oct 2024 22:06:21 +0200 Subject: [PATCH 39/44] Add some fun logic to prevent deleting the question that defines done. --- .../[canvassAssId]/editor.tsx | 111 +++++++++++++++++- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index ee6af71c8..e8b39c1ef 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -1,3 +1,4 @@ +import { Close } from '@mui/icons-material'; import { GetServerSideProps } from 'next'; import { useState } from 'react'; import { @@ -6,6 +7,8 @@ import { Card, CardActions, CardContent, + Dialog, + IconButton, Typography, } from '@mui/material'; @@ -49,6 +52,10 @@ const CanvassAssignmentEditorPage: PageWithLayout< ); const [editingMetric, setEditingMetric] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + const [idOfMetricBeingDeleted, setIdOfQuestionBeingDeleted] = useState< + string | null + >(null); const handleSaveMetric = async (metric: ZetkinMetric) => { if (canvassAssignmentFuture.data) { @@ -82,6 +89,8 @@ const CanvassAssignmentEditorPage: PageWithLayout< }); }; + //console.log(canvassAssignmentFuture?.data?.metrics); + return ( @@ -149,15 +158,109 @@ const CanvassAssignmentEditorPage: PageWithLayout< - + + {assignment.metrics.length > 1 && ( + + )} ))} + setAnchorEl(null)} open={!!anchorEl}> + + + {`Delete "${ + assignment.metrics.find( + (metric) => metric.id == idOfMetricBeingDeleted + )?.question + }"`} + { + setIdOfQuestionBeingDeleted(null); + setAnchorEl(null); + }} + > + + + + + {`If you want to delete "${ + assignment.metrics.find( + (metric) => metric.id == idOfMetricBeingDeleted + )?.question + }" you need to pick another + yes/no-question to be the question that defines if the msision + was successful`} + + + Yes/no questions + {assignment.metrics + .filter( + (metric) => + metric.kind == 'boolean' && + metric.id != idOfMetricBeingDeleted + ) + .map((metric) => ( + + {metric.question} + + + ))} + + + )} From 141809c4cf372637e68aab3b79644a785dac8e32 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Mon, 14 Oct 2024 22:48:25 +0200 Subject: [PATCH 40/44] Prevent deleting in edit mode. --- .../areas/components/Metrics/MetricCard.tsx | 13 +++++++-- .../[canvassAssId]/editor.tsx | 28 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/features/areas/components/Metrics/MetricCard.tsx b/src/features/areas/components/Metrics/MetricCard.tsx index a53615226..64a0d8d08 100644 --- a/src/features/areas/components/Metrics/MetricCard.tsx +++ b/src/features/areas/components/Metrics/MetricCard.tsx @@ -15,15 +15,17 @@ import { ZetkinMetric } from 'features/areas/types'; const MetricCard = ({ hasDefinedDone, + isOnlyQuestion, metric, onClose, onDelete, onSave, }: { hasDefinedDone: boolean; + isOnlyQuestion: boolean; metric: ZetkinMetric; onClose: () => void; - onDelete: () => void; + onDelete: (target: EventTarget & HTMLButtonElement) => void; onSave: (metric: ZetkinMetric) => void; }) => { const [question, setQuestion] = useState(metric.question || ''); @@ -80,6 +82,7 @@ const MetricCard = ({ setDefinesDone(ev.target.checked)} /> @@ -89,8 +92,12 @@ const MetricCard = ({ )} - {isEditing && ( - )} diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index e8b39c1ef..0b13f9f00 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -51,7 +51,8 @@ const CanvassAssignmentEditorPage: PageWithLayout< canvassAssId ); - const [editingMetric, setEditingMetric] = useState(null); + const [metricBeingEdited, setMetricBeingEdited] = + useState(null); const [anchorEl, setAnchorEl] = useState(null); const [idOfMetricBeingDeleted, setIdOfQuestionBeingDeleted] = useState< string | null @@ -65,7 +66,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< .concat(metric.id ? [] : [metric]), }); } - setEditingMetric(null); + setMetricBeingEdited(null); }; const handleDeleteMetric = async (id: string) => { @@ -76,11 +77,11 @@ const CanvassAssignmentEditorPage: PageWithLayout< ), }); } - setEditingMetric(null); + setMetricBeingEdited(null); }; const handleAddNewMetric = (kind: 'boolean' | 'scale5') => { - setEditingMetric({ + setMetricBeingEdited({ definesDone: false, description: '', id: '', @@ -114,14 +115,23 @@ const CanvassAssignmentEditorPage: PageWithLayout< Add scale question - {editingMetric && ( + {metricBeingEdited && ( metric.definesDone )} - metric={editingMetric} - onClose={() => setEditingMetric(null)} - onDelete={() => handleDeleteMetric(editingMetric.id)} + isOnlyQuestion={assignment.metrics.length == 1} + metric={metricBeingEdited} + onClose={() => setMetricBeingEdited(null)} + onDelete={(target: EventTarget & HTMLButtonElement) => { + if (metricBeingEdited.definesDone) { + setIdOfQuestionBeingDeleted(metricBeingEdited.id); + setAnchorEl(target); + setMetricBeingEdited(null); + } else { + handleDeleteMetric(metricBeingEdited.id); + } + }} onSave={handleSaveMetric} /> )} @@ -157,7 +167,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< - From 944b1fad2a07efb9e60a544696908ddd534d23fe Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Mon, 14 Oct 2024 22:50:27 +0200 Subject: [PATCH 41/44] Make all non-essential questions skippable, not just scale questions. --- src/features/areas/components/PlaceDialog/VisitWizard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/areas/components/PlaceDialog/VisitWizard.tsx index 1ef027658..6cd8d1fba 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/areas/components/PlaceDialog/VisitWizard.tsx @@ -46,7 +46,7 @@ const Question: FC<{ {metric.question} {metric.description} - {metric.kind == 'scale5' && ( + {!metric.definesDone && ( )} Date: Mon, 14 Oct 2024 23:16:54 +0200 Subject: [PATCH 42/44] Add stats for successful visits. --- .../[canvassAssId]/stats/route.ts | 13 +++++++++++++ src/features/areas/types.ts | 1 + .../canvassassignments/[canvassAssId]/index.tsx | 12 +++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts index fd50b7151..f8b81afc9 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts @@ -117,11 +117,15 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { ]; const visitsInAreas: Visit[] = []; + const successfulVisitsInAreas: Visit[] = []; const visitedPlacesInAreas: string[] = []; const visitedAreas: string[] = []; const householdsInAreas: Household[] = []; const configuredMetrics = model.metrics; + const idOfMetricThatDefinesDone = configuredMetrics.find( + (metric) => metric.definesDone + )?._id; const accumulatedMetrics: ZetkinCanvassAssignmentStats['metrics'] = configuredMetrics.map((metric) => ({ metric: { @@ -170,6 +174,14 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { visitedAreas.push(place.areaId); visitedPlacesInAreas.push(place.id); visitsInAreas.push(visit); + + visit.responses.forEach((response) => { + if (response.metricId == idOfMetricThatDefinesDone) { + if (response.response == 'yes') { + successfulVisitsInAreas.push(visit); + } + } + }); } }); }); @@ -202,6 +214,7 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { num_areas: uniqueAreas.length, num_households: householdsInAreas.length, num_places: uniquePlacesInAreas.length, + num_successful_visited_households: successfulVisitsInAreas.length, num_visited_areas: Array.from(new Set(visitedAreas)).length, num_visited_households: visitsInAreas.length, num_visited_households_outside_areas: visitsOutsideAreas.length, diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index b5e918c99..bb141cf15 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -8,6 +8,7 @@ export type ZetkinCanvassAssignmentStats = { num_areas: number; num_households: number; num_places: number; + num_successful_visited_households: number; num_visited_areas: number; num_visited_households: number; num_visited_households_outside_areas: number; diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx index a301f58fe..f9670ac0c 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx @@ -224,17 +224,23 @@ const CanvassAssignmentPage: PageWithLayout = ({ values={[ { color: theme.palette.primary.main, - value: stats.num_visited_households, + value: stats.num_successful_visited_households, }, { - color: lighten(theme.palette.primary.main, 0.6), + color: lighten(theme.palette.primary.main, 0.5), + value: + stats.num_visited_households - + stats.num_successful_visited_households, + }, + { + color: lighten(theme.palette.primary.main, 0.8), value: stats.num_households - stats.num_visited_households, }, ]} /> - {`${stats.num_visited_households} logged`} + {`${stats.num_successful_visited_households} success of ${stats.num_visited_households} visits`} From 466d769325f7149fb6dfaaa0f9983428f65cd3d8 Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Tue, 15 Oct 2024 22:16:21 +0200 Subject: [PATCH 43/44] Move and clean up canvass assignments file structure. --- .../assignees/[assigneeId]/route.ts | 2 +- .../[canvassAssId]/assignees/route.ts | 4 +- .../[canvassAssId]/route.ts | 13 +- .../[canvassAssId]/sessions/route.ts | 5 +- .../[canvassAssId]/stats/route.ts | 19 +- .../orgs/[orgId]/canvassassignments/route.ts | 4 +- .../households/[householdId]/route.ts | 3 +- .../households/[householdId]/visits/route.ts | 3 +- .../places/[placeId]/households/route.ts | 3 +- .../orgs/[orgId]/places/[placeId]/route.ts | 3 +- src/app/beta/orgs/[orgId]/places/route.ts | 7 +- .../beta/users/me/canvassassignments/route.ts | 5 +- src/app/my/canvassassignments/page.tsx | 2 +- src/app/o/[orgId]/areas/[areaId]/page.tsx | 4 +- src/core/store.ts | 9 +- .../AreaFilters/AddFilterButton.tsx | 4 +- .../AreaFilters/AreaFilterButton.tsx | 4 +- .../areas/components/AreaFilters/index.tsx | 13 +- .../components/AreaOverlay/TagsSection.tsx | 6 +- .../areas/components/AreaOverlay/index.tsx | 15 +- .../areas/components/AreaPlanningOverlay.tsx | 20 +- .../areas/components/AreasMap/index.tsx | 17 +- .../components/PlaceDialog/EditPlace.tsx | 78 ----- src/features/areas/l10n/messageIds.ts | 113 ------- src/features/areas/models.ts | 112 +------ src/features/areas/store.ts | 293 +--------------- src/features/areas/types.ts | 106 +----- .../items/CanvassAssignmentListItem.tsx | 2 +- .../components/CampaignActionButtons.tsx | 2 +- .../campaigns/hooks/useActivityArchive.ts | 2 +- .../campaigns/hooks/useActivityList.ts | 2 +- .../campaigns/hooks/useActivityOverview.ts | 2 +- src/features/campaigns/types.ts | 2 +- .../components/AssignmentMetricsChart.tsx | 0 .../components/CreatePlaceCard.tsx | 44 +-- .../components}/MetricCard.tsx | 26 +- .../components/MyCanvassAssignmentsPage.tsx | 16 +- .../components/PlaceDialog/EditPlace.tsx | 43 +++ .../components/PlaceDialog/Household.tsx | 2 +- .../components/PlaceDialog/Place.tsx | 21 +- .../components/PlaceDialog/VisitWizard.tsx | 8 +- .../components/PlaceDialog/index.tsx | 53 +-- .../components/PlanMap.tsx | 20 +- .../components/PlanMapRenderer.tsx | 5 +- .../components/PublicAreaMap.tsx | 15 +- .../components/PublicAreaPage.tsx} | 14 +- .../hooks/useAddAssignee.ts | 0 .../hooks/useAssigneeMutations.ts | 0 .../hooks/useAssignees.ts | 3 +- .../hooks/useCanvassAssignment.ts | 2 +- .../hooks/useCanvassAssignmentActivities.ts | 4 +- .../hooks/useCanvassAssignmentMutations.ts | 0 .../hooks/useCanvassAssignmentStats.ts | 2 +- .../hooks/useCanvassSessions.ts | 2 +- .../hooks/useCreateCanvassAssignment.ts | 0 .../hooks/useCreateCanvassSession.ts | 0 .../hooks/useCreatePlace.ts | 0 .../hooks/useMyCanvassSessions.ts | 4 +- .../hooks/usePlaceMutations.ts | 0 .../hooks/usePlaces.ts | 4 +- .../layouts/CanvassAssignmentLayout.tsx | 9 +- src/features/canvassAssignments/models.ts | 90 +++++ src/features/canvassAssignments/store.ts | 316 ++++++++++++++++++ src/features/canvassAssignments/types.ts | 107 ++++++ .../utils/getCrosshairPositionOnMap.tsx | 0 .../utils/isPointInsidePolygon.ts | 0 .../utils/isWithinLast24Hours.tsx | 0 .../utils/markerIcon.tsx | 0 src/pages/organize/[orgId]/areas/index.tsx | 2 +- .../[canvassAssId]/canvassers.tsx | 13 +- .../[canvassAssId]/editor.tsx | 42 ++- .../[canvassAssId]/index.tsx | 37 +- .../[canvassAssId]/plan.tsx | 11 +- src/utils/testing/mocks/mockState.ts | 14 +- 74 files changed, 771 insertions(+), 1037 deletions(-) delete mode 100644 src/features/areas/components/PlaceDialog/EditPlace.tsx delete mode 100644 src/features/areas/l10n/messageIds.ts rename src/features/{areas => canvassAssignments}/components/AssignmentMetricsChart.tsx (100%) rename src/features/{areas => canvassAssignments}/components/CreatePlaceCard.tsx (50%) rename src/features/{areas/components/Metrics => canvassAssignments/components}/MetricCard.tsx (93%) rename src/features/{areas => canvassAssignments}/components/MyCanvassAssignmentsPage.tsx (83%) create mode 100644 src/features/canvassAssignments/components/PlaceDialog/EditPlace.tsx rename src/features/{areas => canvassAssignments}/components/PlaceDialog/Household.tsx (97%) rename src/features/{areas => canvassAssignments}/components/PlaceDialog/Place.tsx (74%) rename src/features/{areas => canvassAssignments}/components/PlaceDialog/VisitWizard.tsx (97%) rename src/features/{areas => canvassAssignments}/components/PlaceDialog/index.tsx (87%) rename src/features/{areas => canvassAssignments}/components/PlanMap.tsx (87%) rename src/features/{areas => canvassAssignments}/components/PlanMapRenderer.tsx (97%) rename src/features/{areas => canvassAssignments}/components/PublicAreaMap.tsx (96%) rename src/features/{areas/components/AreaPage.tsx => canvassAssignments/components/PublicAreaPage.tsx} (81%) rename src/features/{areas => canvassAssignments}/hooks/useAddAssignee.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/useAssigneeMutations.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/useAssignees.ts (89%) rename src/features/{areas => canvassAssignments}/hooks/useCanvassAssignment.ts (93%) rename src/features/{areas => canvassAssignments}/hooks/useCanvassAssignmentActivities.ts (94%) rename src/features/{areas => canvassAssignments}/hooks/useCanvassAssignmentMutations.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/useCanvassAssignmentStats.ts (91%) rename src/features/{areas => canvassAssignments}/hooks/useCanvassSessions.ts (91%) rename src/features/{areas => canvassAssignments}/hooks/useCreateCanvassAssignment.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/useCreateCanvassSession.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/useCreatePlace.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/useMyCanvassSessions.ts (87%) rename src/features/{areas => canvassAssignments}/hooks/usePlaceMutations.ts (100%) rename src/features/{areas => canvassAssignments}/hooks/usePlaces.ts (86%) rename src/features/{areas => canvassAssignments}/layouts/CanvassAssignmentLayout.tsx (82%) create mode 100644 src/features/canvassAssignments/models.ts create mode 100644 src/features/canvassAssignments/store.ts create mode 100644 src/features/canvassAssignments/types.ts rename src/features/{areas => canvassAssignments}/utils/getCrosshairPositionOnMap.tsx (100%) rename src/features/{areas => canvassAssignments}/utils/isPointInsidePolygon.ts (100%) rename src/features/{areas => canvassAssignments}/utils/isWithinLast24Hours.tsx (100%) rename src/features/{areas => canvassAssignments}/utils/markerIcon.tsx (100%) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/[assigneeId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/[assigneeId]/route.ts index 1adfd98b4..d543badc3 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/[assigneeId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/[assigneeId]/route.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; -import { CanvassAssigneeModel } from 'features/areas/models'; +import { CanvassAssigneeModel } from 'features/canvassAssignments/models'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; type RouteMeta = { diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/route.ts index 0f2446f80..5f4bae82a 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/assignees/route.ts @@ -1,9 +1,9 @@ import mongoose from 'mongoose'; import { NextRequest } from 'next/server'; +import { ZetkinCanvassAssignee } from 'features/canvassAssignments/types'; +import { CanvassAssigneeModel } from 'features/canvassAssignments/models'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { CanvassAssigneeModel } from 'features/areas/models'; -import { ZetkinCanvassAssignee } from 'features/areas/types'; type RouteMeta = { params: { diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index 3809508c7..6ee0444cb 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -2,8 +2,11 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { CanvassAssignmentModel } from 'features/areas/models'; -import { ZetkinCanvassAssignment, ZetkinMetric } from 'features/areas/types'; +import { CanvassAssignmentModel } from 'features/canvassAssignments/models'; +import { + ZetkinCanvassAssignment, + ZetkinMetric, +} from 'features/canvassAssignments/types'; type RouteMeta = { params: { @@ -35,9 +38,9 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { campaign: { id: canvassAssignmentModel.campId }, id: canvassAssignmentModel._id.toString(), metrics: (canvassAssignmentModel.metrics || []).map((metric) => ({ + _id: metric._id, definesDone: metric.definesDone || false, description: metric.description || '', - id: metric._id, kind: metric.kind, question: metric.question, })), @@ -79,7 +82,7 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { // Identify metrics to be deleted const metricsToDelete = existingMetricsIds.filter( - (id) => !newMetrics.some((metric: ZetkinMetric) => metric.id === id) + (id) => !newMetrics.some((metric: ZetkinMetric) => metric._id === id) ); // Remove metrics that are no longer included @@ -133,9 +136,9 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { campaign: { id: model.campId }, id: model._id.toString(), metrics: (model.metrics || []).map((metric) => ({ + _id: metric._id, definesDone: metric.definesDone || false, description: metric.description || '', - id: metric._id, kind: metric.kind, question: metric.question, })), diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/sessions/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/sessions/route.ts index 3fd0e1ebe..e7796c87a 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/sessions/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/sessions/route.ts @@ -1,10 +1,11 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; -import { AreaModel, CanvassAssignmentModel } from 'features/areas/models'; -import { ZetkinCanvassSession } from 'features/areas/types'; +import { CanvassAssignmentModel } from 'features/canvassAssignments/models'; +import { ZetkinCanvassSession } from 'features/canvassAssignments/types'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; import { ZetkinPerson } from 'utils/types/zetkin'; +import { AreaModel } from 'features/areas/models'; type RouteMeta = { params: { diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts index f8b81afc9..cafe58aad 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts @@ -1,22 +1,22 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; +import asOrgAuthorized from 'utils/api/asOrgAuthorized'; +import { ZetkinPerson } from 'utils/types/zetkin'; +import isPointInsidePolygon from 'features/canvassAssignments/utils/isPointInsidePolygon'; import { - AreaModel, CanvassAssignmentModel, PlaceModel, -} from 'features/areas/models'; +} from 'features/canvassAssignments/models'; import { Household, Visit, - ZetkinArea, ZetkinCanvassAssignmentStats, ZetkinCanvassSession, ZetkinPlace, -} from 'features/areas/types'; -import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { ZetkinPerson } from 'utils/types/zetkin'; -import isPointInsidePolygon from 'features/areas/utils/isPointInsidePolygon'; +} from 'features/canvassAssignments/types'; +import { AreaModel } from 'features/areas/models'; +import { ZetkinArea } from 'features/areas/types'; type RouteMeta = { params: { @@ -91,7 +91,6 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, })); type PlaceWithAreaId = ZetkinPlace & { areaId: ZetkinArea['id'] }; @@ -129,9 +128,9 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { const accumulatedMetrics: ZetkinCanvassAssignmentStats['metrics'] = configuredMetrics.map((metric) => ({ metric: { + _id: metric._id, definesDone: metric.definesDone, description: metric.description, - id: metric._id, kind: metric.kind, question: metric.question, }, @@ -148,7 +147,7 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { ); const accumulatedMetric = accumulatedMetrics.find( - (accum) => accum.metric.id == response.metricId + (accum) => accum.metric._id == response.metricId ); if (accumulatedMetric && configuredMetric) { diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/route.ts index e897841fe..e032021a6 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/route.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { CanvassAssignmentModel } from 'features/areas/models'; +import { CanvassAssignmentModel } from 'features/canvassAssignments/models'; type RouteMeta = { params: { @@ -71,9 +71,9 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { campaign: { id: model.campId }, id: model._id.toString(), metrics: model.metrics.map((metric) => ({ + _id: metric._id, definesDone: metric.definesDone || false, description: metric.description || '', - id: metric._id, kind: metric.kind, question: metric.question, })), diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/route.ts index c2cabd89a..e4dbf5dbe 100644 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/route.ts +++ b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/route.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { PlaceModel } from 'features/areas/models'; +import { PlaceModel } from 'features/canvassAssignments/models'; type RouteMeta = { params: { @@ -47,7 +47,6 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, }, }); } diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts index 801150a2c..0b3b03506 100644 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts +++ b/src/app/beta/orgs/[orgId]/places/[placeId]/households/[householdId]/visits/route.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; -import { PlaceModel } from 'features/areas/models'; +import { PlaceModel } from 'features/canvassAssignments/models'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; type RouteMeta = { @@ -59,7 +59,6 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, }, }); } diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/households/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/households/route.ts index de4f6c43e..6e4601ca5 100644 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/households/route.ts +++ b/src/app/beta/orgs/[orgId]/places/[placeId]/households/route.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; -import { PlaceModel } from 'features/areas/models'; +import { PlaceModel } from 'features/canvassAssignments/models'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; type RouteMeta = { @@ -57,7 +57,6 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, }, }); } diff --git a/src/app/beta/orgs/[orgId]/places/[placeId]/route.ts b/src/app/beta/orgs/[orgId]/places/[placeId]/route.ts index beaa2bda1..69c1fc067 100644 --- a/src/app/beta/orgs/[orgId]/places/[placeId]/route.ts +++ b/src/app/beta/orgs/[orgId]/places/[placeId]/route.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { PlaceModel } from 'features/areas/models'; +import { PlaceModel } from 'features/canvassAssignments/models'; type RouteMeta = { params: { @@ -47,7 +47,6 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, }, }); } diff --git a/src/app/beta/orgs/[orgId]/places/route.ts b/src/app/beta/orgs/[orgId]/places/route.ts index f85daa914..5967ff192 100644 --- a/src/app/beta/orgs/[orgId]/places/route.ts +++ b/src/app/beta/orgs/[orgId]/places/route.ts @@ -2,8 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import mongoose from 'mongoose'; import asOrgAuthorized from 'utils/api/asOrgAuthorized'; -import { PlaceModel } from 'features/areas/models'; -import { ZetkinPlace } from 'features/areas/types'; +import { PlaceModel } from 'features/canvassAssignments/models'; +import { ZetkinPlace } from 'features/canvassAssignments/types'; type RouteMeta = { params: { @@ -29,7 +29,6 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, })); return Response.json({ data: places }); @@ -55,7 +54,6 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: payload.position, title: payload.title, - type: payload.type, }); await model.save(); @@ -68,7 +66,6 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { orgId: orgId, position: model.position, title: model.title, - type: model.type, }, }); } diff --git a/src/app/beta/users/me/canvassassignments/route.ts b/src/app/beta/users/me/canvassassignments/route.ts index a76b7d6b3..c0edec3a4 100644 --- a/src/app/beta/users/me/canvassassignments/route.ts +++ b/src/app/beta/users/me/canvassassignments/route.ts @@ -3,9 +3,10 @@ import mongoose from 'mongoose'; import { NextRequest, NextResponse } from 'next/server'; import BackendApiClient from 'core/api/client/BackendApiClient'; -import { AreaModel, CanvassAssignmentModel } from 'features/areas/models'; -import { ZetkinCanvassSession } from 'features/areas/types'; +import { CanvassAssignmentModel } from 'features/canvassAssignments/models'; +import { ZetkinCanvassSession } from 'features/canvassAssignments/types'; import { ZetkinMembership, ZetkinPerson } from 'utils/types/zetkin'; +import { AreaModel } from 'features/areas/models'; export async function GET(request: NextRequest) { const headers: IncomingHttpHeaders = {}; diff --git a/src/app/my/canvassassignments/page.tsx b/src/app/my/canvassassignments/page.tsx index 031635108..5fc6e02c1 100644 --- a/src/app/my/canvassassignments/page.tsx +++ b/src/app/my/canvassassignments/page.tsx @@ -2,7 +2,7 @@ import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; import BackendApiClient from 'core/api/client/BackendApiClient'; -import MyCanvassAssignmentsPage from 'features/areas/components/MyCanvassAssignmentsPage'; +import MyCanvassAssignmentsPage from 'features/canvassAssignments/components/MyCanvassAssignmentsPage'; import { ZetkinOrganization } from 'utils/types/zetkin'; export default async function Page() { diff --git a/src/app/o/[orgId]/areas/[areaId]/page.tsx b/src/app/o/[orgId]/areas/[areaId]/page.tsx index 45d798c2b..bd2a57f3c 100644 --- a/src/app/o/[orgId]/areas/[areaId]/page.tsx +++ b/src/app/o/[orgId]/areas/[areaId]/page.tsx @@ -1,7 +1,7 @@ import 'leaflet/dist/leaflet.css'; import { notFound } from 'next/navigation'; -import AreaPage from 'features/areas/components/AreaPage'; +import PublicAreaPage from 'features/canvassAssignments/components/PublicAreaPage'; import { AREAS, hasFeature } from 'utils/featureFlags'; interface PageProps { @@ -19,5 +19,5 @@ export default function Page({ params }: PageProps) { return notFound(); } - return ; + return ; } diff --git a/src/core/store.ts b/src/core/store.ts index 7458f4839..b0c49058d 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -49,15 +49,17 @@ import tagsSlice, { TagsStoreSlice } from 'features/tags/store'; import tasksSlice, { TasksStoreSlice } from 'features/tasks/store'; import userSlice, { UserStoreSlice } from 'features/user/store'; import viewsSlice, { ViewsStoreSlice } from 'features/views/store'; -import areasSlice, { - AreasStoreSlice, +import areasSlice, { AreasStoreSlice } from 'features/areas/store'; +import canvassAssignmentSlice, { canvassAssignmentCreated, -} from 'features/areas/store'; + CanvassAssignmentsStoreSlice, +} from 'features/canvassAssignments/store'; export interface RootState { areas: AreasStoreSlice; breadcrumbs: BreadcrumbsStoreSlice; callAssignments: CallAssignmentSlice; + canvassAssignments: CanvassAssignmentsStoreSlice; campaigns: CampaignsStoreSlice; duplicates: PotentialDuplicatesStoreSlice; emails: EmailStoreSlice; @@ -83,6 +85,7 @@ const reducer = { breadcrumbs: breadcrumbsSlice.reducer, callAssignments: callAssignmentsSlice.reducer, campaigns: campaignsSlice.reducer, + canvassAssignments: canvassAssignmentSlice.reducer, duplicates: potentialDuplicatesSlice.reducer, emails: emailsSlice.reducer, events: eventsSlice.reducer, diff --git a/src/features/areas/components/AreaFilters/AddFilterButton.tsx b/src/features/areas/components/AreaFilters/AddFilterButton.tsx index a6fa40fa2..17ada2b3e 100644 --- a/src/features/areas/components/AreaFilters/AddFilterButton.tsx +++ b/src/features/areas/components/AreaFilters/AddFilterButton.tsx @@ -6,8 +6,6 @@ import { MenuItem } from '@mui/material'; import { FC, useState } from 'react'; import theme from 'theme'; -import messageIds from 'features/areas/l10n/messageIds'; -import { Msg } from 'core/i18n'; type Props = { items: { @@ -31,7 +29,7 @@ const AddFilterButton: FC = ({ items, open, onToggle }) => { startIcon={} variant="text" > - + Add filter void; @@ -27,7 +25,7 @@ const AreaFilterButton: FC = ({ onToggle }) => { ) } > - + Filter ); }; diff --git a/src/features/areas/components/AreaFilters/index.tsx b/src/features/areas/components/AreaFilters/index.tsx index 85d11dc46..c88a63d06 100644 --- a/src/features/areas/components/AreaFilters/index.tsx +++ b/src/features/areas/components/AreaFilters/index.tsx @@ -4,8 +4,6 @@ import { useTheme } from '@mui/styles'; import { ZetkinArea } from 'features/areas/types'; import { ZetkinTag, ZetkinTagGroup } from 'utils/types/zetkin'; -import { useMessages } from 'core/i18n'; -import messageIds from 'features/areas/l10n/messageIds'; import FilterDropDown from '../FilterDropDown'; import { areaFilterContext } from './AreaFilterContext'; import AddFilterButton from './AddFilterButton'; @@ -24,7 +22,6 @@ const AreaFilters: FC = ({ areas, onFilteredIdsChange }) => { activeTagIdsByGroup, setActiveTagIdsByGroup, } = useContext(areaFilterContext); - const messages = useMessages(messageIds); const groupsById = useMemo(() => { const groupsById: Record< @@ -96,11 +93,7 @@ const AreaFilters: FC = ({ areas, onFilteredIdsChange }) => { }, }; })} - label={ - info.group - ? info.group.title - : messages.filters.tagsWithoutGroup() - } + label={info.group ? info.group.title : 'Ungrouped tags'} onToggle={(open) => setOpenDropdown(open ? groupId : null)} open={openDropdown == groupId} startIcon={ @@ -141,9 +134,7 @@ const AreaFilters: FC = ({ areas, onFilteredIdsChange }) => { return { icon: , - label: item.group - ? messages.filters.tagGroup({ label: item.group.title }) - : messages.filters.tagsWithoutGroup(), + label: item.group ? item.group.title : 'Ungrouped tags', onClick: () => { if (selected) { setActiveGroupIds(activeGroupIds.filter((id) => groupId != id)); diff --git a/src/features/areas/components/AreaOverlay/TagsSection.tsx b/src/features/areas/components/AreaOverlay/TagsSection.tsx index e9798c2a6..672e251e5 100644 --- a/src/features/areas/components/AreaOverlay/TagsSection.tsx +++ b/src/features/areas/components/AreaOverlay/TagsSection.tsx @@ -1,10 +1,8 @@ import { FC, useState } from 'react'; import { Box, Typography } from '@mui/material'; -import { Msg } from 'core/i18n'; import useAreaTagging from 'features/areas/hooks/useAreaTagging'; import useAreaTags from 'features/areas/hooks/useAreaTags'; -import messageIds from 'features/areas/l10n/messageIds'; import { ZetkinArea } from 'features/areas/types'; import TagManager from 'features/tags/components/TagManager'; import GroupToggle from 'features/tags/components/TagManager/components/GroupToggle'; @@ -33,9 +31,7 @@ const TagsSection: FC = ({ area }) => { mb={2} minHeight={38} > - - - + Area tags = ({ area.organization.id, area.id ); - const messages = useMessages(messageIds); const { showConfirmDialog } = useContext(ZUIConfirmDialogContext); const handleDescriptionTextAreaRef = useCallback( @@ -116,7 +113,7 @@ const AreaOverlay: FC = ({ )} renderPreview={() => ( - {area.title || messages.empty.title()} + {area.title || 'Untitled area'} )} value={area.title || ''} @@ -165,7 +162,7 @@ const AreaOverlay: FC = ({ > {area.description?.trim().length ? area.description - : messages.empty.description()} + : 'Empty description'} )} @@ -189,22 +186,22 @@ const AreaOverlay: FC = ({ }} variant="contained" > - + Save )} {!editing && ( <> { showConfirmDialog({ onSubmit: () => { diff --git a/src/features/areas/components/AreaPlanningOverlay.tsx b/src/features/areas/components/AreaPlanningOverlay.tsx index b505184fb..ca09b8c84 100644 --- a/src/features/areas/components/AreaPlanningOverlay.tsx +++ b/src/features/areas/components/AreaPlanningOverlay.tsx @@ -3,8 +3,6 @@ import { Close } from '@mui/icons-material'; import { Box, Divider, Paper, Typography } from '@mui/material'; import { ZetkinArea } from '../types'; -import { Msg, useMessages } from 'core/i18n'; -import messageIds from '../l10n/messageIds'; import { ZetkinPerson } from 'utils/types/zetkin'; import ZUIPerson from 'zui/ZUIPerson'; import { MUIOnlyPersonSelect as ZUIPersonSelect } from 'zui/ZUIPersonSelect'; @@ -22,8 +20,6 @@ const AreaPlanningOverlay: FC = ({ onAddAssignee, onClose, }) => { - const messages = useMessages(messageIds); - return ( = ({ > - - {area.title || messages.empty.title()} - + {area.title || 'Untitled area'} { @@ -61,23 +55,21 @@ const AreaPlanningOverlay: FC = ({ > {area.description?.trim().length ? area.description - : messages.empty.description()} + : 'Empty description'} - - - + Assignees {!assignees.length && ( - + No assignees )} {assignees.map((assignee) => ( @@ -90,9 +82,7 @@ const AreaPlanningOverlay: FC = ({ ))} - - - + Add assignee = ({ areas }) => { - const messages = useMessages(messageIds); const mapRef = useRef(null); const [drawingPoints, setDrawingPoints] = useState(null); const [selectedId, setSelectedId] = useState(''); @@ -92,8 +89,8 @@ const Map: FC = ({ areas }) => { inputValue.length == 0 ? areas.concat() : areas.filter((area) => { - const areaTitle = area.title || messages.empty.title(); - const areaDesc = area.description || messages.empty.description(); + const areaTitle = area.title || 'Untitled area'; + const areaDesc = area.description || 'Empty description'; return ( areaTitle.toLowerCase().includes(inputValue) || @@ -131,7 +128,7 @@ const Map: FC = ({ areas }) => { }} startIcon={} > - + Draw )} {drawingPoints && ( @@ -141,7 +138,7 @@ const Map: FC = ({ areas }) => { }} startIcon={} > - + Cancel )} {drawingPoints && drawingPoints.length > 2 && ( @@ -151,7 +148,7 @@ const Map: FC = ({ areas }) => { }} startIcon={} > - + Save )} @@ -188,9 +185,7 @@ const Map: FC = ({ areas }) => { /> )} renderOption={(props, area) => ( - - {area.title || messages.empty.title()} - + {area.title || 'Untitled area'} )} value={null} /> diff --git a/src/features/areas/components/PlaceDialog/EditPlace.tsx b/src/features/areas/components/PlaceDialog/EditPlace.tsx deleted file mode 100644 index b9b540ef7..000000000 --- a/src/features/areas/components/PlaceDialog/EditPlace.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - Box, - FormControl, - InputLabel, - MenuItem, - Select, - TextField, -} from '@mui/material'; -import { FC } from 'react'; - -import { Msg, useMessages } from 'core/i18n'; -import messageIds from 'features/areas/l10n/messageIds'; -import { PlaceType } from '.'; - -type EditPlaceProps = { - description: string; - onDescriptionChange: (newDescription: string) => void; - onTitleChange: (newTitle: string) => void; - onTypeChange: (newType: PlaceType) => void; - title: string; - type: PlaceType; -}; - -const EditPlace: FC = ({ - description, - onDescriptionChange, - onTitleChange, - onTypeChange, - title, - type, -}) => { - const messages = useMessages(messageIds); - return ( - - onTitleChange(ev.target.value)} - /> - - - - - - - onDescriptionChange(ev.target.value)} - rows={5} - /> - - ); -}; - -export default EditPlace; diff --git a/src/features/areas/l10n/messageIds.ts b/src/features/areas/l10n/messageIds.ts deleted file mode 100644 index 3ec66ab09..000000000 --- a/src/features/areas/l10n/messageIds.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { m, makeMessages } from 'core/i18n'; - -export default makeMessages('feat.areas', { - addNewPlaceButton: m('Add new place'), - canvassAssignment: { - addAssignee: m('Add assignee'), - assigneesTitle: m('Assignees'), - canvassers: { - areasColumn: m('Areas'), - nameColumn: m('Name'), - }, - canvassing: { - goToMapButton: m('Go to map'), - title: m('Canvassing'), - }, - empty: { - title: m('Untitled canvass assignment'), - }, - overview: { - areas: { - defineButton: m('Plan now'), - editButton: m('Edit plan'), - subtitle: m('This assignment has not been planned yet.'), - title: m('Areas'), - }, - }, - planFilters: { - assigned: m('Assigned'), - unassigned: m('Unassigned'), - }, - tabs: { - canvassers: m('Canvassers'), - overview: m('Overview'), - plan: m('Plan'), - }, - }, - empty: { - description: m('Empty description'), - title: m('Untitled area'), - }, - filters: { - addFilterButton: m('Add filter'), - filterButton: m('Filters'), - tagGroup: m<{ label: string }>('{label} (tag group)'), - tagsWithoutGroup: m('Tags without group'), - }, - overlay: { - buttons: { - cancel: m('Cancel'), - delete: m('Delete'), - edit: m('Edit'), - save: m('Save'), - }, - tags: { - header: m('Tags'), - }, - }, - place: { - activityHeader: m('Activity'), - addHouseholdButton: m('Add household'), - backButton: m('Back'), - badRatingLog: m('Bad rating'), - cancelButton: m('Cancel'), - closeButton: m('Close'), - description: m('Description'), - editButton: m('Edit'), - editDescription: m('Place description'), - editPlace: m<{ placeName: string }>('Edit {placeName}'), - editTitle: m('Place title'), - empty: { - description: m('Empty description'), - title: m('Untitled place'), - }, - goodRatingLog: m('Good rating'), - household: { - empty: { - title: m('Untitled household'), - }, - }, - householdsHeader: m<{ numberOfHouseholds: number }>( - '{numberOfHouseholds, plural, =0 {No households} =1 {Household 1} other {Households #}}' - ), - logActivityButton: m('Log activity'), - logActivityHeader: m<{ title: string }>('Log activity at {title}'), - logList: m('Log'), - logVisit: m('Log visit'), - noActivity: m('No visits have been recorded at this place.'), - notePlaceholder: m('Note'), - saveButton: m('Save'), - selectType: m('Place type'), - visitedButton: m('Visited'), - }, - placeCard: { - address: m('Address'), - cancel: m('Cancel'), - createPlace: m('Create place'), - inputLabel: m('Type of place'), - misc: m('Misc'), - placeholderAddress: m('Enter address here'), - placeholderTitle: m('Enter title here'), - }, - planOverlay: { - addAssignee: m('Add assignee'), - assignees: m('Assignees'), - noAssignees: m('No assignees'), - }, - tools: { - cancel: m('Cancel'), - draw: m('Draw'), - save: m('Save'), - }, - viewPlaceButton: m('View place'), -}); diff --git a/src/features/areas/models.ts b/src/features/areas/models.ts index dd558f575..87fc31607 100644 --- a/src/features/areas/models.ts +++ b/src/features/areas/models.ts @@ -1,11 +1,6 @@ import mongoose from 'mongoose'; -import { - Household, - ZetkinArea, - ZetkinCanvassAssignee, - ZetkinPlace, -} from './types'; +import { ZetkinArea } from './types'; type ZetkinAreaModelType = { description: string | null; @@ -15,35 +10,6 @@ type ZetkinAreaModelType = { title: string | null; }; -type ZetkinPlaceModelType = { - description: string | null; - households: Household[]; - orgId: number; - position: ZetkinPlace['position']; - title: string | null; - type: 'address' | 'misc'; -}; - -type ZetkinCanvassAssignmentModelType = { - campId: number; - id: number; - metrics: { - _id: string; - definesDone: boolean; - description: string; - kind: 'boolean' | 'scale5'; - question: string; - }[]; - orgId: number; - sessions: { - areaId: string; - personId: number; - }[]; - title: string | null; -}; - -type ZetkinCanvassAssigneeModelType = ZetkinCanvassAssignee; - const areaSchema = new mongoose.Schema({ description: String, orgId: { required: true, type: Number }, @@ -52,82 +18,6 @@ const areaSchema = new mongoose.Schema({ title: String, }); -const placeSchema = new mongoose.Schema({ - description: String, - households: [ - { - _id: false, - id: String, - title: String, - visits: [ - { - _id: false, - canvassAssId: String, - id: String, - noteToOfficial: String, - responses: [ - { - metricId: String, - response: String, - }, - ], - timestamp: String, - }, - ], - }, - ], - orgId: { required: true, type: Number }, - position: Object, - title: String, - type: String, -}); - -const canvassAssignmentSchema = - new mongoose.Schema({ - campId: Number, - metrics: [ - { - definesDone: Boolean, - description: String, - id: String, - kind: String, - question: String, - }, - ], - orgId: { required: true, type: Number }, - sessions: [ - { - areaId: String, - personId: Number, - }, - ], - title: String, - }); - -const canvassAssigneeSchema = - new mongoose.Schema({ - canvassAssId: String, - id: { required: true, type: Number }, - }); - export const AreaModel: mongoose.Model = mongoose.models.Area || mongoose.model('Area', areaSchema); - -export const PlaceModel: mongoose.Model = - mongoose.models.Place || - mongoose.model('Place', placeSchema); - -export const CanvassAssignmentModel: mongoose.Model = - mongoose.models.CanvassAssignment || - mongoose.model( - 'CanvassAssignment', - canvassAssignmentSchema - ); - -export const CanvassAssigneeModel: mongoose.Model = - mongoose.models.CanvassAssignee || - mongoose.model( - 'CanvassAssignee', - canvassAssigneeSchema - ); diff --git a/src/features/areas/store.ts b/src/features/areas/store.ts index 0c58065ce..c17504226 100644 --- a/src/features/areas/store.ts +++ b/src/features/areas/store.ts @@ -2,49 +2,20 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { findOrAddItem, - RemoteItem, remoteItem, remoteList, RemoteList, } from 'utils/storeUtils'; -import { - ZetkinCanvassAssignmentStats, - ZetkinArea, - ZetkinCanvassAssignee, - ZetkinCanvassAssignment, - ZetkinCanvassSession, - ZetkinPlace, -} from './types'; +import { ZetkinArea } from './types'; import { ZetkinTag } from 'utils/types/zetkin'; export interface AreasStoreSlice { areaList: RemoteList; - canvassAssignmentList: RemoteList; - sessionsByAssignmentId: Record< - string, - RemoteList - >; - assigneesByCanvassAssignmentId: Record< - string, - RemoteList - >; - mySessionsList: RemoteList; - placeList: RemoteList; - statsByCanvassAssId: Record< - string, - RemoteItem - >; tagsByAreaId: Record>; } const initialState: AreasStoreSlice = { areaList: remoteList(), - assigneesByCanvassAssignmentId: {}, - canvassAssignmentList: remoteList(), - mySessionsList: remoteList(), - placeList: remoteList(), - sessionsByAssignmentId: {}, - statsByCanvassAssId: {}, tagsByAreaId: {}, }; @@ -108,246 +79,6 @@ const areasSlice = createSlice({ state.areaList.loaded = timestamp; state.areaList.items.forEach((item) => (item.loaded = timestamp)); }, - assigneeAdd: (state, action: PayloadAction<[string, number]>) => { - const [canvassAssId, assigneeId] = action.payload; - - if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { - state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); - } - - state.assigneesByCanvassAssignmentId[canvassAssId].items.push( - remoteItem(assigneeId, { isLoading: true }) - ); - }, - assigneeAdded: ( - state, - action: PayloadAction<[string, ZetkinCanvassAssignee]> - ) => { - const [canvassAssId, assignee] = action.payload; - - if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { - state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); - } - - state.assigneesByCanvassAssignmentId[canvassAssId].items = - state.assigneesByCanvassAssignmentId[canvassAssId].items - .filter((item) => item.id != assignee.id) - .concat([ - remoteItem(assignee.canvassAssId, { - data: assignee, - }), - ]); - }, - assigneeUpdated: ( - state, - action: PayloadAction<[string, ZetkinCanvassAssignee]> - ) => { - const [canvassAssId, assignee] = action.payload; - - if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { - state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); - } - - state.assigneesByCanvassAssignmentId[canvassAssId].items - .filter((item) => item.id == assignee.id) - .concat([ - remoteItem(assignee.canvassAssId, { - data: assignee, - }), - ]); - }, - assigneesLoad: (state, action: PayloadAction) => { - const canvassAssId = action.payload; - - if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { - state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); - } - - state.assigneesByCanvassAssignmentId[canvassAssId].isLoading = true; - }, - assigneesLoaded: ( - state, - action: PayloadAction<[string, ZetkinCanvassAssignee[]]> - ) => { - const [canvassAssId, assignees] = action.payload; - - if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { - state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); - } - - state.assigneesByCanvassAssignmentId[canvassAssId] = - remoteList(assignees); - state.assigneesByCanvassAssignmentId[canvassAssId].loaded = - new Date().toISOString(); - }, - canvassAssignmentCreated: ( - state, - action: PayloadAction - ) => { - const canvassAssignment = action.payload; - const item = remoteItem(canvassAssignment.id, { - data: canvassAssignment, - loaded: new Date().toISOString(), - }); - - state.canvassAssignmentList.items.push(item); - }, - canvassAssignmentLoad: (state, action: PayloadAction) => { - const canvassAssId = action.payload; - const item = state.canvassAssignmentList.items.find( - (item) => item.id == canvassAssId - ); - - if (item) { - item.isLoading = true; - } else { - state.canvassAssignmentList.items = - state.canvassAssignmentList.items.concat([ - remoteItem(canvassAssId, { isLoading: true }), - ]); - } - }, - canvassAssignmentLoaded: ( - state, - action: PayloadAction - ) => { - const canvassAssignment = action.payload; - const item = state.canvassAssignmentList.items.find( - (item) => item.id == canvassAssignment.id - ); - - if (!item) { - throw new Error('Finished loading item that never started loading'); - } - - item.data = canvassAssignment; - item.isLoading = false; - item.loaded = new Date().toISOString(); - }, - canvassAssignmentUpdated: ( - state, - action: PayloadAction - ) => { - const assignment = action.payload; - const item = findOrAddItem(state.canvassAssignmentList, assignment.id); - - item.data = assignment; - item.loaded = new Date().toISOString(); - }, - canvassAssignmentsLoad: (state) => { - state.canvassAssignmentList.isLoading = true; - }, - canvassAssignmentsLoaded: ( - state, - action: PayloadAction - ) => { - state.canvassAssignmentList = remoteList(action.payload); - state.canvassAssignmentList.loaded = new Date().toISOString(); - }, - canvassSessionCreated: ( - state, - action: PayloadAction - ) => { - const session = action.payload; - if (!state.sessionsByAssignmentId[session.assignment.id]) { - state.sessionsByAssignmentId[session.assignment.id] = remoteList(); - } - const item = remoteItem(session.assignment.id, { - data: { ...session, id: session.assignee.id }, - loaded: new Date().toISOString(), - }); - - state.sessionsByAssignmentId[session.assignment.id].items.push(item); - }, - canvassSessionsLoad: (state, action: PayloadAction) => { - const assignmentId = action.payload; - - if (!state.sessionsByAssignmentId[assignmentId]) { - state.sessionsByAssignmentId[assignmentId] = remoteList(); - } - - state.sessionsByAssignmentId[assignmentId].isLoading = true; - }, - canvassSessionsLoaded: ( - state, - action: PayloadAction<[string, ZetkinCanvassSession[]]> - ) => { - const [assignmentId, sessions] = action.payload; - - state.sessionsByAssignmentId[assignmentId] = remoteList( - sessions.map((session) => ({ ...session, id: session.assignee.id })) - ); - - state.sessionsByAssignmentId[assignmentId].loaded = - new Date().toISOString(); - }, - myAssignmentsLoad: (state) => { - state.mySessionsList.isLoading = true; - }, - myAssignmentsLoaded: ( - state, - action: PayloadAction - ) => { - const sessions = action.payload; - const timestamp = new Date().toISOString(); - - state.mySessionsList = remoteList( - sessions.map((session) => ({ - ...session, - id: `${session.assignment.id} ${session.assignee.id}`, - })) - ); - state.mySessionsList.loaded = timestamp; - state.mySessionsList.items.forEach((item) => (item.loaded = timestamp)); - }, - placeCreated: (state, action: PayloadAction) => { - const place = action.payload; - const item = remoteItem(place.id, { - data: place, - loaded: new Date().toISOString(), - }); - - state.placeList.items.push(item); - }, - placeUpdated: (state, action: PayloadAction) => { - const place = action.payload; - const item = findOrAddItem(state.placeList, place.id); - - item.data = place; - item.loaded = new Date().toISOString(); - }, - placesLoad: (state) => { - state.placeList.isLoading = true; - }, - placesLoaded: (state, action: PayloadAction) => { - const timestamp = new Date().toISOString(); - const places = action.payload; - state.placeList = remoteList(places); - state.placeList.loaded = timestamp; - state.placeList.items.forEach((item) => (item.loaded = timestamp)); - }, - statsLoad: (state, action: PayloadAction) => { - const canvassAssId = action.payload; - const statsItem = state.statsByCanvassAssId[canvassAssId]; - - state.statsByCanvassAssId[canvassAssId] = remoteItem(canvassAssId, { - data: statsItem?.data || null, - isLoading: true, - }); - }, - statsLoaded: ( - state, - action: PayloadAction<[string, ZetkinCanvassAssignmentStats]> - ) => { - const [canvassAssId, stats] = action.payload; - - state.statsByCanvassAssId[canvassAssId] = remoteItem(canvassAssId, { - data: { id: canvassAssId, ...stats }, - isLoading: false, - isStale: false, - loaded: new Date().toISOString(), - }); - }, tagAssigned: (state, action: PayloadAction<[string, ZetkinTag]>) => { const [areaId, tag] = action.payload; state.tagsByAreaId[areaId] ||= remoteList(); @@ -396,28 +127,6 @@ export const { areasLoad, areasLoaded, areaUpdated, - assigneeAdd, - assigneeAdded, - assigneeUpdated, - assigneesLoad, - assigneesLoaded, - myAssignmentsLoad, - myAssignmentsLoaded, - canvassAssignmentCreated, - canvassAssignmentLoad, - canvassAssignmentLoaded, - canvassAssignmentUpdated, - canvassAssignmentsLoad, - canvassAssignmentsLoaded, - canvassSessionCreated, - canvassSessionsLoad, - canvassSessionsLoaded, - placeCreated, - placesLoad, - placesLoaded, - placeUpdated, - statsLoad, - statsLoaded, tagAssigned, tagUnassigned, tagsLoad, diff --git a/src/features/areas/types.ts b/src/features/areas/types.ts index bb141cf15..716281563 100644 --- a/src/features/areas/types.ts +++ b/src/features/areas/types.ts @@ -1,40 +1,7 @@ -import { ZetkinPerson, ZetkinTag } from 'utils/types/zetkin'; - -export type ZetkinCanvassAssignmentStats = { - metrics: { - metric: ZetkinCanvassAssignment['metrics'][0]; - values: number[]; - }[]; - num_areas: number; - num_households: number; - num_places: number; - num_successful_visited_households: number; - num_visited_areas: number; - num_visited_households: number; - num_visited_households_outside_areas: number; - num_visited_places: number; - num_visited_places_outside_areas: number; -}; +import { ZetkinTag } from 'utils/types/zetkin'; export type PointData = [number, number]; -export type Visit = { - canvassAssId: string | null; - id: string; - noteToOfficial: string | null; - responses: { - metricId: string; - response: string; - }[]; - timestamp: string; -}; - -export type Household = { - id: string; - title: string; - visits: Visit[]; -}; - export type ZetkinArea = { description: string | null; id: string; @@ -46,75 +13,4 @@ export type ZetkinArea = { title: string | null; }; -export type ZetkinPlace = { - description: string | null; - households: Household[]; - id: string; - orgId: number; - position: { lat: number; lng: number }; - title: string | null; - type: 'address' | 'misc'; -}; - -export type ZetkinMetric = { - definesDone: boolean; - description: string; - id: string; - kind: 'boolean' | 'scale5'; - question: string; -}; - -export type ZetkinCanvassAssignment = { - campaign: { - id: number; - }; - id: string; - metrics: ZetkinMetric[]; - organization: { - id: number; - }; - title: string | null; -}; - -export type ZetkinCanvassSession = { - area: ZetkinArea; - assignee: ZetkinPerson; - assignment: { - id: string; - title: string | null; - }; -}; - -export type ZetkinCanvassSessionPostBody = { - areaId: string; - personId: number; -}; - export type ZetkinAreaPostBody = Partial>; -export type ZetkinPlacePostBody = Partial< - Omit ->; - -export type ZetkinPlacePatchBody = Partial< - Omit -> & { - households?: Partial> & - { visits?: Partial>[] }[]; -}; -export type ZetkinCanvassAssignmentPostBody = Partial< - Omit -> & { - campaign_id: number; - metrics: Omit[]; -}; -export type ZetkinCanvassAssignmentPatchbody = Partial< - Omit ->; - -export type ZetkinCanvassAssignee = { - canvassAssId: string; - id: number; -}; -export type ZetkinCanvassAssigneePatchBody = Partial; - -export type HouseholdPatchBody = Partial>; diff --git a/src/features/campaigns/components/ActivityList/items/CanvassAssignmentListItem.tsx b/src/features/campaigns/components/ActivityList/items/CanvassAssignmentListItem.tsx index 812a42c92..40e8630f7 100644 --- a/src/features/campaigns/components/ActivityList/items/CanvassAssignmentListItem.tsx +++ b/src/features/campaigns/components/ActivityList/items/CanvassAssignmentListItem.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { Map } from '@mui/icons-material'; import ActivityListItem, { STATUS_COLORS } from './ActivityListItem'; -import useCanvassAssignment from 'features/areas/hooks/useCanvassAssignment'; +import useCanvassAssignment from 'features/canvassAssignments/hooks/useCanvassAssignment'; type Props = { caId: string; diff --git a/src/features/campaigns/components/CampaignActionButtons.tsx b/src/features/campaigns/components/CampaignActionButtons.tsx index 586ea785f..27b444108 100644 --- a/src/features/campaigns/components/CampaignActionButtons.tsx +++ b/src/features/campaigns/components/CampaignActionButtons.tsx @@ -29,7 +29,7 @@ import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; import ZUIDialog from 'zui/ZUIDialog'; import ZUIEllipsisMenu from 'zui/ZUIEllipsisMenu'; import { Msg, useMessages } from 'core/i18n'; -import useCreateCanvassAssignment from 'features/areas/hooks/useCreateCanvassAssignment'; +import useCreateCanvassAssignment from 'features/canvassAssignments/hooks/useCreateCanvassAssignment'; import useFeature from 'utils/featureFlags/useFeature'; import { AREAS } from 'utils/featureFlags'; diff --git a/src/features/campaigns/hooks/useActivityArchive.ts b/src/features/campaigns/hooks/useActivityArchive.ts index 2c9dcaedb..2650cb986 100644 --- a/src/features/campaigns/hooks/useActivityArchive.ts +++ b/src/features/campaigns/hooks/useActivityArchive.ts @@ -1,4 +1,4 @@ -import useCanvassAssignmentActivities from 'features/areas/hooks/useCanvassAssignmentActivities'; +import useCanvassAssignmentActivities from 'features/canvassAssignments/hooks/useCanvassAssignmentActivities'; import { CampaignActivity } from '../types'; import useCallAssignmentActivities from './useCallAssignmentActivities'; import useEmailActivities from './useEmailActivities'; diff --git a/src/features/campaigns/hooks/useActivityList.ts b/src/features/campaigns/hooks/useActivityList.ts index 6c5c2a63f..cdc92acc6 100644 --- a/src/features/campaigns/hooks/useActivityList.ts +++ b/src/features/campaigns/hooks/useActivityList.ts @@ -1,4 +1,4 @@ -import useCanvassAssignmentActivities from 'features/areas/hooks/useCanvassAssignmentActivities'; +import useCanvassAssignmentActivities from 'features/canvassAssignments/hooks/useCanvassAssignmentActivities'; import { CampaignActivity } from '../types'; import useCallAssignmentActivities from './useCallAssignmentActivities'; import useEmailActivities from './useEmailActivities'; diff --git a/src/features/campaigns/hooks/useActivityOverview.ts b/src/features/campaigns/hooks/useActivityOverview.ts index fb84edbbf..9ead95210 100644 --- a/src/features/campaigns/hooks/useActivityOverview.ts +++ b/src/features/campaigns/hooks/useActivityOverview.ts @@ -1,6 +1,6 @@ import { isSameDate } from 'utils/dateUtils'; import useCallAssignmentActivities from './useCallAssignmentActivities'; -import useCanvassAssignmentActivities from 'features/areas/hooks/useCanvassAssignmentActivities'; +import useCanvassAssignmentActivities from 'features/canvassAssignments/hooks/useCanvassAssignmentActivities'; import useEmailActivities from './useEmailActivities'; import useEventsFromDateRange from 'features/events/hooks/useEventsFromDateRange'; import useSurveyActivities from './useSurveyActivities'; diff --git a/src/features/campaigns/types.ts b/src/features/campaigns/types.ts index 25ef3444a..7ed1ca2eb 100644 --- a/src/features/campaigns/types.ts +++ b/src/features/campaigns/types.ts @@ -1,4 +1,4 @@ -import { ZetkinCanvassAssignment } from 'features/areas/types'; +import { ZetkinCanvassAssignment } from 'features/canvassAssignments/types'; import { ZetkinCallAssignment, ZetkinEmail, diff --git a/src/features/areas/components/AssignmentMetricsChart.tsx b/src/features/canvassAssignments/components/AssignmentMetricsChart.tsx similarity index 100% rename from src/features/areas/components/AssignmentMetricsChart.tsx rename to src/features/canvassAssignments/components/AssignmentMetricsChart.tsx diff --git a/src/features/areas/components/CreatePlaceCard.tsx b/src/features/canvassAssignments/components/CreatePlaceCard.tsx similarity index 50% rename from src/features/areas/components/CreatePlaceCard.tsx rename to src/features/canvassAssignments/components/CreatePlaceCard.tsx index 1089be055..d08fe7ad5 100644 --- a/src/features/areas/components/CreatePlaceCard.tsx +++ b/src/features/canvassAssignments/components/CreatePlaceCard.tsx @@ -5,18 +5,11 @@ import { CardActions, CardContent, FormControl, - InputLabel, - MenuItem, - Select, - SelectChangeEvent, TextField, } from '@mui/material'; import { FC, useState } from 'react'; import { makeStyles } from '@mui/styles'; -import messageIds from '../l10n/messageIds'; -import { Msg, useMessages } from 'core/i18n'; - export const useStyles = makeStyles(() => ({ card: { bottom: 15, @@ -32,7 +25,7 @@ export const useStyles = makeStyles(() => ({ type AddPlaceDialogProps = { onClose: () => void; - onCreate: (title: string, type: string) => void; + onCreate: (title: string) => void; }; export const CreatePlaceCard: FC = ({ @@ -40,62 +33,35 @@ export const CreatePlaceCard: FC = ({ onCreate, }) => { const classes = useStyles(); - const messages = useMessages(messageIds); const [title, setTitle] = useState(''); - const [type, setType] = useState(''); - - const handleChange = (event: SelectChangeEvent) => { - setType(event.target.value); - }; return ( - - - - setTitle(ev.target.value)} - placeholder={ - type === 'address' - ? messages.placeCard.placeholderAddress() - : messages.placeCard.placeholderTitle() - } + placeholder="title" sx={{ paddingTop: 1 }} /> diff --git a/src/features/areas/components/Metrics/MetricCard.tsx b/src/features/canvassAssignments/components/MetricCard.tsx similarity index 93% rename from src/features/areas/components/Metrics/MetricCard.tsx rename to src/features/canvassAssignments/components/MetricCard.tsx index 64a0d8d08..b504e7685 100644 --- a/src/features/areas/components/Metrics/MetricCard.tsx +++ b/src/features/canvassAssignments/components/MetricCard.tsx @@ -1,5 +1,5 @@ import { Close } from '@mui/icons-material'; -import React, { useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { Card, CardContent, @@ -11,22 +11,24 @@ import { IconButton, } from '@mui/material'; -import { ZetkinMetric } from 'features/areas/types'; +import { ZetkinMetric } from '../types'; -const MetricCard = ({ - hasDefinedDone, - isOnlyQuestion, - metric, - onClose, - onDelete, - onSave, -}: { +type MetricCardProps = { hasDefinedDone: boolean; isOnlyQuestion: boolean; metric: ZetkinMetric; onClose: () => void; onDelete: (target: EventTarget & HTMLButtonElement) => void; onSave: (metric: ZetkinMetric) => void; +}; + +const MetricCard: FC = ({ + hasDefinedDone, + isOnlyQuestion, + metric, + onClose, + onDelete, + onSave, }) => { const [question, setQuestion] = useState(metric.question || ''); const [description, setDescription] = useState( @@ -36,7 +38,7 @@ const MetricCard = ({ metric.definesDone || false ); - const isEditing = !!metric?.id; + const isEditing = !!metric?._id; useEffect(() => { setQuestion(metric.question || ''); @@ -104,9 +106,9 @@ const MetricCard = ({ @@ -77,9 +73,7 @@ const MyCanvassAssignmentsPage: FC = () => { return ( - - - + Canvassing {Object.values(sessionsByAssignmentId).map((sessions) => { return ( void; + onTitleChange: (newTitle: string) => void; + title: string; +}; + +const EditPlace: FC = ({ + description, + onDescriptionChange, + onTitleChange, + title, +}) => { + return ( + + onTitleChange(ev.target.value)} + /> + onDescriptionChange(ev.target.value)} + rows={5} + /> + + ); +}; + +export default EditPlace; diff --git a/src/features/areas/components/PlaceDialog/Household.tsx b/src/features/canvassAssignments/components/PlaceDialog/Household.tsx similarity index 97% rename from src/features/areas/components/PlaceDialog/Household.tsx rename to src/features/canvassAssignments/components/PlaceDialog/Household.tsx index 540512d17..88fa9aff9 100644 --- a/src/features/areas/components/PlaceDialog/Household.tsx +++ b/src/features/canvassAssignments/components/PlaceDialog/Household.tsx @@ -1,7 +1,7 @@ import { Box, Button, TextField } from '@mui/material'; import { FC } from 'react'; -import { HouseholdPatchBody } from 'features/areas/types'; +import { HouseholdPatchBody } from 'features/canvassAssignments/types'; type HouseholdProps = { editingHouseholdTitle: boolean; diff --git a/src/features/areas/components/PlaceDialog/Place.tsx b/src/features/canvassAssignments/components/PlaceDialog/Place.tsx similarity index 74% rename from src/features/areas/components/PlaceDialog/Place.tsx rename to src/features/canvassAssignments/components/PlaceDialog/Place.tsx index 93b73dcbc..8fcc054b1 100644 --- a/src/features/areas/components/PlaceDialog/Place.tsx +++ b/src/features/canvassAssignments/components/PlaceDialog/Place.tsx @@ -2,10 +2,8 @@ import { Check } from '@mui/icons-material'; import { Box, Divider, Typography } from '@mui/material'; import { FC } from 'react'; -import { Msg } from 'core/i18n'; -import { ZetkinPlace } from 'features/areas/types'; -import messageIds from 'features/areas/l10n/messageIds'; -import { isWithinLast24Hours } from 'features/areas/utils/isWithinLast24Hours'; +import { isWithinLast24Hours } from 'features/canvassAssignments/utils/isWithinLast24Hours'; +import { ZetkinPlace } from 'features/canvassAssignments/types'; type PlaceProps = { onSelectHousehold: (householdId: string) => void; @@ -22,12 +20,10 @@ const Place: FC = ({ onSelectHousehold, place }) => { justifyContent="space-between" paddingTop={1} > - - - + Description - {place.description || } + {place.description || 'Empty description'} = ({ onSelectHousehold, place }) => { overflow="hidden" > - + {`${place.households.length} household/s`} @@ -75,9 +68,7 @@ const Place: FC = ({ onSelectHousehold, place }) => { width="100%" > - {household.title || ( - - )} + {household.title || 'Untitled household'} {visitedRecently ? : ''} diff --git a/src/features/areas/components/PlaceDialog/VisitWizard.tsx b/src/features/canvassAssignments/components/PlaceDialog/VisitWizard.tsx similarity index 97% rename from src/features/areas/components/PlaceDialog/VisitWizard.tsx rename to src/features/canvassAssignments/components/PlaceDialog/VisitWizard.tsx index 6cd8d1fba..13471fb31 100644 --- a/src/features/areas/components/PlaceDialog/VisitWizard.tsx +++ b/src/features/canvassAssignments/components/PlaceDialog/VisitWizard.tsx @@ -12,7 +12,7 @@ import { Visit, ZetkinCanvassAssignment, ZetkinMetric, -} from 'features/areas/types'; +} from 'features/canvassAssignments/types'; const Question: FC<{ metric: ZetkinMetric; @@ -124,7 +124,7 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { return ( <> { setStep(index); setResponses(responses.slice(0, index + 1)); @@ -175,7 +175,7 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { return ( { if (newValue == null) { @@ -199,7 +199,7 @@ const VisitWizard: FC = ({ metrics, onLogVisit }) => { setResponses([ ...responses, { - metricId: metric.id, + metricId: metric._id, response: newValue.toString(), }, ]); diff --git a/src/features/areas/components/PlaceDialog/index.tsx b/src/features/canvassAssignments/components/PlaceDialog/index.tsx similarity index 87% rename from src/features/areas/components/PlaceDialog/index.tsx rename to src/features/canvassAssignments/components/PlaceDialog/index.tsx index 0045f975a..4db5fc320 100644 --- a/src/features/areas/components/PlaceDialog/index.tsx +++ b/src/features/canvassAssignments/components/PlaceDialog/index.tsx @@ -18,20 +18,16 @@ import { Typography, } from '@mui/material'; -import messageIds from '../../l10n/messageIds'; -import usePlaceMutations from '../../hooks/usePlaceMutations'; -import { HouseholdPatchBody, ZetkinPlace } from '../../types'; -import { Msg, useMessages } from 'core/i18n'; import VisitWizard from './VisitWizard'; import EditPlace from './EditPlace'; import Place from './Place'; import Household from './Household'; -import { isWithinLast24Hours } from 'features/areas/utils/isWithinLast24Hours'; -import { PlaceDialogStep } from '../PublicAreaMap'; -import useCanvassAssignment from 'features/areas/hooks/useCanvassAssignment'; +import { isWithinLast24Hours } from 'features/canvassAssignments/utils/isWithinLast24Hours'; import ZUIFuture from 'zui/ZUIFuture'; - -export type PlaceType = 'address' | 'misc'; +import { PlaceDialogStep } from '../PublicAreaMap'; +import { ZetkinPlace } from 'features/canvassAssignments/types'; +import usePlaceMutations from 'features/canvassAssignments/hooks/usePlaceMutations'; +import useCanvassAssignment from 'features/canvassAssignments/hooks/useCanvassAssignment'; type PlaceDialogProps = { canvassAssId: string; @@ -60,7 +56,6 @@ const PlaceDialog: FC = ({ orgId, place, }) => { - const messages = useMessages(messageIds); const { addVisit, addHousehold, updateHousehold, updatePlace } = usePlaceMutations(orgId, place.id); const assignmentFuture = useCanvassAssignment(orgId, canvassAssId); @@ -72,7 +67,6 @@ const PlaceDialog: FC = ({ place.description ?? '' ); const [title, setTitle] = useState(place.title ?? ''); - const [type, setType] = useState(place.type); const [editingHouseholdTitle, setEditingHouseholdTitle] = useState(false); const [householdTitle, setHousholdTitle] = useState(''); const [anchorEl, setAnchorEl] = useState(null); @@ -97,7 +91,6 @@ const PlaceDialog: FC = ({ const nothingHasBeenEdited = dialogStep == 'edit' && title == place.title && - type == place.type && (description == place.description || (!description && !place.description)); const saveButtonDisabled = nothingHasBeenEdited; @@ -120,7 +113,7 @@ const PlaceDialog: FC = ({ {dialogStep == 'place' && ( <> - {place?.title || } + {place?.title || 'Untitled place'} @@ -152,12 +145,7 @@ const PlaceDialog: FC = ({ {dialogStep == 'edit' && ( <> - + {`Edit ${place.title || 'Untitled place'}`} @@ -175,9 +163,7 @@ const PlaceDialog: FC = ({ - {selectedHousehold.title || ( - - )} + {selectedHousehold.title || 'Untitled household'} @@ -203,9 +189,7 @@ const PlaceDialog: FC = ({ - {selectedHousehold.title || ( - - )} + {selectedHousehold.title || 'Untitled household'} )} @@ -247,11 +231,7 @@ const PlaceDialog: FC = ({ - {household.title || ( - - )} + {household.title || 'Untitled household'} {visitedRecently ? : ''} @@ -267,9 +247,7 @@ const PlaceDialog: FC = ({ setDescription(newDescription) } onTitleChange={(newTitle) => setTitle(newTitle)} - onTypeChange={(newType) => setType(newType)} title={title} - type={type} /> )} {selectedHousehold && dialogStep == 'household' && ( @@ -282,7 +260,7 @@ const PlaceDialog: FC = ({ onHouseholdTitleChange={(newTitle) => setHousholdTitle(newTitle) } - onHouseholdUpdate={(data: HouseholdPatchBody) => + onHouseholdUpdate={(data) => updateHousehold(selectedHousehold.id, data) } onWizardStart={() => { @@ -320,7 +298,7 @@ const PlaceDialog: FC = ({ }} variant="contained" > - + Add household )} {dialogStep == 'place' && place.households.length == 1 && ( @@ -332,7 +310,7 @@ const PlaceDialog: FC = ({ onWizard(); }} > - + Log visit )} = ({ onAddAssigneeToArea, sessions, }) => { - const messages = useMessages(messageIds); const [filterAssigned, setFilterAssigned] = useState(false); const [filterUnassigned, setFilterUnassigned] = useState(false); @@ -48,8 +46,8 @@ const PlanMap: FC = ({ } return areas.filter((area) => { - const areaTitle = area.title || messages.empty.title(); - const areaDesc = area.description || messages.empty.description(); + const areaTitle = area.title || 'Untitled area'; + const areaDesc = area.description || 'Empty description'; return ( areaTitle.toLowerCase().includes(inputValue) || @@ -91,12 +89,12 @@ const PlanMap: FC = ({ setFilterAssigned(!filterAssigned)} /> setFilterUnassigned(!filterUnassigned)} /> = ({ /> )} renderOption={(props, area) => ( - - {area.title || messages.empty.title()} - + {area.title || 'Untitled area'} )} value={null} /> diff --git a/src/features/areas/components/PlanMapRenderer.tsx b/src/features/canvassAssignments/components/PlanMapRenderer.tsx similarity index 97% rename from src/features/areas/components/PlanMapRenderer.tsx rename to src/features/canvassAssignments/components/PlanMapRenderer.tsx index cdd17b6d4..bae40e46f 100644 --- a/src/features/areas/components/PlanMapRenderer.tsx +++ b/src/features/canvassAssignments/components/PlanMapRenderer.tsx @@ -10,10 +10,11 @@ import { FeatureGroup as FeatureGroupType, latLngBounds } from 'leaflet'; import { useTheme } from '@mui/styles'; import { Box } from '@mui/material'; -import { ZetkinArea, ZetkinCanvassSession } from '../types'; import { DivIconMarker } from 'features/events/components/LocationModal/DivIconMarker'; import ZUIAvatar from 'zui/ZUIAvatar'; -import objToLatLng from '../utils/objToLatLng'; +import { ZetkinCanvassSession } from '../types'; +import { ZetkinArea } from 'features/areas/types'; +import objToLatLng from 'features/areas/utils/objToLatLng'; type PlanMapRendererProps = { areas: ZetkinArea[]; diff --git a/src/features/areas/components/PublicAreaMap.tsx b/src/features/canvassAssignments/components/PublicAreaMap.tsx similarity index 96% rename from src/features/areas/components/PublicAreaMap.tsx rename to src/features/canvassAssignments/components/PublicAreaMap.tsx index 817515052..291cc9bb6 100644 --- a/src/features/areas/components/PublicAreaMap.tsx +++ b/src/features/canvassAssignments/components/PublicAreaMap.tsx @@ -17,16 +17,14 @@ import { TileLayer, } from 'react-leaflet'; -import { ZetkinArea } from '../types'; -import { Msg } from 'core/i18n'; -import messageIds from '../l10n/messageIds'; -import { CreatePlaceCard } from './CreatePlaceCard'; +import { ZetkinArea } from '../../areas/types'; import { DivIconMarker } from 'features/events/components/LocationModal/DivIconMarker'; import useCreatePlace from '../hooks/useCreatePlace'; import usePlaces from '../hooks/usePlaces'; import getCrosshairPositionOnMap from '../utils/getCrosshairPositionOnMap'; import MarkerIcon from '../utils/markerIcon'; import PlaceDialog from './PlaceDialog'; +import { CreatePlaceCard } from './CreatePlaceCard'; const useStyles = makeStyles((theme) => ({ '@keyframes ghostMarkerBounce': { @@ -259,7 +257,7 @@ const PublicAreaMap: FC = ({ canvassAssId, area }) => { {showViewPlaceButton && ( - {selectedPlace.title || } + {selectedPlace.title || 'Untitled place'} )} {!selectedPlace && !isCreating && ( )} @@ -341,7 +339,7 @@ const PublicAreaMap: FC = ({ canvassAssId, area }) => { onClose={() => { setIsCreating(false); }} - onCreate={(title, type) => { + onCreate={(title) => { const crosshair = crosshairRef.current; if (crosshair && map) { @@ -355,7 +353,6 @@ const PublicAreaMap: FC = ({ canvassAssId, area }) => { createPlace({ position: point, title, - type: type === 'address' ? 'address' : 'misc', }); } } diff --git a/src/features/areas/components/AreaPage.tsx b/src/features/canvassAssignments/components/PublicAreaPage.tsx similarity index 81% rename from src/features/areas/components/AreaPage.tsx rename to src/features/canvassAssignments/components/PublicAreaPage.tsx index cde2ff73c..2d0394040 100644 --- a/src/features/areas/components/AreaPage.tsx +++ b/src/features/canvassAssignments/components/PublicAreaPage.tsx @@ -5,21 +5,19 @@ import { FC } from 'react'; import dynamic from 'next/dynamic'; import { useSearchParams } from 'next/navigation'; -import useArea from '../hooks/useArea'; +import useArea from '../../areas/hooks/useArea'; import useOrganization from 'features/organizations/hooks/useOrganization'; import ZUIFutures from 'zui/ZUIFutures'; -import { Msg } from 'core/i18n'; -import messageIds from '../l10n/messageIds'; import useServerSide from 'core/useServerSide'; const PublicAreaMap = dynamic(() => import('./PublicAreaMap'), { ssr: false }); -type AreaPageProps = { +type PublicAreaPageProps = { areaId: string; orgId: number; }; -const AreaPage: FC = ({ areaId, orgId }) => { +const PublicAreaPage: FC = ({ areaId, orgId }) => { const orgFuture = useOrganization(orgId); const areaFuture = useArea(orgId, areaId); const searchParams = useSearchParams(); @@ -48,9 +46,9 @@ const AreaPage: FC = ({ areaId, orgId }) => { {org.title} - {area.title ?? } + {area.title ?? 'Untitled canvassassignment'} - {area.description ?? } + {area.description ?? 'Untitled area'} @@ -63,4 +61,4 @@ const AreaPage: FC = ({ areaId, orgId }) => { ); }; -export default AreaPage; +export default PublicAreaPage; diff --git a/src/features/areas/hooks/useAddAssignee.ts b/src/features/canvassAssignments/hooks/useAddAssignee.ts similarity index 100% rename from src/features/areas/hooks/useAddAssignee.ts rename to src/features/canvassAssignments/hooks/useAddAssignee.ts diff --git a/src/features/areas/hooks/useAssigneeMutations.ts b/src/features/canvassAssignments/hooks/useAssigneeMutations.ts similarity index 100% rename from src/features/areas/hooks/useAssigneeMutations.ts rename to src/features/canvassAssignments/hooks/useAssigneeMutations.ts diff --git a/src/features/areas/hooks/useAssignees.ts b/src/features/canvassAssignments/hooks/useAssignees.ts similarity index 89% rename from src/features/areas/hooks/useAssignees.ts rename to src/features/canvassAssignments/hooks/useAssignees.ts index e0f707fbd..490cbf910 100644 --- a/src/features/areas/hooks/useAssignees.ts +++ b/src/features/canvassAssignments/hooks/useAssignees.ts @@ -7,7 +7,8 @@ export default function useAssignees(orgId: number, canvassAssId: string) { const apiClient = useApiClient(); const dispatch = useAppDispatch(); const assigneeList = useAppSelector( - (state) => state.areas.assigneesByCanvassAssignmentId[canvassAssId] + (state) => + state.canvassAssignments.assigneesByCanvassAssignmentId[canvassAssId] ); return loadListIfNecessary(assigneeList, dispatch, { diff --git a/src/features/areas/hooks/useCanvassAssignment.ts b/src/features/canvassAssignments/hooks/useCanvassAssignment.ts similarity index 93% rename from src/features/areas/hooks/useCanvassAssignment.ts rename to src/features/canvassAssignments/hooks/useCanvassAssignment.ts index 53b194fd9..ff8052193 100644 --- a/src/features/areas/hooks/useCanvassAssignment.ts +++ b/src/features/canvassAssignments/hooks/useCanvassAssignment.ts @@ -10,7 +10,7 @@ export default function useCanvassAssignment( const apiClient = useApiClient(); const dispatch = useAppDispatch(); const canvassAssignmenList = useAppSelector( - (state) => state.areas.canvassAssignmentList.items + (state) => state.canvassAssignments.canvassAssignmentList.items ); const canvassAssignmentItem = canvassAssignmenList.find( (item) => item.id == canvassAssId diff --git a/src/features/areas/hooks/useCanvassAssignmentActivities.ts b/src/features/canvassAssignments/hooks/useCanvassAssignmentActivities.ts similarity index 94% rename from src/features/areas/hooks/useCanvassAssignmentActivities.ts rename to src/features/canvassAssignments/hooks/useCanvassAssignmentActivities.ts index 5b99900cc..dfe3db4c3 100644 --- a/src/features/areas/hooks/useCanvassAssignmentActivities.ts +++ b/src/features/canvassAssignments/hooks/useCanvassAssignmentActivities.ts @@ -18,7 +18,9 @@ export default function useCanvassAssignmentActivities( ): IFuture { const apiClient = useApiClient(); const dispatch = useAppDispatch(); - const list = useAppSelector((state) => state.areas.canvassAssignmentList); + const list = useAppSelector( + (state) => state.canvassAssignments.canvassAssignmentList + ); const hasCanvassing = useFeature(AREAS); if (!hasCanvassing) { diff --git a/src/features/areas/hooks/useCanvassAssignmentMutations.ts b/src/features/canvassAssignments/hooks/useCanvassAssignmentMutations.ts similarity index 100% rename from src/features/areas/hooks/useCanvassAssignmentMutations.ts rename to src/features/canvassAssignments/hooks/useCanvassAssignmentMutations.ts diff --git a/src/features/areas/hooks/useCanvassAssignmentStats.ts b/src/features/canvassAssignments/hooks/useCanvassAssignmentStats.ts similarity index 91% rename from src/features/areas/hooks/useCanvassAssignmentStats.ts rename to src/features/canvassAssignments/hooks/useCanvassAssignmentStats.ts index dd00259df..e32634341 100644 --- a/src/features/areas/hooks/useCanvassAssignmentStats.ts +++ b/src/features/canvassAssignments/hooks/useCanvassAssignmentStats.ts @@ -10,7 +10,7 @@ export default function useCanvassAssignmentStats( const apiClient = useApiClient(); const dispatch = useAppDispatch(); const statsItem = useAppSelector( - (state) => state.areas.statsByCanvassAssId[canvassAssId] + (state) => state.canvassAssignments.statsByCanvassAssId[canvassAssId] ); return loadItemIfNecessary(statsItem, dispatch, { diff --git a/src/features/areas/hooks/useCanvassSessions.ts b/src/features/canvassAssignments/hooks/useCanvassSessions.ts similarity index 91% rename from src/features/areas/hooks/useCanvassSessions.ts rename to src/features/canvassAssignments/hooks/useCanvassSessions.ts index 0bff66bda..d2b944a24 100644 --- a/src/features/areas/hooks/useCanvassSessions.ts +++ b/src/features/canvassAssignments/hooks/useCanvassSessions.ts @@ -10,7 +10,7 @@ export default function useCanvassSessions( const apiClient = useApiClient(); const dispatch = useAppDispatch(); const sessions = useAppSelector( - (state) => state.areas.sessionsByAssignmentId[canvassAssId] + (state) => state.canvassAssignments.sessionsByAssignmentId[canvassAssId] ); return loadListIfNecessary(sessions, dispatch, { diff --git a/src/features/areas/hooks/useCreateCanvassAssignment.ts b/src/features/canvassAssignments/hooks/useCreateCanvassAssignment.ts similarity index 100% rename from src/features/areas/hooks/useCreateCanvassAssignment.ts rename to src/features/canvassAssignments/hooks/useCreateCanvassAssignment.ts diff --git a/src/features/areas/hooks/useCreateCanvassSession.ts b/src/features/canvassAssignments/hooks/useCreateCanvassSession.ts similarity index 100% rename from src/features/areas/hooks/useCreateCanvassSession.ts rename to src/features/canvassAssignments/hooks/useCreateCanvassSession.ts diff --git a/src/features/areas/hooks/useCreatePlace.ts b/src/features/canvassAssignments/hooks/useCreatePlace.ts similarity index 100% rename from src/features/areas/hooks/useCreatePlace.ts rename to src/features/canvassAssignments/hooks/useCreatePlace.ts diff --git a/src/features/areas/hooks/useMyCanvassSessions.ts b/src/features/canvassAssignments/hooks/useMyCanvassSessions.ts similarity index 87% rename from src/features/areas/hooks/useMyCanvassSessions.ts rename to src/features/canvassAssignments/hooks/useMyCanvassSessions.ts index 5a4cd0375..aa9776560 100644 --- a/src/features/areas/hooks/useMyCanvassSessions.ts +++ b/src/features/canvassAssignments/hooks/useMyCanvassSessions.ts @@ -6,7 +6,9 @@ import { ZetkinCanvassSession } from '../types'; export default function useMyCanvassSessions() { const apiClient = useApiClient(); const dispatch = useAppDispatch(); - const mySessions = useAppSelector((state) => state.areas.mySessionsList); + const mySessions = useAppSelector( + (state) => state.canvassAssignments.mySessionsList + ); return loadListIfNecessary(mySessions, dispatch, { actionOnLoad: () => myAssignmentsLoad(), diff --git a/src/features/areas/hooks/usePlaceMutations.ts b/src/features/canvassAssignments/hooks/usePlaceMutations.ts similarity index 100% rename from src/features/areas/hooks/usePlaceMutations.ts rename to src/features/canvassAssignments/hooks/usePlaceMutations.ts diff --git a/src/features/areas/hooks/usePlaces.ts b/src/features/canvassAssignments/hooks/usePlaces.ts similarity index 86% rename from src/features/areas/hooks/usePlaces.ts rename to src/features/canvassAssignments/hooks/usePlaces.ts index dc6ea2b78..c8fc28ace 100644 --- a/src/features/areas/hooks/usePlaces.ts +++ b/src/features/canvassAssignments/hooks/usePlaces.ts @@ -6,7 +6,9 @@ import { placesLoad, placesLoaded } from '../store'; export default function usePlaces(orgId: number) { const apiClient = useApiClient(); const dispatch = useAppDispatch(); - const placeList = useAppSelector((state) => state.areas.placeList); + const placeList = useAppSelector( + (state) => state.canvassAssignments.placeList + ); return loadListIfNecessary(placeList, dispatch, { actionOnLoad: () => placesLoad(), diff --git a/src/features/areas/layouts/CanvassAssignmentLayout.tsx b/src/features/canvassAssignments/layouts/CanvassAssignmentLayout.tsx similarity index 82% rename from src/features/areas/layouts/CanvassAssignmentLayout.tsx rename to src/features/canvassAssignments/layouts/CanvassAssignmentLayout.tsx index eb0fd7459..c35e1dd53 100644 --- a/src/features/areas/layouts/CanvassAssignmentLayout.tsx +++ b/src/features/canvassAssignments/layouts/CanvassAssignmentLayout.tsx @@ -1,9 +1,7 @@ import { FC, ReactNode } from 'react'; import { useRouter } from 'next/router'; -import { useMessages } from 'core/i18n'; import TabbedLayout from 'utils/layout/TabbedLayout'; -import messageIds from '../l10n/messageIds'; import useCanvassAssignment from '../hooks/useCanvassAssignment'; import ZUIEditTextinPlace from 'zui/ZUIEditTextInPlace'; import useCanvassAssignmentMutations from '../hooks/useCanvassAssignmentMutations'; @@ -22,7 +20,6 @@ const CanvassAssignmentLayout: FC = ({ canvassAssId, }) => { const path = useRouter().pathname; - const messages = useMessages(messageIds); const canvassAssignment = useCanvassAssignment(orgId, canvassAssId).data; const updateCanvassAssignment = useCanvassAssignmentMutations( orgId, @@ -41,7 +38,7 @@ const CanvassAssignmentLayout: FC = ({ defaultTab="/" fixedHeight={isPlanTab} tabs={[ - { href: '/', label: messages.canvassAssignment.tabs.overview() }, + { href: '/', label: 'Overview' }, { href: '/plan', label: 'Plan' }, { href: '/canvassers', label: 'Canvassers' }, { href: '/editor', label: 'Editor' }, @@ -49,9 +46,7 @@ const CanvassAssignmentLayout: FC = ({ title={ updateCanvassAssignment({ title: newTitle })} - value={ - canvassAssignment.title || messages.canvassAssignment.empty.title() - } + value={canvassAssignment.title || 'Untitled canvass assignment'} /> } > diff --git a/src/features/canvassAssignments/models.ts b/src/features/canvassAssignments/models.ts new file mode 100644 index 000000000..e75d1baae --- /dev/null +++ b/src/features/canvassAssignments/models.ts @@ -0,0 +1,90 @@ +import mongoose from 'mongoose'; + +import { ZetkinCanvassAssignee, ZetkinMetric, ZetkinPlace } from './types'; + +type ZetkinCanvassAssignmentModelType = { + campId: number; + id: number; + metrics: ZetkinMetric[]; + orgId: number; + sessions: { + areaId: string; + personId: number; + }[]; + title: string | null; +}; + +type ZetkinPlaceModelType = Omit; + +const canvassAssignmentSchema = + new mongoose.Schema({ + campId: Number, + metrics: [ + { + definesDone: Boolean, + description: String, + id: String, + kind: String, + question: String, + }, + ], + orgId: { required: true, type: Number }, + sessions: [ + { + areaId: String, + personId: Number, + }, + ], + title: String, + }); + +const placeSchema = new mongoose.Schema({ + description: String, + households: [ + { + _id: false, + id: String, + title: String, + visits: [ + { + _id: false, + canvassAssId: String, + id: String, + noteToOfficial: String, + responses: [ + { + metricId: String, + response: String, + }, + ], + timestamp: String, + }, + ], + }, + ], + orgId: { required: true, type: Number }, + position: Object, + title: String, +}); + +const canvassAssigneeSchema = new mongoose.Schema({ + canvassAssId: String, + id: { required: true, type: Number }, +}); + +export const CanvassAssignmentModel: mongoose.Model = + mongoose.models.CanvassAssignment || + mongoose.model( + 'CanvassAssignment', + canvassAssignmentSchema + ); +export const PlaceModel: mongoose.Model = + mongoose.models.Place || + mongoose.model('Place', placeSchema); + +export const CanvassAssigneeModel: mongoose.Model = + mongoose.models.CanvassAssignee || + mongoose.model( + 'CanvassAssignee', + canvassAssigneeSchema + ); diff --git a/src/features/canvassAssignments/store.ts b/src/features/canvassAssignments/store.ts new file mode 100644 index 000000000..f135aa31c --- /dev/null +++ b/src/features/canvassAssignments/store.ts @@ -0,0 +1,316 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { + findOrAddItem, + RemoteItem, + remoteItem, + remoteList, + RemoteList, +} from 'utils/storeUtils'; +import { + ZetkinCanvassAssignmentStats, + ZetkinCanvassAssignee, + ZetkinCanvassAssignment, + ZetkinCanvassSession, + ZetkinPlace, +} from './types'; + +export interface CanvassAssignmentsStoreSlice { + canvassAssignmentList: RemoteList; + sessionsByAssignmentId: Record< + string, + RemoteList + >; + assigneesByCanvassAssignmentId: Record< + string, + RemoteList + >; + mySessionsList: RemoteList; + placeList: RemoteList; + statsByCanvassAssId: Record< + string, + RemoteItem + >; +} + +const initialState: CanvassAssignmentsStoreSlice = { + assigneesByCanvassAssignmentId: {}, + canvassAssignmentList: remoteList(), + mySessionsList: remoteList(), + placeList: remoteList(), + sessionsByAssignmentId: {}, + statsByCanvassAssId: {}, +}; + +const canvassAssignmentSlice = createSlice({ + initialState: initialState, + name: 'canvassAssignments', + reducers: { + assigneeAdd: (state, action: PayloadAction<[string, number]>) => { + const [canvassAssId, assigneeId] = action.payload; + + if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { + state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); + } + + state.assigneesByCanvassAssignmentId[canvassAssId].items.push( + remoteItem(assigneeId, { isLoading: true }) + ); + }, + assigneeAdded: ( + state, + action: PayloadAction<[string, ZetkinCanvassAssignee]> + ) => { + const [canvassAssId, assignee] = action.payload; + + if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { + state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); + } + + state.assigneesByCanvassAssignmentId[canvassAssId].items = + state.assigneesByCanvassAssignmentId[canvassAssId].items + .filter((item) => item.id != assignee.id) + .concat([ + remoteItem(assignee.canvassAssId, { + data: assignee, + }), + ]); + }, + assigneeUpdated: ( + state, + action: PayloadAction<[string, ZetkinCanvassAssignee]> + ) => { + const [canvassAssId, assignee] = action.payload; + + if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { + state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); + } + + state.assigneesByCanvassAssignmentId[canvassAssId].items + .filter((item) => item.id == assignee.id) + .concat([ + remoteItem(assignee.canvassAssId, { + data: assignee, + }), + ]); + }, + assigneesLoad: (state, action: PayloadAction) => { + const canvassAssId = action.payload; + + if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { + state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); + } + + state.assigneesByCanvassAssignmentId[canvassAssId].isLoading = true; + }, + assigneesLoaded: ( + state, + action: PayloadAction<[string, ZetkinCanvassAssignee[]]> + ) => { + const [canvassAssId, assignees] = action.payload; + + if (!state.assigneesByCanvassAssignmentId[canvassAssId]) { + state.assigneesByCanvassAssignmentId[canvassAssId] = remoteList(); + } + + state.assigneesByCanvassAssignmentId[canvassAssId] = + remoteList(assignees); + state.assigneesByCanvassAssignmentId[canvassAssId].loaded = + new Date().toISOString(); + }, + canvassAssignmentCreated: ( + state, + action: PayloadAction + ) => { + const canvassAssignment = action.payload; + const item = remoteItem(canvassAssignment.id, { + data: canvassAssignment, + loaded: new Date().toISOString(), + }); + + state.canvassAssignmentList.items.push(item); + }, + canvassAssignmentLoad: (state, action: PayloadAction) => { + const canvassAssId = action.payload; + const item = state.canvassAssignmentList.items.find( + (item) => item.id == canvassAssId + ); + + if (item) { + item.isLoading = true; + } else { + state.canvassAssignmentList.items = + state.canvassAssignmentList.items.concat([ + remoteItem(canvassAssId, { isLoading: true }), + ]); + } + }, + canvassAssignmentLoaded: ( + state, + action: PayloadAction + ) => { + const canvassAssignment = action.payload; + const item = state.canvassAssignmentList.items.find( + (item) => item.id == canvassAssignment.id + ); + + if (!item) { + throw new Error('Finished loading item that never started loading'); + } + + item.data = canvassAssignment; + item.isLoading = false; + item.loaded = new Date().toISOString(); + }, + canvassAssignmentUpdated: ( + state, + action: PayloadAction + ) => { + const assignment = action.payload; + const item = findOrAddItem(state.canvassAssignmentList, assignment.id); + + item.data = assignment; + item.loaded = new Date().toISOString(); + }, + canvassAssignmentsLoad: (state) => { + state.canvassAssignmentList.isLoading = true; + }, + canvassAssignmentsLoaded: ( + state, + action: PayloadAction + ) => { + state.canvassAssignmentList = remoteList(action.payload); + state.canvassAssignmentList.loaded = new Date().toISOString(); + }, + canvassSessionCreated: ( + state, + action: PayloadAction + ) => { + const session = action.payload; + if (!state.sessionsByAssignmentId[session.assignment.id]) { + state.sessionsByAssignmentId[session.assignment.id] = remoteList(); + } + const item = remoteItem(session.assignment.id, { + data: { ...session, id: session.assignee.id }, + loaded: new Date().toISOString(), + }); + + state.sessionsByAssignmentId[session.assignment.id].items.push(item); + }, + canvassSessionsLoad: (state, action: PayloadAction) => { + const assignmentId = action.payload; + + if (!state.sessionsByAssignmentId[assignmentId]) { + state.sessionsByAssignmentId[assignmentId] = remoteList(); + } + + state.sessionsByAssignmentId[assignmentId].isLoading = true; + }, + canvassSessionsLoaded: ( + state, + action: PayloadAction<[string, ZetkinCanvassSession[]]> + ) => { + const [assignmentId, sessions] = action.payload; + + state.sessionsByAssignmentId[assignmentId] = remoteList( + sessions.map((session) => ({ ...session, id: session.assignee.id })) + ); + + state.sessionsByAssignmentId[assignmentId].loaded = + new Date().toISOString(); + }, + myAssignmentsLoad: (state) => { + state.mySessionsList.isLoading = true; + }, + myAssignmentsLoaded: ( + state, + action: PayloadAction + ) => { + const sessions = action.payload; + const timestamp = new Date().toISOString(); + + state.mySessionsList = remoteList( + sessions.map((session) => ({ + ...session, + id: `${session.assignment.id} ${session.assignee.id}`, + })) + ); + state.mySessionsList.loaded = timestamp; + state.mySessionsList.items.forEach((item) => (item.loaded = timestamp)); + }, + placeCreated: (state, action: PayloadAction) => { + const place = action.payload; + const item = remoteItem(place.id, { + data: place, + loaded: new Date().toISOString(), + }); + + state.placeList.items.push(item); + }, + placeUpdated: (state, action: PayloadAction) => { + const place = action.payload; + const item = findOrAddItem(state.placeList, place.id); + + item.data = place; + item.loaded = new Date().toISOString(); + }, + placesLoad: (state) => { + state.placeList.isLoading = true; + }, + placesLoaded: (state, action: PayloadAction) => { + const timestamp = new Date().toISOString(); + const places = action.payload; + state.placeList = remoteList(places); + state.placeList.loaded = timestamp; + state.placeList.items.forEach((item) => (item.loaded = timestamp)); + }, + statsLoad: (state, action: PayloadAction) => { + const canvassAssId = action.payload; + const statsItem = state.statsByCanvassAssId[canvassAssId]; + + state.statsByCanvassAssId[canvassAssId] = remoteItem(canvassAssId, { + data: statsItem?.data || null, + isLoading: true, + }); + }, + statsLoaded: ( + state, + action: PayloadAction<[string, ZetkinCanvassAssignmentStats]> + ) => { + const [canvassAssId, stats] = action.payload; + + state.statsByCanvassAssId[canvassAssId] = remoteItem(canvassAssId, { + data: { id: canvassAssId, ...stats }, + isLoading: false, + isStale: false, + loaded: new Date().toISOString(), + }); + }, + }, +}); + +export default canvassAssignmentSlice; +export const { + assigneeAdd, + assigneeAdded, + assigneeUpdated, + assigneesLoad, + assigneesLoaded, + myAssignmentsLoad, + myAssignmentsLoaded, + canvassAssignmentCreated, + canvassAssignmentLoad, + canvassAssignmentLoaded, + canvassAssignmentUpdated, + canvassAssignmentsLoad, + canvassAssignmentsLoaded, + canvassSessionCreated, + canvassSessionsLoad, + canvassSessionsLoaded, + placeCreated, + placesLoad, + placesLoaded, + placeUpdated, + statsLoad, + statsLoaded, +} = canvassAssignmentSlice.actions; diff --git a/src/features/canvassAssignments/types.ts b/src/features/canvassAssignments/types.ts new file mode 100644 index 000000000..653b0d027 --- /dev/null +++ b/src/features/canvassAssignments/types.ts @@ -0,0 +1,107 @@ +import { ZetkinArea } from 'features/areas/types'; +import { ZetkinPerson } from 'utils/types/zetkin'; + +export type ZetkinMetric = { + _id: string; + definesDone: boolean; + description: string; + kind: 'boolean' | 'scale5'; + question: string; +}; + +export type ZetkinCanvassAssignment = { + campaign: { + id: number; + }; + id: string; + metrics: ZetkinMetric[]; + organization: { + id: number; + }; + title: string | null; +}; + +export type ZetkinCanvassAssignmentPostBody = Partial< + Omit +> & { + campaign_id: number; + metrics: Omit[]; +}; +export type ZetkinCanvassAssignmentPatchbody = Partial< + Omit +>; + +export type Visit = { + canvassAssId: string | null; + id: string; + noteToOfficial: string | null; + responses: { + metricId: string; + response: string; + }[]; + timestamp: string; +}; + +export type Household = { + id: string; + title: string; + visits: Visit[]; +}; + +export type HouseholdPatchBody = Partial>; + +export type ZetkinPlace = { + description: string | null; + households: Household[]; + id: string; + orgId: number; + position: { lat: number; lng: number }; + title: string | null; +}; + +export type ZetkinPlacePostBody = Partial< + Omit +>; + +export type ZetkinPlacePatchBody = Partial< + Omit +> & { + households?: Partial> & + { visits?: Partial>[] }[]; +}; + +export type ZetkinCanvassSession = { + area: ZetkinArea; + assignee: ZetkinPerson; + assignment: { + id: string; + title: string | null; + }; +}; + +export type ZetkinCanvassSessionPostBody = { + areaId: string; + personId: number; +}; + +export type ZetkinCanvassAssignee = { + canvassAssId: string; + id: number; +}; +export type ZetkinCanvassAssigneePatchBody = Partial; + +export type ZetkinCanvassAssignmentStats = { + metrics: { + metric: ZetkinMetric; + values: number[]; + }[]; + num_areas: number; + num_households: number; + num_places: number; + num_successful_visited_households: number; + num_visited_areas: number; + num_visited_households: number; + num_visited_households_outside_areas: number; + num_visited_places: number; + num_visited_places_outside_areas: number; +}; diff --git a/src/features/areas/utils/getCrosshairPositionOnMap.tsx b/src/features/canvassAssignments/utils/getCrosshairPositionOnMap.tsx similarity index 100% rename from src/features/areas/utils/getCrosshairPositionOnMap.tsx rename to src/features/canvassAssignments/utils/getCrosshairPositionOnMap.tsx diff --git a/src/features/areas/utils/isPointInsidePolygon.ts b/src/features/canvassAssignments/utils/isPointInsidePolygon.ts similarity index 100% rename from src/features/areas/utils/isPointInsidePolygon.ts rename to src/features/canvassAssignments/utils/isPointInsidePolygon.ts diff --git a/src/features/areas/utils/isWithinLast24Hours.tsx b/src/features/canvassAssignments/utils/isWithinLast24Hours.tsx similarity index 100% rename from src/features/areas/utils/isWithinLast24Hours.tsx rename to src/features/canvassAssignments/utils/isWithinLast24Hours.tsx diff --git a/src/features/areas/utils/markerIcon.tsx b/src/features/canvassAssignments/utils/markerIcon.tsx similarity index 100% rename from src/features/areas/utils/markerIcon.tsx rename to src/features/canvassAssignments/utils/markerIcon.tsx diff --git a/src/pages/organize/[orgId]/areas/index.tsx b/src/pages/organize/[orgId]/areas/index.tsx index ccc68f2a0..727efc1c2 100644 --- a/src/pages/organize/[orgId]/areas/index.tsx +++ b/src/pages/organize/[orgId]/areas/index.tsx @@ -22,7 +22,7 @@ export const getServerSideProps: GetServerSideProps = scaffold(async () => { }, scaffoldOptions); const AreasMap = dynamic( - () => import('../../../../features/areas/components/AreasMap'), + () => import('../../../../features/areas/components/AreasMap/index'), { ssr: false } ); diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/canvassers.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/canvassers.tsx index 6f75d6bb8..7b1a13e2f 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/canvassers.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/canvassers.tsx @@ -1,17 +1,15 @@ import { GetServerSideProps } from 'next'; import { DataGridPro, GridColDef } from '@mui/x-data-grid-pro'; -import CanvassAssignmentLayout from 'features/areas/layouts/CanvassAssignmentLayout'; import { scaffold } from 'utils/next'; import { PageWithLayout } from 'utils/types'; -import useCanvassSessions from 'features/areas/hooks/useCanvassSessions'; -import { ZetkinCanvassSession } from 'features/areas/types'; import { ZetkinPerson } from 'utils/types/zetkin'; import ZUIAvatar from 'zui/ZUIAvatar'; -import { useMessages } from 'core/i18n'; -import messageIds from 'features/areas/l10n/messageIds'; import ZUIPersonHoverCard from 'zui/ZUIPersonHoverCard'; import { AREAS } from 'utils/featureFlags'; +import { ZetkinCanvassSession } from 'features/canvassAssignments/types'; +import useCanvassSessions from 'features/canvassAssignments/hooks/useCanvassSessions'; +import CanvassAssignmentLayout from 'features/canvassAssignments/layouts/CanvassAssignmentLayout'; const scaffoldOptions = { authLevelRequired: 2, @@ -40,7 +38,6 @@ const CanvassAssignmentPage: PageWithLayout = ({ orgId, canvassAssId, }) => { - const messages = useMessages(messageIds); const allSessions = useCanvassSessions(parseInt(orgId), canvassAssId).data || []; const sessions = allSessions.filter( @@ -81,7 +78,7 @@ const CanvassAssignmentPage: PageWithLayout = ({ { field: 'name', flex: 1, - headerName: messages.canvassAssignment.canvassers.nameColumn(), + headerName: 'Name', valueGetter: (params) => `${params.row.person.first_name} ${params.row.person.last_name}`, }, @@ -90,7 +87,7 @@ const CanvassAssignmentPage: PageWithLayout = ({ field: 'areas', flex: 1, headerAlign: 'left', - headerName: messages.canvassAssignment.canvassers.areasColumn(), + headerName: 'Areas', type: 'number', valueGetter: (params) => params.row.sessions.length, }, diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx index 0b13f9f00..53d627412 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/editor.tsx @@ -12,15 +12,15 @@ import { Typography, } from '@mui/material'; +import ZUIFuture from 'zui/ZUIFuture'; +import MetricCard from 'features/canvassAssignments/components/MetricCard'; import { AREAS } from 'utils/featureFlags'; -import CanvassAssignmentLayout from 'features/areas/layouts/CanvassAssignmentLayout'; -import { PageWithLayout } from 'utils/types'; import { scaffold } from 'utils/next'; -import useCanvassAssignmentMutations from 'features/areas/hooks/useCanvassAssignmentMutations'; -import useCanvassAssignment from 'features/areas/hooks/useCanvassAssignment'; -import ZUIFuture from 'zui/ZUIFuture'; -import { ZetkinMetric } from 'features/areas/types'; -import MetricCard from 'features/areas/components/Metrics/MetricCard'; +import { PageWithLayout } from 'utils/types'; +import useCanvassAssignmentMutations from 'features/canvassAssignments/hooks/useCanvassAssignmentMutations'; +import useCanvassAssignment from 'features/canvassAssignments/hooks/useCanvassAssignment'; +import { ZetkinMetric } from 'features/canvassAssignments/types'; +import CanvassAssignmentLayout from 'features/canvassAssignments/layouts/CanvassAssignmentLayout'; const scaffoldOptions = { authLevelRequired: 2, @@ -62,8 +62,8 @@ const CanvassAssignmentEditorPage: PageWithLayout< if (canvassAssignmentFuture.data) { await updateCanvassAssignment({ metrics: canvassAssignmentFuture.data.metrics - .map((m) => (m.id === metric.id ? metric : m)) - .concat(metric.id ? [] : [metric]), + .map((m) => (m._id === metric._id ? metric : m)) + .concat(metric._id ? [] : [metric]), }); } setMetricBeingEdited(null); @@ -73,7 +73,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< if (canvassAssignmentFuture.data) { await updateCanvassAssignment({ metrics: canvassAssignmentFuture.data.metrics.filter( - (m) => m.id !== id + (m) => m._id !== id ), }); } @@ -82,16 +82,14 @@ const CanvassAssignmentEditorPage: PageWithLayout< const handleAddNewMetric = (kind: 'boolean' | 'scale5') => { setMetricBeingEdited({ + _id: '', definesDone: false, description: '', - id: '', kind: kind, question: '', }); }; - //console.log(canvassAssignmentFuture?.data?.metrics); - return ( @@ -125,11 +123,11 @@ const CanvassAssignmentEditorPage: PageWithLayout< onClose={() => setMetricBeingEdited(null)} onDelete={(target: EventTarget & HTMLButtonElement) => { if (metricBeingEdited.definesDone) { - setIdOfQuestionBeingDeleted(metricBeingEdited.id); + setIdOfQuestionBeingDeleted(metricBeingEdited._id); setAnchorEl(target); setMetricBeingEdited(null); } else { - handleDeleteMetric(metricBeingEdited.id); + handleDeleteMetric(metricBeingEdited._id); } }} onSave={handleSaveMetric} @@ -138,7 +136,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< {assignment.metrics.length > 0 ? 'Your list of questions:' : ''} {assignment.metrics.map((metric) => ( - + { if (metric.definesDone) { - setIdOfQuestionBeingDeleted(metric.id); + setIdOfQuestionBeingDeleted(metric._id); setAnchorEl(ev.currentTarget); } else { - handleDeleteMetric(metric.id); + handleDeleteMetric(metric._id); } }} > @@ -198,7 +196,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< > {`Delete "${ assignment.metrics.find( - (metric) => metric.id == idOfMetricBeingDeleted + (metric) => metric._id == idOfMetricBeingDeleted )?.question }"`} {`If you want to delete "${ assignment.metrics.find( - (metric) => metric.id == idOfMetricBeingDeleted + (metric) => metric._id == idOfMetricBeingDeleted )?.question }" you need to pick another yes/no-question to be the question that defines if the msision @@ -225,7 +223,7 @@ const CanvassAssignmentEditorPage: PageWithLayout< .filter( (metric) => metric.kind == 'boolean' && - metric.id != idOfMetricBeingDeleted + metric._id != idOfMetricBeingDeleted ) .map((metric) => ( { if (idOfMetricBeingDeleted) { const filtered = assignment.metrics.filter( - (metric) => metric.id != idOfMetricBeingDeleted + (metric) => metric._id != idOfMetricBeingDeleted ); updateCanvassAssignment({ metrics: [ diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx index f9670ac0c..c43807875 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/index.tsx @@ -4,19 +4,17 @@ import { Edit } from '@mui/icons-material'; import { useRouter } from 'next/router'; import { makeStyles, useTheme } from '@mui/styles'; -import CanvassAssignmentLayout from 'features/areas/layouts/CanvassAssignmentLayout'; -import { scaffold } from 'utils/next'; import { PageWithLayout } from 'utils/types'; -import useCanvassAssignment from 'features/areas/hooks/useCanvassAssignment'; -import { Msg } from 'core/i18n'; -import messageIds from 'features/areas/l10n/messageIds'; -import ZUIFutures from 'zui/ZUIFutures'; -import ZUIAnimatedNumber from 'zui/ZUIAnimatedNumber'; -import useCanvassAssignmentStats from 'features/areas/hooks/useCanvassAssignmentStats'; import ZUIStackedStatusBar from 'zui/ZUIStackedStatusBar'; import { getContrastColor } from 'utils/colorUtils'; import { AREAS } from 'utils/featureFlags'; -import AssignmentMetricsChart from 'features/areas/components/AssignmentMetricsChart'; +import { scaffold } from 'utils/next'; +import ZUIFutures from 'zui/ZUIFutures'; +import useCanvassAssignment from 'features/canvassAssignments/hooks/useCanvassAssignment'; +import useCanvassAssignmentStats from 'features/canvassAssignments/hooks/useCanvassAssignmentStats'; +import ZUIAnimatedNumber from 'zui/ZUIAnimatedNumber'; +import CanvassAssignmentLayout from 'features/canvassAssignments/layouts/CanvassAssignmentLayout'; +import AssignmentMetricsChart from 'features/canvassAssignments/components/AssignmentMetricsChart'; const scaffoldOptions = { authLevelRequired: 2, @@ -86,9 +84,7 @@ const CanvassAssignmentPage: PageWithLayout = ({ - - - + Areas {!!stats.num_areas && ( {(animatedValue) => ( @@ -105,19 +101,13 @@ const CanvassAssignmentPage: PageWithLayout = ({ startIcon={} variant="text" > - + Edit plan ) : ( - + This assignment has not been planned yet. diff --git a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/plan.tsx b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/plan.tsx index bdd649335..62157ef33 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/plan.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/canvassassignments/[canvassAssId]/plan.tsx @@ -5,16 +5,19 @@ import { GetServerSideProps } from 'next'; import { scaffold } from 'utils/next'; import { PageWithLayout } from 'utils/types'; -import CanvassAssignmentLayout from 'features/areas/layouts/CanvassAssignmentLayout'; +import CanvassAssignmentLayout from 'features/canvassAssignments/layouts/CanvassAssignmentLayout'; import useAreas from 'features/areas/hooks/useAreas'; import useServerSide from 'core/useServerSide'; -import useCanvassSessions from 'features/areas/hooks/useCanvassSessions'; +import useCanvassSessions from 'features/canvassAssignments/hooks/useCanvassSessions'; import ZUIFuture from 'zui/ZUIFuture'; -import useCreateCanvassSession from 'features/areas/hooks/useCreateCanvassSession'; +import useCreateCanvassSession from 'features/canvassAssignments/hooks/useCreateCanvassSession'; import { AREAS } from 'utils/featureFlags'; const PlanMap = dynamic( - () => import('../../../../../../../features/areas/components/PlanMap'), + () => + import( + '../../../../../../../features/canvassAssignments/components/PlanMap' + ), { ssr: false } ); diff --git a/src/utils/testing/mocks/mockState.ts b/src/utils/testing/mocks/mockState.ts index cd6a721e5..2fb3840b1 100644 --- a/src/utils/testing/mocks/mockState.ts +++ b/src/utils/testing/mocks/mockState.ts @@ -5,12 +5,6 @@ export default function mockState(overrides?: RootState) { const emptyState: RootState = { areas: { areaList: remoteList(), - assigneesByCanvassAssignmentId: {}, - canvassAssignmentList: remoteList(), - mySessionsList: remoteList(), - placeList: remoteList(), - sessionsByAssignmentId: {}, - statsByCanvassAssId: {}, tagsByAreaId: {}, }, breadcrumbs: { @@ -28,6 +22,14 @@ export default function mockState(overrides?: RootState) { campaignsByOrgId: {}, recentlyCreatedCampaign: null, }, + canvassAssignments: { + assigneesByCanvassAssignmentId: {}, + canvassAssignmentList: remoteList(), + mySessionsList: remoteList(), + placeList: remoteList(), + sessionsByAssignmentId: {}, + statsByCanvassAssId: {}, + }, duplicates: { potentialDuplicatesList: remoteList(), }, From 0a6b67c82220ba05bd088b5c574afaa79d2113cf Mon Sep 17 00:00:00 2001 From: ziggabyte Date: Wed, 16 Oct 2024 10:43:49 +0200 Subject: [PATCH 44/44] Add separate type for the metrics on the backend. --- .../[canvassAssId]/route.ts | 6 ++-- .../[canvassAssId]/stats/route.ts | 28 +++++++++---------- .../orgs/[orgId]/canvassassignments/route.ts | 2 +- .../components/MetricCard.tsx | 4 +-- .../components/PlaceDialog/VisitWizard.tsx | 6 ++-- src/features/canvassAssignments/models.ts | 2 +- src/features/canvassAssignments/types.ts | 4 +-- .../[canvassAssId]/editor.tsx | 26 ++++++++--------- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts index 6ee0444cb..aaa8395f6 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/route.ts @@ -38,9 +38,9 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { campaign: { id: canvassAssignmentModel.campId }, id: canvassAssignmentModel._id.toString(), metrics: (canvassAssignmentModel.metrics || []).map((metric) => ({ - _id: metric._id, definesDone: metric.definesDone || false, description: metric.description || '', + id: metric._id, kind: metric.kind, question: metric.question, })), @@ -82,7 +82,7 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { // Identify metrics to be deleted const metricsToDelete = existingMetricsIds.filter( - (id) => !newMetrics.some((metric: ZetkinMetric) => metric._id === id) + (id) => !newMetrics.some((metric: ZetkinMetric) => metric.id === id) ); // Remove metrics that are no longer included @@ -136,9 +136,9 @@ export async function PATCH(request: NextRequest, { params }: RouteMeta) { campaign: { id: model.campId }, id: model._id.toString(), metrics: (model.metrics || []).map((metric) => ({ - _id: metric._id, definesDone: metric.definesDone || false, description: metric.description || '', + id: metric._id, kind: metric.kind, question: metric.question, })), diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts index cafe58aad..59948ba59 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/[canvassAssId]/stats/route.ts @@ -36,40 +36,40 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { await mongoose.connect(process.env.MONGODB_URL || ''); //Find all sessions of the assignment - const model = await CanvassAssignmentModel.findOne({ + const assignmentModel = await CanvassAssignmentModel.findOne({ _id: params.canvassAssId, }); - if (!model) { + if (!assignmentModel) { return new NextResponse(null, { status: 404 }); } const sessions: ZetkinCanvassSession[] = []; - for await (const sessionData of model.sessions) { + for await (const sessionData of assignmentModel.sessions) { const person = await apiClient.get( `/api/orgs/${orgId}/people/${sessionData.personId}` ); - const area = await AreaModel.findOne({ + const areaModel = await AreaModel.findOne({ _id: sessionData.areaId, }); - if (area && person) { + if (areaModel && person) { sessions.push({ area: { - description: area.description, - id: area._id.toString(), + description: areaModel.description, + id: areaModel._id.toString(), organization: { id: orgId, }, - points: area.points, + points: areaModel.points, tags: [], //TODO: Is this really neccessary here? - title: area.title, + title: areaModel.title, }, assignee: person, assignment: { - id: model._id.toString(), - title: model.title, + id: assignmentModel._id.toString(), + title: assignmentModel.title, }, }); } @@ -121,16 +121,16 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { const visitedAreas: string[] = []; const householdsInAreas: Household[] = []; - const configuredMetrics = model.metrics; + const configuredMetrics = assignmentModel.metrics; const idOfMetricThatDefinesDone = configuredMetrics.find( (metric) => metric.definesDone )?._id; const accumulatedMetrics: ZetkinCanvassAssignmentStats['metrics'] = configuredMetrics.map((metric) => ({ metric: { - _id: metric._id, definesDone: metric.definesDone, description: metric.description, + id: metric._id, kind: metric.kind, question: metric.question, }, @@ -147,7 +147,7 @@ export async function GET(request: NextRequest, { params }: RouteMeta) { ); const accumulatedMetric = accumulatedMetrics.find( - (accum) => accum.metric._id == response.metricId + (accum) => accum.metric.id == response.metricId ); if (accumulatedMetric && configuredMetric) { diff --git a/src/app/beta/orgs/[orgId]/canvassassignments/route.ts b/src/app/beta/orgs/[orgId]/canvassassignments/route.ts index e032021a6..2171ae1f6 100644 --- a/src/app/beta/orgs/[orgId]/canvassassignments/route.ts +++ b/src/app/beta/orgs/[orgId]/canvassassignments/route.ts @@ -71,9 +71,9 @@ export async function POST(request: NextRequest, { params }: RouteMeta) { campaign: { id: model.campId }, id: model._id.toString(), metrics: model.metrics.map((metric) => ({ - _id: metric._id, definesDone: metric.definesDone || false, description: metric.description || '', + id: metric._id, kind: metric.kind, question: metric.question, })), diff --git a/src/features/canvassAssignments/components/MetricCard.tsx b/src/features/canvassAssignments/components/MetricCard.tsx index b504e7685..83b5d711b 100644 --- a/src/features/canvassAssignments/components/MetricCard.tsx +++ b/src/features/canvassAssignments/components/MetricCard.tsx @@ -38,7 +38,7 @@ const MetricCard: FC = ({ metric.definesDone || false ); - const isEditing = !!metric?._id; + const isEditing = !!metric?.id; useEffect(() => { setQuestion(metric.question || ''); @@ -106,9 +106,9 @@ const MetricCard: FC = ({