6760 lines
222 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Build Power. Not Rent It. Own your digital infrastructure.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://bnkserve.org/v2/features/map/canvassing/">
<link rel="prev" href="../shifts/">
<link rel="next" href="../tracking/">
<link rel="icon" href="../../../../assets/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
<title>Canvassing - Changemaker Lite</title>
<link rel="stylesheet" href="../../../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../../../stylesheets/extra.css">
<link rel="stylesheet" href="../../../../stylesheets/home.css">
<link rel="stylesheet" href="../../../../assets/css/video-player.css">
<script>__md_scope=new URL("../../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="Canvassing - Changemaker Lite" />
<meta property="og:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://bnkserve.org/assets/images/social/v2/features/map/canvassing.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://bnkserve.org/v2/features/map/canvassing/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Canvassing - Changemaker Lite" />
<meta property="twitter:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/v2/features/map/canvassing.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#canvassing-session-system" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Changemaker Lite
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Canvassing
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
</nav>
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../../" class="md-tabs__link">
V2 Documentation
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../phil/" class="md-tabs__link">
Philosophy
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../v1/" class="md-tabs__link">
V1 Documentation (Legacy)
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../../../" class="md-nav__link ">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
V2 Documentation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_2" >
<div class="md-nav__link md-nav__container">
<a href="../../../getting-started/" class="md-nav__link ">
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../getting-started/quick-start/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_3" >
<div class="md-nav__link md-nav__container">
<a href="../../../architecture/" class="md-nav__link ">
<span class="md-ellipsis">
Architecture
</span>
</a>
<label class="md-nav__link " for="__nav_2_3" id="__nav_2_3_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_3">
<span class="md-nav__icon md-icon"></span>
Architecture
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../architecture/dual-api/" class="md-nav__link">
<span class="md-ellipsis">
Dual API System
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../architecture/authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication & Security
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_4" >
<div class="md-nav__link md-nav__container">
<a href="../../../backend/" class="md-nav__link ">
<span class="md-ellipsis">
Backend
</span>
</a>
<label class="md-nav__link " for="__nav_2_4" id="__nav_2_4_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_4">
<span class="md-nav__icon md-icon"></span>
Backend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/modules/" class="md-nav__link">
<span class="md-ellipsis">
Modules
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/middleware/" class="md-nav__link">
<span class="md-ellipsis">
Middleware
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/utilities/" class="md-nav__link">
<span class="md-ellipsis">
Utilities
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_5" >
<div class="md-nav__link md-nav__container">
<a href="../../../frontend/" class="md-nav__link ">
<span class="md-ellipsis">
Frontend
</span>
</a>
<label class="md-nav__link " for="__nav_2_5" id="__nav_2_5_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_5">
<span class="md-nav__icon md-icon"></span>
Frontend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/components/" class="md-nav__link">
<span class="md-ellipsis">
Components
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/layouts/" class="md-nav__link">
<span class="md-ellipsis">
Layouts
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_6" >
<div class="md-nav__link md-nav__container">
<a href="../../../database/" class="md-nav__link ">
<span class="md-ellipsis">
Database
</span>
</a>
<label class="md-nav__link " for="__nav_2_6" id="__nav_2_6_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_6">
<span class="md-nav__icon md-icon"></span>
Database
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../database/schema/" class="md-nav__link">
<span class="md-ellipsis">
Schema Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/seeding/" class="md-nav__link">
<span class="md-ellipsis">
Seeding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/indexes/" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../database/models/" class="md-nav__link">
<span class="md-ellipsis">
Models
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_7" checked>
<div class="md-nav__link md-nav__container">
<a href="../../" class="md-nav__link ">
<span class="md-ellipsis">
Features
</span>
</a>
<label class="md-nav__link " for="__nav_2_7" id="__nav_2_7_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_7_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_7">
<span class="md-nav__icon md-icon"></span>
Features
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../influence/" class="md-nav__link">
<span class="md-ellipsis">
Influence
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_7_3" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<span class="md-ellipsis">
Map
</span>
</a>
<label class="md-nav__link " for="__nav_2_7_3" id="__nav_2_7_3_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="3" aria-labelledby="__nav_2_7_3_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_7_3">
<span class="md-nav__icon md-icon"></span>
Map
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../locations/" class="md-nav__link">
<span class="md-ellipsis">
Locations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../geocoding/" class="md-nav__link">
<span class="md-ellipsis">
Geocoding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../cuts/" class="md-nav__link">
<span class="md-ellipsis">
Cuts
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../shifts/" class="md-nav__link">
<span class="md-ellipsis">
Shifts
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Canvassing
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Canvassing
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#architecture" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-models" class="md-nav__link">
<span class="md-ellipsis">
Database Models
</span>
</a>
<nav class="md-nav" aria-label="Database Models">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#canvasssession-model" class="md-nav__link">
<span class="md-ellipsis">
CanvassSession Model
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#canvassvisit-model" class="md-nav__link">
<span class="md-ellipsis">
CanvassVisit Model
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#api-endpoints" class="md-nav__link">
<span class="md-ellipsis">
API Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuration" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
<nav class="md-nav" aria-label="Configuration">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#environment-variables" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rate-limiting" class="md-nav__link">
<span class="md-ellipsis">
Rate Limiting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#session-auto-cleanup" class="md-nav__link">
<span class="md-ellipsis">
Session Auto-Cleanup
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#admin-workflow" class="md-nav__link">
<span class="md-ellipsis">
Admin Workflow
</span>
</a>
<nav class="md-nav" aria-label="Admin Workflow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#viewing-active-sessions" class="md-nav__link">
<span class="md-ellipsis">
Viewing Active Sessions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#monitoring-canvass-activity" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Canvass Activity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#tracking-cut-completion" class="md-nav__link">
<span class="md-ellipsis">
Tracking Cut Completion
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#volunteer-leaderboard" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Leaderboard
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#volunteer-workflow" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Workflow
</span>
</a>
<nav class="md-nav" aria-label="Volunteer Workflow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#starting-a-canvass-session" class="md-nav__link">
<span class="md-ellipsis">
Starting a Canvass Session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#following-walking-route" class="md-nav__link">
<span class="md-ellipsis">
Following Walking Route
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recording-a-visit" class="md-nav__link">
<span class="md-ellipsis">
Recording a Visit
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ending-a-canvass-session" class="md-nav__link">
<span class="md-ellipsis">
Ending a Canvass Session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#viewing-visit-history" class="md-nav__link">
<span class="md-ellipsis">
Viewing Visit History
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#code-examples" class="md-nav__link">
<span class="md-ellipsis">
Code Examples
</span>
</a>
<nav class="md-nav" aria-label="Code Examples">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#start-canvass-session-backend" class="md-nav__link">
<span class="md-ellipsis">
Start Canvass Session (Backend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#calculate-walking-route-backend" class="md-nav__link">
<span class="md-ellipsis">
Calculate Walking Route (Backend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#record-visit-backend" class="md-nav__link">
<span class="md-ellipsis">
Record Visit (Backend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gps-auto-tracking-frontend" class="md-nav__link">
<span class="md-ellipsis">
GPS Auto-Tracking (Frontend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#visit-recording-form-frontend" class="md-nav__link">
<span class="md-ellipsis">
Visit Recording Form (Frontend)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#issue-walking-route-not-optimal" class="md-nav__link">
<span class="md-ellipsis">
Issue: Walking Route Not Optimal
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#issue-session-auto-abandoned-prematurely" class="md-nav__link">
<span class="md-ellipsis">
Issue: Session Auto-Abandoned Prematurely
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#issue-gps-tracking-draining-battery" class="md-nav__link">
<span class="md-ellipsis">
Issue: GPS Tracking Draining Battery
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-considerations" class="md-nav__link">
<span class="md-ellipsis">
Performance Considerations
</span>
</a>
<nav class="md-nav" aria-label="Performance Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#visit-recording-rate-limiting" class="md-nav__link">
<span class="md-ellipsis">
Visit Recording Rate Limiting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#session-cleanup-performance" class="md-nav__link">
<span class="md-ellipsis">
Session Cleanup Performance
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#cut-completion-calculation" class="md-nav__link">
<span class="md-ellipsis">
Cut Completion Calculation
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../tracking/" class="md-nav__link">
<span class="md-ellipsis">
Tracking
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../walk-sheets/" class="md-nav__link">
<span class="md-ellipsis">
Walk Sheets
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../nar-import/" class="md-nav__link">
<span class="md-ellipsis">
NAR Import
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../data-quality/" class="md-nav__link">
<span class="md-ellipsis">
Data Quality
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../landing-pages/" class="md-nav__link">
<span class="md-ellipsis">
Landing Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../email-templates/" class="md-nav__link">
<span class="md-ellipsis">
Email Templates
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../media/" class="md-nav__link">
<span class="md-ellipsis">
Media
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../newsletter/" class="md-nav__link">
<span class="md-ellipsis">
Newsletter
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../observability/" class="md-nav__link">
<span class="md-ellipsis">
Observability
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../tunnel/" class="md-nav__link">
<span class="md-ellipsis">
Tunnel
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_8" >
<div class="md-nav__link md-nav__container">
<a href="../../../deployment/" class="md-nav__link ">
<span class="md-ellipsis">
Deployment
</span>
</a>
<label class="md-nav__link " for="__nav_2_8" id="__nav_2_8_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_8">
<span class="md-nav__icon md-icon"></span>
Deployment
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../deployment/docker-compose/" class="md-nav__link">
<span class="md-ellipsis">
Docker Compose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/environment-variables/" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/nginx/" class="md-nav__link">
<span class="md-ellipsis">
Nginx Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/ssl-tls/" class="md-nav__link">
<span class="md-ellipsis">
SSL/TLS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/tunneling/" class="md-nav__link">
<span class="md-ellipsis">
Tunneling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/monitoring-stack/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Stack
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/healthchecks/" class="md-nav__link">
<span class="md-ellipsis">
Health Checks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/scaling/" class="md-nav__link">
<span class="md-ellipsis">
Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/backup-restore/" class="md-nav__link">
<span class="md-ellipsis">
Backup & Restore
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_9" >
<div class="md-nav__link md-nav__container">
<a href="../../../development/" class="md-nav__link ">
<span class="md-ellipsis">
Development
</span>
</a>
<label class="md-nav__link " for="__nav_2_9" id="__nav_2_9_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_9">
<span class="md-nav__icon md-icon"></span>
Development
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../development/local-setup/" class="md-nav__link">
<span class="md-ellipsis">
Local Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/docker-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Docker Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/git-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Git Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/npm-commands/" class="md-nav__link">
<span class="md-ellipsis">
NPM Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/typescript/" class="md-nav__link">
<span class="md-ellipsis">
TypeScript
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/debugging/" class="md-nav__link">
<span class="md-ellipsis">
Debugging
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/code-style/" class="md-nav__link">
<span class="md-ellipsis">
Code Style
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_10" >
<div class="md-nav__link md-nav__container">
<a href="../../../api-reference/" class="md-nav__link ">
<span class="md-ellipsis">
API Reference
</span>
</a>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_10_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_10">
<span class="md-nav__icon md-icon"></span>
API Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_11" >
<div class="md-nav__link md-nav__container">
<a href="../../../user-guides/" class="md-nav__link ">
<span class="md-ellipsis">
User Guides
</span>
</a>
<label class="md-nav__link " for="__nav_2_11" id="__nav_2_11_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_11_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_11">
<span class="md-nav__icon md-icon"></span>
User Guides
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../user-guides/admin-guide/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/campaign-manager-guide/" class="md-nav__link">
<span class="md-ellipsis">
Campaign Manager Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/map-organizer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Map Organizer Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/content-editor-guide/" class="md-nav__link">
<span class="md-ellipsis">
Content Editor Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/volunteer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_12" >
<div class="md-nav__link md-nav__container">
<a href="../../../troubleshooting/" class="md-nav__link ">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<label class="md-nav__link " for="__nav_2_12" id="__nav_2_12_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_12_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_12">
<span class="md-nav__icon md-icon"></span>
Troubleshooting
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../troubleshooting/faq/" class="md-nav__link">
<span class="md-ellipsis">
FAQ
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/common-errors/" class="md-nav__link">
<span class="md-ellipsis">
Common Errors
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/auth-issues/" class="md-nav__link">
<span class="md-ellipsis">
Auth Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/database-issues/" class="md-nav__link">
<span class="md-ellipsis">
Database Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/docker-issues/" class="md-nav__link">
<span class="md-ellipsis">
Docker Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/email-issues/" class="md-nav__link">
<span class="md-ellipsis">
Email Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/geocoding-issues/" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/monitoring-issues/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/performance-optimization/" class="md-nav__link">
<span class="md-ellipsis">
Performance Optimization
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_13" >
<div class="md-nav__link md-nav__container">
<a href="../../../migration/" class="md-nav__link ">
<span class="md-ellipsis">
Migration
</span>
</a>
<label class="md-nav__link " for="__nav_2_13" id="__nav_2_13_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_13_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_13">
<span class="md-nav__icon md-icon"></span>
Migration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../migration/feature-parity/" class="md-nav__link">
<span class="md-ellipsis">
Feature Parity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/breaking-changes/" class="md-nav__link">
<span class="md-ellipsis">
Breaking Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/api-changes/" class="md-nav__link">
<span class="md-ellipsis">
API Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/data-migration/" class="md-nav__link">
<span class="md-ellipsis">
Data Migration
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_14" >
<div class="md-nav__link md-nav__container">
<a href="../../../contributing/" class="md-nav__link ">
<span class="md-ellipsis">
Contributing
</span>
</a>
<label class="md-nav__link " for="__nav_2_14" id="__nav_2_14_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_14_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_14">
<span class="md-nav__icon md-icon"></span>
Contributing
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../contributing/development-setup/" class="md-nav__link">
<span class="md-ellipsis">
Development Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/code-of-conduct/" class="md-nav__link">
<span class="md-ellipsis">
Code of Conduct
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/pull-requests/" class="md-nav__link">
<span class="md-ellipsis">
Pull Requests
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/roadmap/" class="md-nav__link">
<span class="md-ellipsis">
Roadmap
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../v1/" class="md-nav__link">
<span class="md-ellipsis">
V1 Documentation (Legacy)
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#architecture" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-models" class="md-nav__link">
<span class="md-ellipsis">
Database Models
</span>
</a>
<nav class="md-nav" aria-label="Database Models">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#canvasssession-model" class="md-nav__link">
<span class="md-ellipsis">
CanvassSession Model
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#canvassvisit-model" class="md-nav__link">
<span class="md-ellipsis">
CanvassVisit Model
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#api-endpoints" class="md-nav__link">
<span class="md-ellipsis">
API Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuration" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
<nav class="md-nav" aria-label="Configuration">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#environment-variables" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rate-limiting" class="md-nav__link">
<span class="md-ellipsis">
Rate Limiting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#session-auto-cleanup" class="md-nav__link">
<span class="md-ellipsis">
Session Auto-Cleanup
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#admin-workflow" class="md-nav__link">
<span class="md-ellipsis">
Admin Workflow
</span>
</a>
<nav class="md-nav" aria-label="Admin Workflow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#viewing-active-sessions" class="md-nav__link">
<span class="md-ellipsis">
Viewing Active Sessions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#monitoring-canvass-activity" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Canvass Activity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#tracking-cut-completion" class="md-nav__link">
<span class="md-ellipsis">
Tracking Cut Completion
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#volunteer-leaderboard" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Leaderboard
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#volunteer-workflow" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Workflow
</span>
</a>
<nav class="md-nav" aria-label="Volunteer Workflow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#starting-a-canvass-session" class="md-nav__link">
<span class="md-ellipsis">
Starting a Canvass Session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#following-walking-route" class="md-nav__link">
<span class="md-ellipsis">
Following Walking Route
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#recording-a-visit" class="md-nav__link">
<span class="md-ellipsis">
Recording a Visit
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#ending-a-canvass-session" class="md-nav__link">
<span class="md-ellipsis">
Ending a Canvass Session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#viewing-visit-history" class="md-nav__link">
<span class="md-ellipsis">
Viewing Visit History
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#code-examples" class="md-nav__link">
<span class="md-ellipsis">
Code Examples
</span>
</a>
<nav class="md-nav" aria-label="Code Examples">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#start-canvass-session-backend" class="md-nav__link">
<span class="md-ellipsis">
Start Canvass Session (Backend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#calculate-walking-route-backend" class="md-nav__link">
<span class="md-ellipsis">
Calculate Walking Route (Backend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#record-visit-backend" class="md-nav__link">
<span class="md-ellipsis">
Record Visit (Backend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#gps-auto-tracking-frontend" class="md-nav__link">
<span class="md-ellipsis">
GPS Auto-Tracking (Frontend)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#visit-recording-form-frontend" class="md-nav__link">
<span class="md-ellipsis">
Visit Recording Form (Frontend)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#issue-walking-route-not-optimal" class="md-nav__link">
<span class="md-ellipsis">
Issue: Walking Route Not Optimal
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#issue-session-auto-abandoned-prematurely" class="md-nav__link">
<span class="md-ellipsis">
Issue: Session Auto-Abandoned Prematurely
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#issue-gps-tracking-draining-battery" class="md-nav__link">
<span class="md-ellipsis">
Issue: GPS Tracking Draining Battery
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-considerations" class="md-nav__link">
<span class="md-ellipsis">
Performance Considerations
</span>
</a>
<nav class="md-nav" aria-label="Performance Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#visit-recording-rate-limiting" class="md-nav__link">
<span class="md-ellipsis">
Visit Recording Rate Limiting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#session-cleanup-performance" class="md-nav__link">
<span class="md-ellipsis">
Session Cleanup Performance
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#cut-completion-calculation" class="md-nav__link">
<span class="md-ellipsis">
Cut Completion Calculation
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../../../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../../" class="md-path__link">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../" class="md-path__link">
<span class="md-ellipsis">
Features
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
Map
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/features/map/canvassing.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/features/map/canvassing.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
<h1 id="canvassing-session-system">Canvassing Session System<a class="headerlink" href="#canvassing-session-system" title="Permanent link">&para;</a></h1>
<h2 id="overview">Overview<a class="headerlink" href="#overview" title="Permanent link">&para;</a></h2>
<p>The canvassing system provides a complete door-to-door organizing workflow with GPS tracking, walking route optimization, visit recording, and progress tracking. It enables volunteers to efficiently canvass assigned territories using mobile devices with real-time location updates.</p>
<p><strong>Key Capabilities:</strong></p>
<ul>
<li><strong>Session Lifecycle</strong>: ACTIVE → COMPLETED → ABANDONED (auto-close after 12h)</li>
<li><strong>Walking Route Algorithm</strong>: Nearest-neighbor optimization from volunteer GPS position</li>
<li><strong>Visit Recording</strong>: 7 outcome types with support level updates</li>
<li><strong>GPS Integration</strong>: Live tracking via TrackingSession (1:1 relationship)</li>
<li><strong>Rate Limiting</strong>: 30 visits/min per IP to prevent abuse</li>
<li><strong>Progress Tracking</strong>: Cut completion percentage auto-calculated</li>
<li><strong>Admin Oversight</strong>: Active sessions dashboard, activity feed, leaderboard</li>
<li><strong>Volunteer Portal</strong>: Full-screen map with bottom-sheet visit recording</li>
</ul>
<p><strong>Use Cases:</strong></p>
<ul>
<li>Door-to-door canvassing for electoral campaigns</li>
<li>Voter ID (identifying supporter levels)</li>
<li>GOTV (Get Out The Vote) efforts</li>
<li>Sign placement tracking</li>
<li>Petition signature collection</li>
<li>Issue surveys</li>
<li>Volunteer coordination</li>
</ul>
<h2 id="architecture">Architecture<a class="headerlink" href="#architecture" title="Permanent link">&para;</a></h2>
<pre class="mermaid"><code>graph TD
A[Volunteer] --&gt;|Start Session| B[VolunteerMapPage]
B --&gt;|POST /api/map/canvass/sessions| C[Canvass Service]
C --&gt;|Create| D[(CanvassSession)]
C --&gt;|Start GPS| E[Tracking Service]
E --&gt;|Create| F[(TrackingSession)]
B --&gt;|Load Addresses| C
C --&gt;|Filter by Cut| G[Spatial Utils]
G --&gt;|Point-in-Polygon| H[(Location Model)]
B --&gt;|Calculate Route| I[Walking Route Service]
I --&gt;|Nearest Neighbor| J[Haversine Distance]
J --&gt;|Return Route| B
K[Volunteer GPS] --&gt;|Submit Points| E
E --&gt;|Save| L[(TrackPoint)]
E --&gt;|Calculate Distance| J
B --&gt;|Record Visit| C
C --&gt;|Create| M[(CanvassVisit)]
C --&gt;|Update Address| N[(Address Model)]
C --&gt;|Update Progress| D
O[Admin] --&gt;|View Dashboard| P[CanvassDashboardPage]
P --&gt;|GET /api/map/canvass/admin/activity| C
C --&gt;|Aggregate Stats| D
C --&gt;|Activity Feed| M
D --&gt;|1:1| F
D --&gt;|1:N| M
M --&gt;|N:1| N
style D fill:#e1f5ff
style F fill:#e1f5ff
style M fill:#e1f5ff
style N fill:#e1f5ff
style H fill:#e1f5ff</code></pre>
<p><strong>Flow Description:</strong></p>
<ol>
<li><strong>Volunteer starts session</strong> → Creates CanvassSession + TrackingSession, loads addresses within cut</li>
<li><strong>Calculate route</strong> → Walking route service uses nearest-neighbor from volunteer GPS position</li>
<li><strong>GPS tracking</strong> → Auto-submit points every 10s, calculate distance with haversine</li>
<li><strong>Record visit</strong> → Create CanvassVisit with outcome, update Address support level, update session progress</li>
<li><strong>End session</strong> → Mark session COMPLETED, end tracking session, calculate final stats</li>
<li><strong>Admin oversight</strong> → View active sessions, activity feed, cut progress, volunteer leaderboard</li>
</ol>
<h2 id="database-models">Database Models<a class="headerlink" href="#database-models" title="Permanent link">&para;</a></h2>
<h3 id="canvasssession-model">CanvassSession Model<a class="headerlink" href="#canvasssession-model" title="Permanent link">&para;</a></h3>
<p>See <a href="../../../database/models/canvass/#canvasssession-model">CanvassSession Model Documentation</a> for full schema.</p>
<p><strong>Key Fields:</strong></p>
<ul>
<li><code>userId</code>: Foreign key to volunteer User</li>
<li><code>cutId</code>: Foreign key to Cut (territory)</li>
<li><code>shiftId</code>: Optional foreign key to Shift (if started from shift)</li>
<li><code>status</code>: ACTIVE | COMPLETED | ABANDONED</li>
<li><code>startedAt</code>: Session start timestamp</li>
<li><code>endedAt</code>: Session end timestamp (null while active)</li>
<li><code>totalVisits</code>: Count of CanvassVisit records</li>
<li><code>completionPercentage</code>: Auto-calculated from cut progress</li>
</ul>
<p><strong>Status Lifecycle:</strong></p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>ACTIVE (session running)
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a> ↓ (volunteer ends session)
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a>COMPLETED
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a>OR
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a>ACTIVE (session running &gt; 12 hours)
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a> ↓ (auto-cleanup cron)
</span><span id="__span-0-9"><a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a>ABANDONED
</span></code></pre></div>
<h3 id="canvassvisit-model">CanvassVisit Model<a class="headerlink" href="#canvassvisit-model" title="Permanent link">&para;</a></h3>
<p>See <a href="../../../database/models/canvass/#canvassvisit-model">CanvassVisit Model Documentation</a> for full schema.</p>
<p><strong>Key Fields:</strong></p>
<ul>
<li><code>sessionId</code>: Foreign key to CanvassSession</li>
<li><code>userId</code>: Foreign key to volunteer User</li>
<li><code>addressId</code>: Foreign key to Address (specific unit visited)</li>
<li><code>outcome</code>: Visit result (7 types)</li>
<li><code>supportLevel</code>: Updated support level (LEVEL_1-4 or null)</li>
<li><code>signRequested</code>: Boolean - resident wants lawn/window sign</li>
<li><code>notes</code>: Free-text canvass notes</li>
<li><code>visitedAt</code>: Visit timestamp</li>
<li><code>durationSeconds</code>: Time spent at door (auto-calculated)</li>
</ul>
<p><strong>Visit Outcome Enum:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="kd">enum</span><span class="w"> </span><span class="nx">VisitOutcome</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a><span class="w"> </span><span class="nx">NOT_HOME</span><span class="w"> </span><span class="c1">// Nobody answered door</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="w"> </span><span class="nx">REFUSED</span><span class="w"> </span><span class="c1">// Refused to speak</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a><span class="w"> </span><span class="nx">MOVED</span><span class="w"> </span><span class="c1">// Resident moved away</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a><span class="w"> </span><span class="nx">ALREADY_VOTED</span><span class="w"> </span><span class="c1">// Already voted (GOTV)</span>
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6" href="#__codelineno-1-6"></a><span class="w"> </span><span class="nx">SPOKE_WITH</span><span class="w"> </span><span class="c1">// Had conversation</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7" href="#__codelineno-1-7"></a><span class="w"> </span><span class="nx">LEFT_LITERATURE</span><span class="w"> </span><span class="c1">// Left campaign material</span>
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8" href="#__codelineno-1-8"></a><span class="w"> </span><span class="nx">COME_BACK_LATER</span><span class="w"> </span><span class="c1">// Asked to return later</span>
</span><span id="__span-1-9"><a id="__codelineno-1-9" name="__codelineno-1-9" href="#__codelineno-1-9"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Related Models:</strong></p>
<ul>
<li><a href="../../../database/models/canvass/#trackingsession-model">TrackingSession</a> — GPS tracking (1:1)</li>
<li><a href="../../../database/models/map/#address-model">Address</a> — Updated with visit data</li>
<li><a href="../../../database/models/map/#cut-model">Cut</a> — Territory boundary</li>
<li><a href="../../../database/models/map/#shift-model">Shift</a> — Optional shift assignment</li>
</ul>
<h2 id="api-endpoints">API Endpoints<a class="headerlink" href="#api-endpoints" title="Permanent link">&para;</a></h2>
<p>See <a href="../../backend/modules/map/canvass.md">Canvass Backend Module Documentation</a> for full API reference.</p>
<p><strong>Volunteer Endpoints:</strong></p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Auth</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/volunteer/assignments</code></td>
<td>Any logged-in user</td>
<td>Get shifts with cut assignments</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/volunteer/stats</code></td>
<td>Any logged-in user</td>
<td>Get volunteer canvass statistics</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/volunteer/visits</code></td>
<td>Any logged-in user</td>
<td>List own canvass visits with pagination</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/map/canvass/sessions</code></td>
<td>Any logged-in user</td>
<td>Start new canvass session</td>
</tr>
<tr>
<td>PATCH</td>
<td><code>/api/map/canvass/sessions/:id</code></td>
<td>Any logged-in user</td>
<td>Update session (end session)</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/sessions/:id/addresses</code></td>
<td>Any logged-in user</td>
<td>Get addresses within session cut</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/map/canvass/sessions/:id/route</code></td>
<td>Any logged-in user</td>
<td>Calculate walking route</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/map/canvass/visits</code></td>
<td>Any logged-in user</td>
<td>Record single visit</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/map/canvass/visits/bulk</code></td>
<td>Any logged-in user</td>
<td>Record multiple visits (batch)</td>
</tr>
<tr>
<td>PATCH</td>
<td><code>/api/map/canvass/volunteer/locations/:id</code></td>
<td>Any logged-in user</td>
<td>Update location from canvass</td>
</tr>
</tbody>
</table>
<p><strong>Admin Endpoints:</strong></p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Auth</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/admin/activity</code></td>
<td>MAP_ADMIN</td>
<td>Get recent canvass activity feed</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/admin/sessions</code></td>
<td>MAP_ADMIN</td>
<td>List active canvass sessions</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/admin/visits</code></td>
<td>MAP_ADMIN</td>
<td>List all canvass visits with filters</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/admin/progress</code></td>
<td>MAP_ADMIN</td>
<td>Get cut completion progress</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/map/canvass/admin/leaderboard</code></td>
<td>MAP_ADMIN</td>
<td>Get volunteer visit leaderboard</td>
</tr>
</tbody>
</table>
<h2 id="configuration">Configuration<a class="headerlink" href="#configuration" title="Permanent link">&para;</a></h2>
<h3 id="environment-variables">Environment Variables<a class="headerlink" href="#environment-variables" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CANVASS_SESSION_TIMEOUT_HOURS</code></td>
<td>number</td>
<td><code>12</code></td>
<td>Auto-abandon active sessions after N hours</td>
</tr>
<tr>
<td><code>CANVASS_VISIT_RATE_LIMIT</code></td>
<td>number</td>
<td><code>30</code></td>
<td>Max visits per minute per IP</td>
</tr>
</tbody>
</table>
<h3 id="rate-limiting">Rate Limiting<a class="headerlink" href="#rate-limiting" title="Permanent link">&para;</a></h3>
<p><strong>Visit Recording Rate Limit:</strong></p>
<ul>
<li><strong>Limit</strong>: 30 visits/min per IP address</li>
<li><strong>Window</strong>: 60 seconds</li>
<li><strong>Redis Prefix</strong>: <code>rl:canvass-visit:</code></li>
<li><strong>Purpose</strong>: Prevent accidental bulk submissions from GPS auto-submit bugs</li>
</ul>
<h3 id="session-auto-cleanup">Session Auto-Cleanup<a class="headerlink" href="#session-auto-cleanup" title="Permanent link">&para;</a></h3>
<p><strong>Abandoned Session Detection:</strong></p>
<p>System automatically marks sessions as ABANDONED if:</p>
<ul>
<li><strong>Status</strong>: ACTIVE</li>
<li><strong>Started</strong>: &gt;12 hours ago</li>
<li><strong>No Activity</strong>: No visits in last hour</li>
</ul>
<p><strong>Cleanup Schedule:</strong></p>
<ul>
<li><strong>Startup</strong>: On API server startup (check all active sessions)</li>
<li><strong>Cron</strong>: Every hour at :00 (setInterval)</li>
</ul>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="c1">// api/src/server.ts</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="nx">setInterval</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">canvassService</span><span class="p">.</span><span class="nx">cleanupAbandonedSessions</span><span class="p">();</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a><span class="p">},</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">);</span><span class="w"> </span><span class="c1">// 1 hour</span>
</span></code></pre></div>
<h2 id="admin-workflow">Admin Workflow<a class="headerlink" href="#admin-workflow" title="Permanent link">&para;</a></h2>
<h3 id="viewing-active-sessions">Viewing Active Sessions<a class="headerlink" href="#viewing-active-sessions" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: Navigate to Canvass Dashboard</strong></p>
<p>Navigate to <strong>Map → Canvass Dashboard</strong> in the admin sidebar.</p>
<p>![CanvassDashboardPage Screenshot Placeholder]</p>
<p><strong>Step 2: View Active Sessions</strong></p>
<p><strong>Active Sessions</strong> card displays:</p>
<ul>
<li><strong>Volunteer Name</strong>: Who is canvassing</li>
<li><strong>Cut Name</strong>: Territory being canvassed</li>
<li><strong>Start Time</strong>: When session started</li>
<li><strong>Duration</strong>: Time elapsed (live updating)</li>
<li><strong>Visits</strong>: Number of visits recorded</li>
<li><strong>Status</strong>: ACTIVE badge</li>
</ul>
<p><strong>Step 3: View Session Details</strong></p>
<p>Click session row to view:</p>
<ul>
<li><strong>Volunteer GPS Location</strong>: Last known position on map</li>
<li><strong>Route</strong>: Walking route polyline</li>
<li><strong>Visited Addresses</strong>: Green markers</li>
<li><strong>Unvisited Addresses</strong>: Blue markers</li>
<li><strong>Recent Visits</strong>: Last 10 visits with outcomes</li>
</ul>
<h3 id="monitoring-canvass-activity">Monitoring Canvass Activity<a class="headerlink" href="#monitoring-canvass-activity" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: View Activity Feed</strong></p>
<p><strong>Recent Activity</strong> section displays:</p>
<ul>
<li><strong>Volunteer Name</strong>: Who recorded visit</li>
<li><strong>Address</strong>: Location visited</li>
<li><strong>Outcome</strong>: Visit result (icon + label)</li>
<li><strong>Support Level</strong>: Updated support level (color-coded)</li>
<li><strong>Time Ago</strong>: "5 minutes ago"</li>
</ul>
<p><strong>Step 2: Filter Activity</strong></p>
<p>Use filters:</p>
<ul>
<li><strong>Date Range</strong>: Last hour / day / week / month</li>
<li><strong>Outcome</strong>: Filter by specific outcome type</li>
<li><strong>Volunteer</strong>: Filter by volunteer name</li>
<li><strong>Cut</strong>: Filter by territory</li>
</ul>
<p><strong>Step 3: Export Activity</strong></p>
<p>Click <strong>Export CSV</strong> to download activity feed for reporting.</p>
<h3 id="tracking-cut-completion">Tracking Cut Completion<a class="headerlink" href="#tracking-cut-completion" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: View Cut Progress</strong></p>
<p><strong>Cut Progress</strong> card displays:</p>
<ul>
<li><strong>Cut Name</strong>: Territory name</li>
<li><strong>Total Addresses</strong>: Count of addresses in cut</li>
<li><strong>Visited</strong>: Count of addresses with CanvassVisit records</li>
<li><strong>Completion</strong>: Percentage (progress bar)</li>
<li><strong>Last Activity</strong>: Time since last visit</li>
</ul>
<p><strong>Step 2: View Detailed Progress</strong></p>
<p>Click cut row to view:</p>
<ul>
<li><strong>Address List</strong>: All addresses in cut with visit status</li>
<li><strong>Visit Heatmap</strong>: Map showing visited (green) vs unvisited (blue)</li>
<li><strong>Outcome Breakdown</strong>: Pie chart of visit outcomes</li>
<li><strong>Volunteer Breakdown</strong>: Who visited which addresses</li>
</ul>
<h3 id="volunteer-leaderboard">Volunteer Leaderboard<a class="headerlink" href="#volunteer-leaderboard" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: View Leaderboard</strong></p>
<p><strong>Leaderboard</strong> card displays:</p>
<ul>
<li><strong>Rank</strong>: 1<sup>st</sup>, 2<sup>nd</sup>, 3<sup>rd</sup> place</li>
<li><strong>Volunteer Name</strong>: Volunteer name</li>
<li><strong>Total Visits</strong>: Visit count</li>
<li><strong>Doors/Hour</strong>: Efficiency metric</li>
<li><strong>Top Outcome</strong>: Most common outcome</li>
</ul>
<p><strong>Step 2: Filter by Time Period</strong></p>
<p>Toggle time period:</p>
<ul>
<li><strong>Today</strong>: Visits since midnight</li>
<li><strong>This Week</strong>: Visits since Monday</li>
<li><strong>This Month</strong>: Visits since 1<sup>st</sup> of month</li>
<li><strong>All Time</strong>: Total visits</li>
</ul>
<h2 id="volunteer-workflow">Volunteer Workflow<a class="headerlink" href="#volunteer-workflow" title="Permanent link">&para;</a></h2>
<h3 id="starting-a-canvass-session">Starting a Canvass Session<a class="headerlink" href="#starting-a-canvass-session" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: Login</strong></p>
<p>Login at <code>/login</code> with volunteer account (or use TEMP account from shift signup).</p>
<p><strong>Step 2: View Assignments</strong></p>
<p>Navigate to <strong>Volunteer → My Assignments</strong>.</p>
<p><strong>Step 3: Select Shift</strong></p>
<p>Click <strong>Start Canvass</strong> on a shift with cut assignment.</p>
<p><strong>Step 4: Grant GPS Permission</strong></p>
<p>Browser requests geolocation permission. Click <strong>Allow</strong>.</p>
<p><strong>Step 5: Start Session</strong></p>
<p>System redirects to <code>/volunteer/canvass/:cutId</code> (full-screen map).</p>
<p>System will:</p>
<ol>
<li>Create CanvassSession (status=ACTIVE)</li>
<li>Create TrackingSession (linked 1:1)</li>
<li>Load addresses within cut polygon</li>
<li>Calculate walking route from current GPS position</li>
<li>Start GPS auto-tracking (submit points every 10s)</li>
</ol>
<h3 id="following-walking-route">Following Walking Route<a class="headerlink" href="#following-walking-route" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: View Route on Map</strong></p>
<p>Map displays:</p>
<ul>
<li><strong>Blue Polyline</strong>: Optimized walking route</li>
<li><strong>Blue Markers</strong>: Unvisited addresses (ordered by route)</li>
<li><strong>Green Markers</strong>: Visited addresses</li>
<li><strong>Red Marker</strong>: Current GPS position (live updating)</li>
</ul>
<p><strong>Step 2: Navigate to First Address</strong></p>
<p>Follow route to nearest unvisited address. Route recalculates when you move.</p>
<p><strong>Step 3: View Address Details</strong></p>
<p>Tap marker to view:</p>
<ul>
<li><strong>Address</strong>: Street address + unit number</li>
<li><strong>Resident Name</strong>: First/Last name (if available)</li>
<li><strong>Support Level</strong>: Previous support level (if available)</li>
<li><strong>Last Visit</strong>: Previous visit outcome + date (if applicable)</li>
</ul>
<h3 id="recording-a-visit">Recording a Visit<a class="headerlink" href="#recording-a-visit" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: Knock on Door</strong></p>
<p>Approach address and knock/ring doorbell.</p>
<p><strong>Step 2: Open Visit Recording Form</strong></p>
<p>Tap <strong>Record Visit</strong> button in bottom toolbar. Bottom sheet slides up.</p>
<p><strong>Step 3: Select Outcome</strong></p>
<p>Choose visit outcome:</p>
<ul>
<li><strong>Not Home</strong>: Nobody answered</li>
<li><strong>Refused</strong>: Refused to speak</li>
<li><strong>Moved</strong>: Resident moved away</li>
<li><strong>Already Voted</strong>: Already voted (GOTV campaigns)</li>
<li><strong>Spoke With</strong>: Had conversation</li>
<li><strong>Left Literature</strong>: Left campaign material</li>
<li><strong>Come Back Later</strong>: Asked to return later</li>
</ul>
<p><strong>Step 4: Update Support Level (if applicable)</strong></p>
<p>For "Spoke With" outcome, select support level:</p>
<ul>
<li><strong>Level 1 (Strong)</strong>: Green badge</li>
<li><strong>Level 2 (Leaning)</strong>: Yellow badge</li>
<li><strong>Level 3 (Undecided)</strong>: Gray badge</li>
<li><strong>Level 4 (Opposed)</strong>: Red badge</li>
</ul>
<p><strong>Step 5: Sign Request (optional)</strong></p>
<p>Toggle <strong>Sign Requested</strong> if resident wants lawn/window sign.</p>
<p><strong>Step 6: Add Notes (optional)</strong></p>
<p>Enter free-text notes (e.g., "Asked about healthcare policy", "Concerned about taxes").</p>
<p><strong>Step 7: Save Visit</strong></p>
<p>Tap <strong>Save Visit</strong>. System will:</p>
<ol>
<li>Create CanvassVisit record with outcome + timestamp</li>
<li>Update Address with new support level + sign status + notes</li>
<li>Increment session.totalVisits count</li>
<li>Update cut.completionPercentage</li>
<li>Create LocationHistory audit record</li>
<li>Submit GPS trackpoint with eventType=VISIT_RECORDED</li>
<li>Update marker to green (visited)</li>
<li>Recalculate walking route (exclude visited address)</li>
</ol>
<h3 id="ending-a-canvass-session">Ending a Canvass Session<a class="headerlink" href="#ending-a-canvass-session" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: Finish Route</strong></p>
<p>Complete visits for all addresses (or as many as possible).</p>
<p><strong>Step 2: End Session</strong></p>
<p>Tap <strong>End Session</strong> button in header.</p>
<p><strong>Step 3: Confirm</strong></p>
<p>Confirmation modal displays session summary:</p>
<ul>
<li><strong>Duration</strong>: Total session time</li>
<li><strong>Visits</strong>: Number of visits recorded</li>
<li><strong>Distance</strong>: Total distance walked (from GPS tracking)</li>
<li><strong>Doors/Hour</strong>: Efficiency metric</li>
</ul>
<p><strong>Step 4: Submit</strong></p>
<p>Tap <strong>End Session</strong>. System will:</p>
<ol>
<li>Update CanvassSession (status=COMPLETED, endedAt=now)</li>
<li>End TrackingSession (isActive=false, endedAt=now)</li>
<li>Calculate final stats (totalVisits, totalDistanceM)</li>
<li>Redirect to <code>/volunteer/activity</code> (visit history page)</li>
</ol>
<h3 id="viewing-visit-history">Viewing Visit History<a class="headerlink" href="#viewing-visit-history" title="Permanent link">&para;</a></h3>
<p><strong>Step 1: Navigate to My Activity</strong></p>
<p>Navigate to <strong>Volunteer → My Activity</strong>.</p>
<p><strong>Step 2: View Visit List</strong></p>
<p>Table displays:</p>
<ul>
<li><strong>Address</strong>: Location visited</li>
<li><strong>Outcome</strong>: Visit result (icon + label)</li>
<li><strong>Support Level</strong>: Updated support level</li>
<li><strong>Visit Date</strong>: Formatted date/time</li>
<li><strong>Notes</strong>: Canvassing notes (truncated)</li>
</ul>
<p><strong>Step 3: Filter Visits</strong></p>
<p>Use filters:</p>
<ul>
<li><strong>Date Range</strong>: Last week / month / all time</li>
<li><strong>Outcome</strong>: Filter by specific outcome</li>
<li><strong>Support Level</strong>: Filter by support level</li>
</ul>
<p><strong>Step 4: View Session History</strong></p>
<p>Navigate to <strong>My Routes</strong> to view:</p>
<ul>
<li><strong>Session List</strong>: Past canvass sessions</li>
<li><strong>Session Map</strong>: GPS route polyline + visited markers</li>
<li><strong>Session Stats</strong>: Duration, visits, distance, doors/hour</li>
</ul>
<h2 id="code-examples">Code Examples<a class="headerlink" href="#code-examples" title="Permanent link">&para;</a></h2>
<h3 id="start-canvass-session-backend">Start Canvass Session (Backend)<a class="headerlink" href="#start-canvass-session-backend" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="c1">// api/src/modules/map/canvass/canvass.service.ts</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a><span class="k">async</span><span class="w"> </span><span class="nx">startSession</span><span class="p">(</span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">StartSessionInput</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">cutId</span><span class="p">,</span><span class="w"> </span><span class="nx">shiftId</span><span class="p">,</span><span class="w"> </span><span class="nx">startLat</span><span class="p">,</span><span class="w"> </span><span class="nx">startLng</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">data</span><span class="p">;</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a><span class="w"> </span><span class="c1">// Check for existing active session</span>
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">findFirst</span><span class="p">({</span>
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">userId</span><span class="p">,</span><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.ACTIVE</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a>
</span><span id="__span-3-10"><a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">existing</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">AppError</span><span class="p">(</span><span class="mf">400</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Already have an active session&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;SESSION_ACTIVE&#39;</span><span class="p">);</span>
</span><span id="__span-3-12"><a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-3-13"><a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a>
</span><span id="__span-3-14"><a id="__codelineno-3-14" name="__codelineno-3-14" href="#__codelineno-3-14"></a><span class="w"> </span><span class="c1">// Create session + tracking session in transaction</span>
</span><span id="__span-3-15"><a id="__codelineno-3-15" name="__codelineno-3-15" href="#__codelineno-3-15"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">$transaction</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">tx</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-16"><a id="__codelineno-3-16" name="__codelineno-3-16" href="#__codelineno-3-16"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">canvassSession</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-3-17"><a id="__codelineno-3-17" name="__codelineno-3-17" href="#__codelineno-3-17"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-18"><a id="__codelineno-3-18" name="__codelineno-3-18" href="#__codelineno-3-18"></a><span class="w"> </span><span class="nx">userId</span><span class="p">,</span>
</span><span id="__span-3-19"><a id="__codelineno-3-19" name="__codelineno-3-19" href="#__codelineno-3-19"></a><span class="w"> </span><span class="nx">cutId</span><span class="p">,</span>
</span><span id="__span-3-20"><a id="__codelineno-3-20" name="__codelineno-3-20" href="#__codelineno-3-20"></a><span class="w"> </span><span class="nx">shiftId</span><span class="p">,</span>
</span><span id="__span-3-21"><a id="__codelineno-3-21" name="__codelineno-3-21" href="#__codelineno-3-21"></a><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.ACTIVE</span><span class="p">,</span>
</span><span id="__span-3-22"><a id="__codelineno-3-22" name="__codelineno-3-22" href="#__codelineno-3-22"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-3-23"><a id="__codelineno-3-23" name="__codelineno-3-23" href="#__codelineno-3-23"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-3-24"><a id="__codelineno-3-24" name="__codelineno-3-24" href="#__codelineno-3-24"></a>
</span><span id="__span-3-25"><a id="__codelineno-3-25" name="__codelineno-3-25" href="#__codelineno-3-25"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">startLat</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">startLng</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-26"><a id="__codelineno-3-26" name="__codelineno-3-26" href="#__codelineno-3-26"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">trackingSession</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-3-27"><a id="__codelineno-3-27" name="__codelineno-3-27" href="#__codelineno-3-27"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-28"><a id="__codelineno-3-28" name="__codelineno-3-28" href="#__codelineno-3-28"></a><span class="w"> </span><span class="nx">userId</span><span class="p">,</span>
</span><span id="__span-3-29"><a id="__codelineno-3-29" name="__codelineno-3-29" href="#__codelineno-3-29"></a><span class="w"> </span><span class="nx">canvassSessionId</span><span class="o">:</span><span class="w"> </span><span class="kt">canvassSession.id</span><span class="p">,</span>
</span><span id="__span-3-30"><a id="__codelineno-3-30" name="__codelineno-3-30" href="#__codelineno-3-30"></a><span class="w"> </span><span class="nx">lastLatitude</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nx">Prisma</span><span class="p">.</span><span class="nx">Decimal</span><span class="p">(</span><span class="nx">startLat</span><span class="p">),</span>
</span><span id="__span-3-31"><a id="__codelineno-3-31" name="__codelineno-3-31" href="#__codelineno-3-31"></a><span class="w"> </span><span class="nx">lastLongitude</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nx">Prisma</span><span class="p">.</span><span class="nx">Decimal</span><span class="p">(</span><span class="nx">startLng</span><span class="p">),</span>
</span><span id="__span-3-32"><a id="__codelineno-3-32" name="__codelineno-3-32" href="#__codelineno-3-32"></a><span class="w"> </span><span class="nx">lastRecordedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">(),</span>
</span><span id="__span-3-33"><a id="__codelineno-3-33" name="__codelineno-3-33" href="#__codelineno-3-33"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-3-34"><a id="__codelineno-3-34" name="__codelineno-3-34" href="#__codelineno-3-34"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-3-35"><a id="__codelineno-3-35" name="__codelineno-3-35" href="#__codelineno-3-35"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-3-36"><a id="__codelineno-3-36" name="__codelineno-3-36" href="#__codelineno-3-36"></a>
</span><span id="__span-3-37"><a id="__codelineno-3-37" name="__codelineno-3-37" href="#__codelineno-3-37"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">canvassSession</span><span class="p">;</span>
</span><span id="__span-3-38"><a id="__codelineno-3-38" name="__codelineno-3-38" href="#__codelineno-3-38"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-3-39"><a id="__codelineno-3-39" name="__codelineno-3-39" href="#__codelineno-3-39"></a>
</span><span id="__span-3-40"><a id="__codelineno-3-40" name="__codelineno-3-40" href="#__codelineno-3-40"></a><span class="w"> </span><span class="nx">setActiveCanvassSessions</span><span class="p">(</span>
</span><span id="__span-3-41"><a id="__codelineno-3-41" name="__codelineno-3-41" href="#__codelineno-3-41"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">count</span><span class="p">({</span>
</span><span id="__span-3-42"><a id="__codelineno-3-42" name="__codelineno-3-42" href="#__codelineno-3-42"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.ACTIVE</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-3-43"><a id="__codelineno-3-43" name="__codelineno-3-43" href="#__codelineno-3-43"></a><span class="w"> </span><span class="p">})</span>
</span><span id="__span-3-44"><a id="__codelineno-3-44" name="__codelineno-3-44" href="#__codelineno-3-44"></a><span class="w"> </span><span class="p">);</span>
</span><span id="__span-3-45"><a id="__codelineno-3-45" name="__codelineno-3-45" href="#__codelineno-3-45"></a>
</span><span id="__span-3-46"><a id="__codelineno-3-46" name="__codelineno-3-46" href="#__codelineno-3-46"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">session</span><span class="p">;</span>
</span><span id="__span-3-47"><a id="__codelineno-3-47" name="__codelineno-3-47" href="#__codelineno-3-47"></a><span class="p">}</span>
</span></code></pre></div>
<h3 id="calculate-walking-route-backend">Calculate Walking Route (Backend)<a class="headerlink" href="#calculate-walking-route-backend" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="c1">// api/src/modules/map/canvass/canvass-route.service.ts</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">calculateWalkingRoute</span><span class="p">(</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span><span class="nx">locations</span><span class="o">:</span><span class="w"> </span><span class="kt">RouteLocation</span><span class="p">[],</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span><span class="nx">startLat?</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">,</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span><span class="nx">startLng?</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">,</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span><span class="nx">cutGeojson?</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="nx">RouteResult</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">locations</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9" href="#__codelineno-4-9"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">orderedLocations</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nx">totalDistanceMeters</span><span class="o">:</span><span class="w"> </span><span class="kt">0</span><span class="p">,</span><span class="w"> </span><span class="nx">estimatedMinutes</span><span class="o">:</span><span class="w"> </span><span class="kt">0</span><span class="w"> </span><span class="p">};</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10" href="#__codelineno-4-10"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11" href="#__codelineno-4-11"></a>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12" href="#__codelineno-4-12"></a><span class="w"> </span><span class="c1">// Determine starting point</span>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13" href="#__codelineno-4-13"></a><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">currentLat</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
</span><span id="__span-4-14"><a id="__codelineno-4-14" name="__codelineno-4-14" href="#__codelineno-4-14"></a><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">currentLng</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">;</span>
</span><span id="__span-4-15"><a id="__codelineno-4-15" name="__codelineno-4-15" href="#__codelineno-4-15"></a>
</span><span id="__span-4-16"><a id="__codelineno-4-16" name="__codelineno-4-16" href="#__codelineno-4-16"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">startLat</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="kc">undefined</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">startLng</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="kc">undefined</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17" href="#__codelineno-4-17"></a><span class="w"> </span><span class="nx">currentLat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">startLat</span><span class="p">;</span>
</span><span id="__span-4-18"><a id="__codelineno-4-18" name="__codelineno-4-18" href="#__codelineno-4-18"></a><span class="w"> </span><span class="nx">currentLng</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">startLng</span><span class="p">;</span>
</span><span id="__span-4-19"><a id="__codelineno-4-19" name="__codelineno-4-19" href="#__codelineno-4-19"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">cutGeojson</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-20"><a id="__codelineno-4-20" name="__codelineno-4-20" href="#__codelineno-4-20"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">polygons</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">parseGeoJsonPolygon</span><span class="p">(</span><span class="nx">cutGeojson</span><span class="p">);</span>
</span><span id="__span-4-21"><a id="__codelineno-4-21" name="__codelineno-4-21" href="#__codelineno-4-21"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">centroid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">calculateCentroid</span><span class="p">(</span><span class="nx">polygons</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="o">!</span><span class="p">);</span>
</span><span id="__span-4-22"><a id="__codelineno-4-22" name="__codelineno-4-22" href="#__codelineno-4-22"></a><span class="w"> </span><span class="nx">currentLat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">centroid</span><span class="p">.</span><span class="nx">lat</span><span class="p">;</span>
</span><span id="__span-4-23"><a id="__codelineno-4-23" name="__codelineno-4-23" href="#__codelineno-4-23"></a><span class="w"> </span><span class="nx">currentLng</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">centroid</span><span class="p">.</span><span class="nx">lng</span><span class="p">;</span>
</span><span id="__span-4-24"><a id="__codelineno-4-24" name="__codelineno-4-24" href="#__codelineno-4-24"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-25"><a id="__codelineno-4-25" name="__codelineno-4-25" href="#__codelineno-4-25"></a><span class="w"> </span><span class="c1">// Use first location as starting point</span>
</span><span id="__span-4-26"><a id="__codelineno-4-26" name="__codelineno-4-26" href="#__codelineno-4-26"></a><span class="w"> </span><span class="nx">currentLat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">locations</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="o">!</span><span class="p">.</span><span class="nx">latitude</span><span class="p">;</span>
</span><span id="__span-4-27"><a id="__codelineno-4-27" name="__codelineno-4-27" href="#__codelineno-4-27"></a><span class="w"> </span><span class="nx">currentLng</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">locations</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="o">!</span><span class="p">.</span><span class="nx">longitude</span><span class="p">;</span>
</span><span id="__span-4-28"><a id="__codelineno-4-28" name="__codelineno-4-28" href="#__codelineno-4-28"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-4-29"><a id="__codelineno-4-29" name="__codelineno-4-29" href="#__codelineno-4-29"></a>
</span><span id="__span-4-30"><a id="__codelineno-4-30" name="__codelineno-4-30" href="#__codelineno-4-30"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">remaining</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[...</span><span class="nx">locations</span><span class="p">];</span>
</span><span id="__span-4-31"><a id="__codelineno-4-31" name="__codelineno-4-31" href="#__codelineno-4-31"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">ordered</span><span class="o">:</span><span class="w"> </span><span class="kt">RouteLocation</span><span class="p">[]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[];</span>
</span><span id="__span-4-32"><a id="__codelineno-4-32" name="__codelineno-4-32" href="#__codelineno-4-32"></a><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">totalDistance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span>
</span><span id="__span-4-33"><a id="__codelineno-4-33" name="__codelineno-4-33" href="#__codelineno-4-33"></a>
</span><span id="__span-4-34"><a id="__codelineno-4-34" name="__codelineno-4-34" href="#__codelineno-4-34"></a><span class="w"> </span><span class="c1">// Nearest-neighbor algorithm</span>
</span><span id="__span-4-35"><a id="__codelineno-4-35" name="__codelineno-4-35" href="#__codelineno-4-35"></a><span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="nx">remaining</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-36"><a id="__codelineno-4-36" name="__codelineno-4-36" href="#__codelineno-4-36"></a><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">nearestIdx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span>
</span><span id="__span-4-37"><a id="__codelineno-4-37" name="__codelineno-4-37" href="#__codelineno-4-37"></a><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">nearestDist</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">Infinity</span><span class="p">;</span>
</span><span id="__span-4-38"><a id="__codelineno-4-38" name="__codelineno-4-38" href="#__codelineno-4-38"></a>
</span><span id="__span-4-39"><a id="__codelineno-4-39" name="__codelineno-4-39" href="#__codelineno-4-39"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">remaining</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-40"><a id="__codelineno-4-40" name="__codelineno-4-40" href="#__codelineno-4-40"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">loc</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">remaining</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span><span class="o">!</span><span class="p">;</span>
</span><span id="__span-4-41"><a id="__codelineno-4-41" name="__codelineno-4-41" href="#__codelineno-4-41"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">dist</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">haversineDistance</span><span class="p">(</span><span class="nx">currentLat</span><span class="p">,</span><span class="w"> </span><span class="nx">currentLng</span><span class="p">,</span><span class="w"> </span><span class="nx">loc</span><span class="p">.</span><span class="nx">latitude</span><span class="p">,</span><span class="w"> </span><span class="nx">loc</span><span class="p">.</span><span class="nx">longitude</span><span class="p">);</span>
</span><span id="__span-4-42"><a id="__codelineno-4-42" name="__codelineno-4-42" href="#__codelineno-4-42"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">dist</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="nx">nearestDist</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-43"><a id="__codelineno-4-43" name="__codelineno-4-43" href="#__codelineno-4-43"></a><span class="w"> </span><span class="nx">nearestDist</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">dist</span><span class="p">;</span>
</span><span id="__span-4-44"><a id="__codelineno-4-44" name="__codelineno-4-44" href="#__codelineno-4-44"></a><span class="w"> </span><span class="nx">nearestIdx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">i</span><span class="p">;</span>
</span><span id="__span-4-45"><a id="__codelineno-4-45" name="__codelineno-4-45" href="#__codelineno-4-45"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-4-46"><a id="__codelineno-4-46" name="__codelineno-4-46" href="#__codelineno-4-46"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-4-47"><a id="__codelineno-4-47" name="__codelineno-4-47" href="#__codelineno-4-47"></a>
</span><span id="__span-4-48"><a id="__codelineno-4-48" name="__codelineno-4-48" href="#__codelineno-4-48"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">nearest</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">remaining</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="nx">nearestIdx</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">)[</span><span class="mf">0</span><span class="p">]</span><span class="o">!</span><span class="p">;</span>
</span><span id="__span-4-49"><a id="__codelineno-4-49" name="__codelineno-4-49" href="#__codelineno-4-49"></a><span class="w"> </span><span class="nx">ordered</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">nearest</span><span class="p">);</span>
</span><span id="__span-4-50"><a id="__codelineno-4-50" name="__codelineno-4-50" href="#__codelineno-4-50"></a><span class="w"> </span><span class="nx">totalDistance</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">nearestDist</span><span class="p">;</span>
</span><span id="__span-4-51"><a id="__codelineno-4-51" name="__codelineno-4-51" href="#__codelineno-4-51"></a><span class="w"> </span><span class="nx">currentLat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">nearest</span><span class="p">.</span><span class="nx">latitude</span><span class="p">;</span>
</span><span id="__span-4-52"><a id="__codelineno-4-52" name="__codelineno-4-52" href="#__codelineno-4-52"></a><span class="w"> </span><span class="nx">currentLng</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">nearest</span><span class="p">.</span><span class="nx">longitude</span><span class="p">;</span>
</span><span id="__span-4-53"><a id="__codelineno-4-53" name="__codelineno-4-53" href="#__codelineno-4-53"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-4-54"><a id="__codelineno-4-54" name="__codelineno-4-54" href="#__codelineno-4-54"></a>
</span><span id="__span-4-55"><a id="__codelineno-4-55" name="__codelineno-4-55" href="#__codelineno-4-55"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">WALKING_SPEED_MPS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">5000</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">60</span><span class="p">;</span><span class="w"> </span><span class="c1">// 5 km/h in m/min</span>
</span><span id="__span-4-56"><a id="__codelineno-4-56" name="__codelineno-4-56" href="#__codelineno-4-56"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">MINUTES_PER_DOOR</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">2</span><span class="p">;</span>
</span><span id="__span-4-57"><a id="__codelineno-4-57" name="__codelineno-4-57" href="#__codelineno-4-57"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">walkingMinutes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">totalDistance</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">WALKING_SPEED_MPS</span><span class="p">;</span>
</span><span id="__span-4-58"><a id="__codelineno-4-58" name="__codelineno-4-58" href="#__codelineno-4-58"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">doorMinutes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">ordered</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">MINUTES_PER_DOOR</span><span class="p">;</span>
</span><span id="__span-4-59"><a id="__codelineno-4-59" name="__codelineno-4-59" href="#__codelineno-4-59"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">estimatedMinutes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">round</span><span class="p">(</span><span class="nx">walkingMinutes</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">doorMinutes</span><span class="p">);</span>
</span><span id="__span-4-60"><a id="__codelineno-4-60" name="__codelineno-4-60" href="#__codelineno-4-60"></a>
</span><span id="__span-4-61"><a id="__codelineno-4-61" name="__codelineno-4-61" href="#__codelineno-4-61"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-62"><a id="__codelineno-4-62" name="__codelineno-4-62" href="#__codelineno-4-62"></a><span class="w"> </span><span class="nx">orderedLocations</span><span class="o">:</span><span class="w"> </span><span class="kt">ordered</span><span class="p">,</span>
</span><span id="__span-4-63"><a id="__codelineno-4-63" name="__codelineno-4-63" href="#__codelineno-4-63"></a><span class="w"> </span><span class="nx">totalDistanceMeters</span><span class="o">:</span><span class="w"> </span><span class="kt">Math.round</span><span class="p">(</span><span class="nx">totalDistance</span><span class="p">),</span>
</span><span id="__span-4-64"><a id="__codelineno-4-64" name="__codelineno-4-64" href="#__codelineno-4-64"></a><span class="w"> </span><span class="nx">estimatedMinutes</span><span class="p">,</span>
</span><span id="__span-4-65"><a id="__codelineno-4-65" name="__codelineno-4-65" href="#__codelineno-4-65"></a><span class="w"> </span><span class="p">};</span>
</span><span id="__span-4-66"><a id="__codelineno-4-66" name="__codelineno-4-66" href="#__codelineno-4-66"></a><span class="p">}</span>
</span></code></pre></div>
<h3 id="record-visit-backend">Record Visit (Backend)<a class="headerlink" href="#record-visit-backend" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="c1">// api/src/modules/map/canvass/canvass.service.ts</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a><span class="k">async</span><span class="w"> </span><span class="nx">recordVisit</span><span class="p">(</span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">RecordVisitInput</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sessionId</span><span class="p">,</span><span class="w"> </span><span class="nx">addressId</span><span class="p">,</span><span class="w"> </span><span class="nx">outcome</span><span class="p">,</span><span class="w"> </span><span class="nx">supportLevel</span><span class="p">,</span><span class="w"> </span><span class="nx">signRequested</span><span class="p">,</span><span class="w"> </span><span class="nx">notes</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">data</span><span class="p">;</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a><span class="w"> </span><span class="c1">// Verify session ownership and active status</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">findFirst</span><span class="p">({</span>
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">sessionId</span><span class="p">,</span><span class="w"> </span><span class="nx">userId</span><span class="p">,</span><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.ACTIVE</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-8"><a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-9"><a id="__codelineno-5-9" name="__codelineno-5-9" href="#__codelineno-5-9"></a>
</span><span id="__span-5-10"><a id="__codelineno-5-10" name="__codelineno-5-10" href="#__codelineno-5-10"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">session</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-11"><a id="__codelineno-5-11" name="__codelineno-5-11" href="#__codelineno-5-11"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">AppError</span><span class="p">(</span><span class="mf">404</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Active session not found&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;SESSION_NOT_FOUND&#39;</span><span class="p">);</span>
</span><span id="__span-5-12"><a id="__codelineno-5-12" name="__codelineno-5-12" href="#__codelineno-5-12"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-5-13"><a id="__codelineno-5-13" name="__codelineno-5-13" href="#__codelineno-5-13"></a>
</span><span id="__span-5-14"><a id="__codelineno-5-14" name="__codelineno-5-14" href="#__codelineno-5-14"></a><span class="w"> </span><span class="c1">// Create visit + update address in transaction</span>
</span><span id="__span-5-15"><a id="__codelineno-5-15" name="__codelineno-5-15" href="#__codelineno-5-15"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">visit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">$transaction</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">tx</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-16"><a id="__codelineno-5-16" name="__codelineno-5-16" href="#__codelineno-5-16"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">canvassVisit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">canvassVisit</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-5-17"><a id="__codelineno-5-17" name="__codelineno-5-17" href="#__codelineno-5-17"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-18"><a id="__codelineno-5-18" name="__codelineno-5-18" href="#__codelineno-5-18"></a><span class="w"> </span><span class="nx">sessionId</span><span class="p">,</span>
</span><span id="__span-5-19"><a id="__codelineno-5-19" name="__codelineno-5-19" href="#__codelineno-5-19"></a><span class="w"> </span><span class="nx">userId</span><span class="p">,</span>
</span><span id="__span-5-20"><a id="__codelineno-5-20" name="__codelineno-5-20" href="#__codelineno-5-20"></a><span class="w"> </span><span class="nx">addressId</span><span class="p">,</span>
</span><span id="__span-5-21"><a id="__codelineno-5-21" name="__codelineno-5-21" href="#__codelineno-5-21"></a><span class="w"> </span><span class="nx">outcome</span><span class="p">,</span>
</span><span id="__span-5-22"><a id="__codelineno-5-22" name="__codelineno-5-22" href="#__codelineno-5-22"></a><span class="w"> </span><span class="nx">supportLevel</span><span class="p">,</span>
</span><span id="__span-5-23"><a id="__codelineno-5-23" name="__codelineno-5-23" href="#__codelineno-5-23"></a><span class="w"> </span><span class="nx">signRequested</span><span class="o">:</span><span class="w"> </span><span class="kt">signRequested</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
</span><span id="__span-5-24"><a id="__codelineno-5-24" name="__codelineno-5-24" href="#__codelineno-5-24"></a><span class="w"> </span><span class="nx">notes</span><span class="p">,</span>
</span><span id="__span-5-25"><a id="__codelineno-5-25" name="__codelineno-5-25" href="#__codelineno-5-25"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-26"><a id="__codelineno-5-26" name="__codelineno-5-26" href="#__codelineno-5-26"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-27"><a id="__codelineno-5-27" name="__codelineno-5-27" href="#__codelineno-5-27"></a>
</span><span id="__span-5-28"><a id="__codelineno-5-28" name="__codelineno-5-28" href="#__codelineno-5-28"></a><span class="w"> </span><span class="c1">// Update address with new data</span>
</span><span id="__span-5-29"><a id="__codelineno-5-29" name="__codelineno-5-29" href="#__codelineno-5-29"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">supportLevel</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">signRequested</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="kc">undefined</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">notes</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-30"><a id="__codelineno-5-30" name="__codelineno-5-30" href="#__codelineno-5-30"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">address</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span>
</span><span id="__span-5-31"><a id="__codelineno-5-31" name="__codelineno-5-31" href="#__codelineno-5-31"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">addressId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-32"><a id="__codelineno-5-32" name="__codelineno-5-32" href="#__codelineno-5-32"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-33"><a id="__codelineno-5-33" name="__codelineno-5-33" href="#__codelineno-5-33"></a><span class="w"> </span><span class="p">...(</span><span class="nx">supportLevel</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">supportLevel</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-5-34"><a id="__codelineno-5-34" name="__codelineno-5-34" href="#__codelineno-5-34"></a><span class="w"> </span><span class="p">...(</span><span class="nx">signRequested</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="kc">undefined</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sign</span><span class="o">:</span><span class="w"> </span><span class="kt">signRequested</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-5-35"><a id="__codelineno-5-35" name="__codelineno-5-35" href="#__codelineno-5-35"></a><span class="w"> </span><span class="p">...(</span><span class="nx">notes</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">notes</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-5-36"><a id="__codelineno-5-36" name="__codelineno-5-36" href="#__codelineno-5-36"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-37"><a id="__codelineno-5-37" name="__codelineno-5-37" href="#__codelineno-5-37"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-38"><a id="__codelineno-5-38" name="__codelineno-5-38" href="#__codelineno-5-38"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-5-39"><a id="__codelineno-5-39" name="__codelineno-5-39" href="#__codelineno-5-39"></a>
</span><span id="__span-5-40"><a id="__codelineno-5-40" name="__codelineno-5-40" href="#__codelineno-5-40"></a><span class="w"> </span><span class="c1">// Increment session visit count</span>
</span><span id="__span-5-41"><a id="__codelineno-5-41" name="__codelineno-5-41" href="#__codelineno-5-41"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span>
</span><span id="__span-5-42"><a id="__codelineno-5-42" name="__codelineno-5-42" href="#__codelineno-5-42"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">sessionId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-43"><a id="__codelineno-5-43" name="__codelineno-5-43" href="#__codelineno-5-43"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">totalVisits</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">increment</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-44"><a id="__codelineno-5-44" name="__codelineno-5-44" href="#__codelineno-5-44"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-45"><a id="__codelineno-5-45" name="__codelineno-5-45" href="#__codelineno-5-45"></a>
</span><span id="__span-5-46"><a id="__codelineno-5-46" name="__codelineno-5-46" href="#__codelineno-5-46"></a><span class="w"> </span><span class="c1">// Update cut completion percentage</span>
</span><span id="__span-5-47"><a id="__codelineno-5-47" name="__codelineno-5-47" href="#__codelineno-5-47"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">session</span><span class="p">.</span><span class="nx">cutId</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-48"><a id="__codelineno-5-48" name="__codelineno-5-48" href="#__codelineno-5-48"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">cutId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">session</span><span class="p">.</span><span class="nx">cutId</span><span class="p">;</span>
</span><span id="__span-5-49"><a id="__codelineno-5-49" name="__codelineno-5-49" href="#__codelineno-5-49"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">totalAddresses</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">address</span><span class="p">.</span><span class="nx">count</span><span class="p">({</span>
</span><span id="__span-5-50"><a id="__codelineno-5-50" name="__codelineno-5-50" href="#__codelineno-5-50"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-51"><a id="__codelineno-5-51" name="__codelineno-5-51" href="#__codelineno-5-51"></a><span class="w"> </span><span class="nx">location</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-52"><a id="__codelineno-5-52" name="__codelineno-5-52" href="#__codelineno-5-52"></a><span class="w"> </span><span class="c1">// Point-in-polygon query omitted for brevity</span>
</span><span id="__span-5-53"><a id="__codelineno-5-53" name="__codelineno-5-53" href="#__codelineno-5-53"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-54"><a id="__codelineno-5-54" name="__codelineno-5-54" href="#__codelineno-5-54"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-55"><a id="__codelineno-5-55" name="__codelineno-5-55" href="#__codelineno-5-55"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-56"><a id="__codelineno-5-56" name="__codelineno-5-56" href="#__codelineno-5-56"></a>
</span><span id="__span-5-57"><a id="__codelineno-5-57" name="__codelineno-5-57" href="#__codelineno-5-57"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">visitedAddresses</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">canvassVisit</span><span class="p">.</span><span class="nx">count</span><span class="p">({</span>
</span><span id="__span-5-58"><a id="__codelineno-5-58" name="__codelineno-5-58" href="#__codelineno-5-58"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-59"><a id="__codelineno-5-59" name="__codelineno-5-59" href="#__codelineno-5-59"></a><span class="w"> </span><span class="nx">session</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">cutId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-60"><a id="__codelineno-5-60" name="__codelineno-5-60" href="#__codelineno-5-60"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-61"><a id="__codelineno-5-61" name="__codelineno-5-61" href="#__codelineno-5-61"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-62"><a id="__codelineno-5-62" name="__codelineno-5-62" href="#__codelineno-5-62"></a>
</span><span id="__span-5-63"><a id="__codelineno-5-63" name="__codelineno-5-63" href="#__codelineno-5-63"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">completionPercentage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">round</span><span class="p">((</span><span class="nx">visitedAddresses</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">totalAddresses</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">100</span><span class="p">);</span>
</span><span id="__span-5-64"><a id="__codelineno-5-64" name="__codelineno-5-64" href="#__codelineno-5-64"></a>
</span><span id="__span-5-65"><a id="__codelineno-5-65" name="__codelineno-5-65" href="#__codelineno-5-65"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">cut</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span>
</span><span id="__span-5-66"><a id="__codelineno-5-66" name="__codelineno-5-66" href="#__codelineno-5-66"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">cutId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-67"><a id="__codelineno-5-67" name="__codelineno-5-67" href="#__codelineno-5-67"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">completionPercentage</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-68"><a id="__codelineno-5-68" name="__codelineno-5-68" href="#__codelineno-5-68"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-69"><a id="__codelineno-5-69" name="__codelineno-5-69" href="#__codelineno-5-69"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-5-70"><a id="__codelineno-5-70" name="__codelineno-5-70" href="#__codelineno-5-70"></a>
</span><span id="__span-5-71"><a id="__codelineno-5-71" name="__codelineno-5-71" href="#__codelineno-5-71"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">canvassVisit</span><span class="p">;</span>
</span><span id="__span-5-72"><a id="__codelineno-5-72" name="__codelineno-5-72" href="#__codelineno-5-72"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-5-73"><a id="__codelineno-5-73" name="__codelineno-5-73" href="#__codelineno-5-73"></a>
</span><span id="__span-5-74"><a id="__codelineno-5-74" name="__codelineno-5-74" href="#__codelineno-5-74"></a><span class="w"> </span><span class="nx">recordCanvassVisit</span><span class="p">(</span><span class="nx">outcome</span><span class="p">);</span>
</span><span id="__span-5-75"><a id="__codelineno-5-75" name="__codelineno-5-75" href="#__codelineno-5-75"></a>
</span><span id="__span-5-76"><a id="__codelineno-5-76" name="__codelineno-5-76" href="#__codelineno-5-76"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">visit</span><span class="p">;</span>
</span><span id="__span-5-77"><a id="__codelineno-5-77" name="__codelineno-5-77" href="#__codelineno-5-77"></a><span class="p">}</span>
</span></code></pre></div>
<h3 id="gps-auto-tracking-frontend">GPS Auto-Tracking (Frontend)<a class="headerlink" href="#gps-auto-tracking-frontend" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="c1">// admin/src/components/canvass/GPSTracker.tsx</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a><span class="nx">useEffect</span><span class="p">(()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">session</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="o">!</span><span class="nx">geolocationEnabled</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span>
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a>
</span><span id="__span-6-5"><a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">watchId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">navigator</span><span class="p">.</span><span class="nx">geolocation</span><span class="p">.</span><span class="nx">watchPosition</span><span class="p">(</span>
</span><span id="__span-6-6"><a id="__codelineno-6-6" name="__codelineno-6-6" href="#__codelineno-6-6"></a><span class="w"> </span><span class="p">(</span><span class="nx">position</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-7"><a id="__codelineno-6-7" name="__codelineno-6-7" href="#__codelineno-6-7"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">point</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-8"><a id="__codelineno-6-8" name="__codelineno-6-8" href="#__codelineno-6-8"></a><span class="w"> </span><span class="nx">latitude</span><span class="o">:</span><span class="w"> </span><span class="kt">position.coords.latitude</span><span class="p">,</span>
</span><span id="__span-6-9"><a id="__codelineno-6-9" name="__codelineno-6-9" href="#__codelineno-6-9"></a><span class="w"> </span><span class="nx">longitude</span><span class="o">:</span><span class="w"> </span><span class="kt">position.coords.longitude</span><span class="p">,</span>
</span><span id="__span-6-10"><a id="__codelineno-6-10" name="__codelineno-6-10" href="#__codelineno-6-10"></a><span class="w"> </span><span class="nx">accuracy</span><span class="o">:</span><span class="w"> </span><span class="kt">position.coords.accuracy</span><span class="p">,</span>
</span><span id="__span-6-11"><a id="__codelineno-6-11" name="__codelineno-6-11" href="#__codelineno-6-11"></a><span class="w"> </span><span class="nx">recordedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">(),</span>
</span><span id="__span-6-12"><a id="__codelineno-6-12" name="__codelineno-6-12" href="#__codelineno-6-12"></a><span class="w"> </span><span class="p">};</span>
</span><span id="__span-6-13"><a id="__codelineno-6-13" name="__codelineno-6-13" href="#__codelineno-6-13"></a>
</span><span id="__span-6-14"><a id="__codelineno-6-14" name="__codelineno-6-14" href="#__codelineno-6-14"></a><span class="w"> </span><span class="c1">// Add to local buffer</span>
</span><span id="__span-6-15"><a id="__codelineno-6-15" name="__codelineno-6-15" href="#__codelineno-6-15"></a><span class="w"> </span><span class="nx">setPointsBuffer</span><span class="p">((</span><span class="nx">prev</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">[...</span><span class="nx">prev</span><span class="p">,</span><span class="w"> </span><span class="nx">point</span><span class="p">]);</span>
</span><span id="__span-6-16"><a id="__codelineno-6-16" name="__codelineno-6-16" href="#__codelineno-6-16"></a>
</span><span id="__span-6-17"><a id="__codelineno-6-17" name="__codelineno-6-17" href="#__codelineno-6-17"></a><span class="w"> </span><span class="c1">// Update current position</span>
</span><span id="__span-6-18"><a id="__codelineno-6-18" name="__codelineno-6-18" href="#__codelineno-6-18"></a><span class="w"> </span><span class="nx">setCurrentPosition</span><span class="p">([</span><span class="nx">point</span><span class="p">.</span><span class="nx">latitude</span><span class="p">,</span><span class="w"> </span><span class="nx">point</span><span class="p">.</span><span class="nx">longitude</span><span class="p">]);</span>
</span><span id="__span-6-19"><a id="__codelineno-6-19" name="__codelineno-6-19" href="#__codelineno-6-19"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-6-20"><a id="__codelineno-6-20" name="__codelineno-6-20" href="#__codelineno-6-20"></a><span class="w"> </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-21"><a id="__codelineno-6-21" name="__codelineno-6-21" href="#__codelineno-6-21"></a><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;GPS error:&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">);</span>
</span><span id="__span-6-22"><a id="__codelineno-6-22" name="__codelineno-6-22" href="#__codelineno-6-22"></a><span class="w"> </span><span class="nx">message</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;GPS tracking failed&#39;</span><span class="p">);</span>
</span><span id="__span-6-23"><a id="__codelineno-6-23" name="__codelineno-6-23" href="#__codelineno-6-23"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-6-24"><a id="__codelineno-6-24" name="__codelineno-6-24" href="#__codelineno-6-24"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-25"><a id="__codelineno-6-25" name="__codelineno-6-25" href="#__codelineno-6-25"></a><span class="w"> </span><span class="nx">enableHighAccuracy</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-6-26"><a id="__codelineno-6-26" name="__codelineno-6-26" href="#__codelineno-6-26"></a><span class="w"> </span><span class="nx">maximumAge</span><span class="o">:</span><span class="w"> </span><span class="kt">0</span><span class="p">,</span>
</span><span id="__span-6-27"><a id="__codelineno-6-27" name="__codelineno-6-27" href="#__codelineno-6-27"></a><span class="w"> </span><span class="nx">timeout</span><span class="o">:</span><span class="w"> </span><span class="kt">10000</span><span class="p">,</span>
</span><span id="__span-6-28"><a id="__codelineno-6-28" name="__codelineno-6-28" href="#__codelineno-6-28"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-6-29"><a id="__codelineno-6-29" name="__codelineno-6-29" href="#__codelineno-6-29"></a><span class="w"> </span><span class="p">);</span>
</span><span id="__span-6-30"><a id="__codelineno-6-30" name="__codelineno-6-30" href="#__codelineno-6-30"></a>
</span><span id="__span-6-31"><a id="__codelineno-6-31" name="__codelineno-6-31" href="#__codelineno-6-31"></a><span class="w"> </span><span class="c1">// Submit buffered points every 10 seconds</span>
</span><span id="__span-6-32"><a id="__codelineno-6-32" name="__codelineno-6-32" href="#__codelineno-6-32"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">setInterval</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-33"><a id="__codelineno-6-33" name="__codelineno-6-33" href="#__codelineno-6-33"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">pointsBuffer</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span>
</span><span id="__span-6-34"><a id="__codelineno-6-34" name="__codelineno-6-34" href="#__codelineno-6-34"></a>
</span><span id="__span-6-35"><a id="__codelineno-6-35" name="__codelineno-6-35" href="#__codelineno-6-35"></a><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-36"><a id="__codelineno-6-36" name="__codelineno-6-36" href="#__codelineno-6-36"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">api</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="sb">`/map/tracking/sessions/</span><span class="si">${</span><span class="nx">trackingSessionId</span><span class="si">}</span><span class="sb">/points`</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-37"><a id="__codelineno-6-37" name="__codelineno-6-37" href="#__codelineno-6-37"></a><span class="w"> </span><span class="nx">points</span><span class="o">:</span><span class="w"> </span><span class="kt">pointsBuffer</span><span class="p">,</span>
</span><span id="__span-6-38"><a id="__codelineno-6-38" name="__codelineno-6-38" href="#__codelineno-6-38"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-6-39"><a id="__codelineno-6-39" name="__codelineno-6-39" href="#__codelineno-6-39"></a>
</span><span id="__span-6-40"><a id="__codelineno-6-40" name="__codelineno-6-40" href="#__codelineno-6-40"></a><span class="w"> </span><span class="nx">setPointsBuffer</span><span class="p">([]);</span>
</span><span id="__span-6-41"><a id="__codelineno-6-41" name="__codelineno-6-41" href="#__codelineno-6-41"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-42"><a id="__codelineno-6-42" name="__codelineno-6-42" href="#__codelineno-6-42"></a><span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Failed to submit GPS points:&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="p">);</span>
</span><span id="__span-6-43"><a id="__codelineno-6-43" name="__codelineno-6-43" href="#__codelineno-6-43"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-6-44"><a id="__codelineno-6-44" name="__codelineno-6-44" href="#__codelineno-6-44"></a><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="mf">10000</span><span class="p">);</span>
</span><span id="__span-6-45"><a id="__codelineno-6-45" name="__codelineno-6-45" href="#__codelineno-6-45"></a>
</span><span id="__span-6-46"><a id="__codelineno-6-46" name="__codelineno-6-46" href="#__codelineno-6-46"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-6-47"><a id="__codelineno-6-47" name="__codelineno-6-47" href="#__codelineno-6-47"></a><span class="w"> </span><span class="nx">navigator</span><span class="p">.</span><span class="nx">geolocation</span><span class="p">.</span><span class="nx">clearWatch</span><span class="p">(</span><span class="nx">watchId</span><span class="p">);</span>
</span><span id="__span-6-48"><a id="__codelineno-6-48" name="__codelineno-6-48" href="#__codelineno-6-48"></a><span class="w"> </span><span class="nx">clearInterval</span><span class="p">(</span><span class="nx">interval</span><span class="p">);</span>
</span><span id="__span-6-49"><a id="__codelineno-6-49" name="__codelineno-6-49" href="#__codelineno-6-49"></a><span class="w"> </span><span class="p">};</span>
</span><span id="__span-6-50"><a id="__codelineno-6-50" name="__codelineno-6-50" href="#__codelineno-6-50"></a><span class="p">},</span><span class="w"> </span><span class="p">[</span><span class="nx">session</span><span class="p">,</span><span class="w"> </span><span class="nx">geolocationEnabled</span><span class="p">,</span><span class="w"> </span><span class="nx">pointsBuffer</span><span class="p">,</span><span class="w"> </span><span class="nx">trackingSessionId</span><span class="p">]);</span>
</span></code></pre></div>
<h3 id="visit-recording-form-frontend">Visit Recording Form (Frontend)<a class="headerlink" href="#visit-recording-form-frontend" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="c1">// admin/src/components/canvass/VisitRecordingForm.tsx</span>
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">handleSubmit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">values</span><span class="o">:</span><span class="w"> </span><span class="kt">any</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">api</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;/map/canvass/visits&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a><span class="w"> </span><span class="nx">sessionId</span><span class="o">:</span><span class="w"> </span><span class="kt">session.id</span><span class="p">,</span>
</span><span id="__span-7-6"><a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a><span class="w"> </span><span class="nx">addressId</span><span class="o">:</span><span class="w"> </span><span class="kt">selectedAddress.id</span><span class="p">,</span>
</span><span id="__span-7-7"><a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a><span class="w"> </span><span class="nx">outcome</span><span class="o">:</span><span class="w"> </span><span class="kt">values.outcome</span><span class="p">,</span>
</span><span id="__span-7-8"><a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a><span class="w"> </span><span class="nx">supportLevel</span><span class="o">:</span><span class="w"> </span><span class="kt">values.supportLevel</span><span class="p">,</span>
</span><span id="__span-7-9"><a id="__codelineno-7-9" name="__codelineno-7-9" href="#__codelineno-7-9"></a><span class="w"> </span><span class="nx">signRequested</span><span class="o">:</span><span class="w"> </span><span class="kt">values.signRequested</span><span class="p">,</span>
</span><span id="__span-7-10"><a id="__codelineno-7-10" name="__codelineno-7-10" href="#__codelineno-7-10"></a><span class="w"> </span><span class="nx">notes</span><span class="o">:</span><span class="w"> </span><span class="kt">values.notes</span><span class="p">,</span>
</span><span id="__span-7-11"><a id="__codelineno-7-11" name="__codelineno-7-11" href="#__codelineno-7-11"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-7-12"><a id="__codelineno-7-12" name="__codelineno-7-12" href="#__codelineno-7-12"></a>
</span><span id="__span-7-13"><a id="__codelineno-7-13" name="__codelineno-7-13" href="#__codelineno-7-13"></a><span class="w"> </span><span class="nx">message</span><span class="p">.</span><span class="nx">success</span><span class="p">(</span><span class="s1">&#39;Visit recorded&#39;</span><span class="p">);</span>
</span><span id="__span-7-14"><a id="__codelineno-7-14" name="__codelineno-7-14" href="#__codelineno-7-14"></a><span class="w"> </span><span class="nx">form</span><span class="p">.</span><span class="nx">resetFields</span><span class="p">();</span>
</span><span id="__span-7-15"><a id="__codelineno-7-15" name="__codelineno-7-15" href="#__codelineno-7-15"></a><span class="w"> </span><span class="nx">onVisitRecorded</span><span class="p">();</span>
</span><span id="__span-7-16"><a id="__codelineno-7-16" name="__codelineno-7-16" href="#__codelineno-7-16"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-17"><a id="__codelineno-7-17" name="__codelineno-7-17" href="#__codelineno-7-17"></a><span class="w"> </span><span class="nx">message</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Failed to record visit&#39;</span><span class="p">);</span>
</span><span id="__span-7-18"><a id="__codelineno-7-18" name="__codelineno-7-18" href="#__codelineno-7-18"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-7-19"><a id="__codelineno-7-19" name="__codelineno-7-19" href="#__codelineno-7-19"></a><span class="p">};</span>
</span></code></pre></div>
<h2 id="troubleshooting">Troubleshooting<a class="headerlink" href="#troubleshooting" title="Permanent link">&para;</a></h2>
<h3 id="issue-walking-route-not-optimal">Issue: Walking Route Not Optimal<a class="headerlink" href="#issue-walking-route-not-optimal" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong></p>
<ul>
<li>Route backtracks frequently</li>
<li>Total distance much longer than expected</li>
<li>Route doesn't start from volunteer GPS position</li>
</ul>
<p><strong>Causes:</strong></p>
<ul>
<li>Nearest-neighbor algorithm is greedy (not globally optimal)</li>
<li>Starting position not provided (defaults to cut centroid)</li>
<li>GPS accuracy poor (volunteer position inaccurate)</li>
</ul>
<p><strong>Solutions:</strong></p>
<ol>
<li><strong>Use volunteer GPS position as start</strong>:</li>
</ol>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="c1">// Always pass volunteer GPS position to route calculation</span>
</span><span id="__span-8-2"><a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">route</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">calculateWalkingRoute</span><span class="p">(</span>
</span><span id="__span-8-3"><a id="__codelineno-8-3" name="__codelineno-8-3" href="#__codelineno-8-3"></a><span class="w"> </span><span class="nx">locations</span><span class="p">,</span>
</span><span id="__span-8-4"><a id="__codelineno-8-4" name="__codelineno-8-4" href="#__codelineno-8-4"></a><span class="w"> </span><span class="nx">currentLat</span><span class="p">,</span>
</span><span id="__span-8-5"><a id="__codelineno-8-5" name="__codelineno-8-5" href="#__codelineno-8-5"></a><span class="w"> </span><span class="nx">currentLng</span><span class="p">,</span>
</span><span id="__span-8-6"><a id="__codelineno-8-6" name="__codelineno-8-6" href="#__codelineno-8-6"></a><span class="w"> </span><span class="nx">cut</span><span class="p">.</span><span class="nx">geojson</span>
</span><span id="__span-8-7"><a id="__codelineno-8-7" name="__codelineno-8-7" href="#__codelineno-8-7"></a><span class="p">);</span>
</span></code></pre></div>
<ol>
<li><strong>Consider alternative algorithms</strong>:</li>
</ol>
<p>For better optimization, use 2-opt or genetic algorithms (computationally expensive):</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="c1">// Install optimization library</span>
</span><span id="__span-9-2"><a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a><span class="nx">npm</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nx">routing</span><span class="o">-</span><span class="nx">js</span>
</span><span id="__span-9-3"><a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a>
</span><span id="__span-9-4"><a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a><span class="c1">// Use 2-opt algorithm</span>
</span><span id="__span-9-5"><a id="__codelineno-9-5" name="__codelineno-9-5" href="#__codelineno-9-5"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">twoOpt</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;routing-js&#39;</span><span class="p">;</span>
</span><span id="__span-9-6"><a id="__codelineno-9-6" name="__codelineno-9-6" href="#__codelineno-9-6"></a><span class="kd">const</span><span class="w"> </span><span class="nx">optimized</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">twoOpt</span><span class="p">(</span><span class="nx">locations</span><span class="p">,</span><span class="w"> </span><span class="nx">distanceMatrix</span><span class="p">);</span>
</span></code></pre></div>
<ol>
<li><strong>Pre-optimize routes for shifts</strong>:</li>
</ol>
<p>Admin can pre-calculate optimal routes and assign to volunteers:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-10-1"><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="c1">// Calculate route once, store in Shift model</span>
</span><span id="__span-10-2"><a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">route</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">calculateWalkingRoute</span><span class="p">(</span><span class="nx">locations</span><span class="p">);</span>
</span><span id="__span-10-3"><a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">shift</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span>
</span><span id="__span-10-4"><a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">shiftId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-10-5"><a id="__codelineno-10-5" name="__codelineno-10-5" href="#__codelineno-10-5"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">preCalculatedRoute</span><span class="o">:</span><span class="w"> </span><span class="kt">JSON.stringify</span><span class="p">(</span><span class="nx">route</span><span class="p">)</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-10-6"><a id="__codelineno-10-6" name="__codelineno-10-6" href="#__codelineno-10-6"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="issue-session-auto-abandoned-prematurely">Issue: Session Auto-Abandoned Prematurely<a class="headerlink" href="#issue-session-auto-abandoned-prematurely" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong></p>
<ul>
<li>Active session marked ABANDONED while volunteer still canvassing</li>
<li>Session timeout after &lt;12 hours</li>
<li>Volunteer can't record visits after timeout</li>
</ul>
<p><strong>Causes:</strong></p>
<ul>
<li><code>CANVASS_SESSION_TIMEOUT_HOURS</code> set too low</li>
<li>Volunteer paused for lunch/break (no activity for &gt;1 hour)</li>
<li>System clock drift</li>
</ul>
<p><strong>Solutions:</strong></p>
<ol>
<li><strong>Increase timeout</strong>:</li>
</ol>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-11-1"><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a><span class="c1"># In .env</span>
</span><span id="__span-11-2"><a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a><span class="nv">CANVASS_SESSION_TIMEOUT_HOURS</span><span class="o">=</span><span class="m">24</span><span class="w"> </span><span class="c1"># Was 12, increase to 24</span>
</span></code></pre></div>
<ol>
<li><strong>Record "heartbeat" visits</strong>:</li>
</ol>
<p>Add periodic "still active" ping to prevent timeout:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-12-1"><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a><span class="c1">// Volunteer app sends heartbeat every 30 minutes</span>
</span><span id="__span-12-2"><a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="nx">setInterval</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-12-3"><a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">api</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="sb">`/map/canvass/sessions/</span><span class="si">${</span><span class="nx">sessionId</span><span class="si">}</span><span class="sb">/heartbeat`</span><span class="p">);</span>
</span><span id="__span-12-4"><a id="__codelineno-12-4" name="__codelineno-12-4" href="#__codelineno-12-4"></a><span class="p">},</span><span class="w"> </span><span class="mf">30</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">);</span>
</span></code></pre></div>
<ol>
<li><strong>Allow session resumption</strong>:</li>
</ol>
<p>Let volunteers resume ABANDONED sessions:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-13-1"><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a><span class="c1">// Backend: Add resume endpoint</span>
</span><span id="__span-13-2"><a id="__codelineno-13-2" name="__codelineno-13-2" href="#__codelineno-13-2"></a><span class="k">async</span><span class="w"> </span><span class="nx">resumeSession</span><span class="p">(</span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">sessionId</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-13-3"><a id="__codelineno-13-3" name="__codelineno-13-3" href="#__codelineno-13-3"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">findFirst</span><span class="p">({</span>
</span><span id="__span-13-4"><a id="__codelineno-13-4" name="__codelineno-13-4" href="#__codelineno-13-4"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">sessionId</span><span class="p">,</span><span class="w"> </span><span class="nx">userId</span><span class="p">,</span><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.ABANDONED</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-13-5"><a id="__codelineno-13-5" name="__codelineno-13-5" href="#__codelineno-13-5"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-13-6"><a id="__codelineno-13-6" name="__codelineno-13-6" href="#__codelineno-13-6"></a>
</span><span id="__span-13-7"><a id="__codelineno-13-7" name="__codelineno-13-7" href="#__codelineno-13-7"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">session</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-13-8"><a id="__codelineno-13-8" name="__codelineno-13-8" href="#__codelineno-13-8"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">AppError</span><span class="p">(</span><span class="mf">404</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Abandoned session not found&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;SESSION_NOT_FOUND&#39;</span><span class="p">);</span>
</span><span id="__span-13-9"><a id="__codelineno-13-9" name="__codelineno-13-9" href="#__codelineno-13-9"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-13-10"><a id="__codelineno-13-10" name="__codelineno-13-10" href="#__codelineno-13-10"></a>
</span><span id="__span-13-11"><a id="__codelineno-13-11" name="__codelineno-13-11" href="#__codelineno-13-11"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span>
</span><span id="__span-13-12"><a id="__codelineno-13-12" name="__codelineno-13-12" href="#__codelineno-13-12"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">sessionId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-13-13"><a id="__codelineno-13-13" name="__codelineno-13-13" href="#__codelineno-13-13"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.ACTIVE</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-13-14"><a id="__codelineno-13-14" name="__codelineno-13-14" href="#__codelineno-13-14"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-13-15"><a id="__codelineno-13-15" name="__codelineno-13-15" href="#__codelineno-13-15"></a><span class="p">}</span>
</span></code></pre></div>
<h3 id="issue-gps-tracking-draining-battery">Issue: GPS Tracking Draining Battery<a class="headerlink" href="#issue-gps-tracking-draining-battery" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong></p>
<ul>
<li>Volunteer phone battery drains rapidly</li>
<li>Phone overheats during canvassing</li>
<li>GPS tracking fails after 2-3 hours</li>
</ul>
<p><strong>Causes:</strong></p>
<ul>
<li><code>enableHighAccuracy</code> uses GPS + WiFi + cellular (power-hungry)</li>
<li>Watchposition submits too frequently (every second)</li>
<li>Screen stays on during entire session</li>
</ul>
<p><strong>Solutions:</strong></p>
<ol>
<li><strong>Reduce GPS accuracy</strong>:</li>
</ol>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-14-1"><a id="__codelineno-14-1" name="__codelineno-14-1" href="#__codelineno-14-1"></a><span class="nx">navigator</span><span class="p">.</span><span class="nx">geolocation</span><span class="p">.</span><span class="nx">watchPosition</span><span class="p">(</span>
</span><span id="__span-14-2"><a id="__codelineno-14-2" name="__codelineno-14-2" href="#__codelineno-14-2"></a><span class="w"> </span><span class="nx">callback</span><span class="p">,</span>
</span><span id="__span-14-3"><a id="__codelineno-14-3" name="__codelineno-14-3" href="#__codelineno-14-3"></a><span class="w"> </span><span class="nx">errorCallback</span><span class="p">,</span>
</span><span id="__span-14-4"><a id="__codelineno-14-4" name="__codelineno-14-4" href="#__codelineno-14-4"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-5"><a id="__codelineno-14-5" name="__codelineno-14-5" href="#__codelineno-14-5"></a><span class="w"> </span><span class="nx">enableHighAccuracy</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span><span class="w"> </span><span class="c1">// Use WiFi/cellular only (less accurate but lower power)</span>
</span><span id="__span-14-6"><a id="__codelineno-14-6" name="__codelineno-14-6" href="#__codelineno-14-6"></a><span class="w"> </span><span class="nx">maximumAge</span><span class="o">:</span><span class="w"> </span><span class="kt">5000</span><span class="p">,</span><span class="w"> </span><span class="c1">// Cache position for 5s</span>
</span><span id="__span-14-7"><a id="__codelineno-14-7" name="__codelineno-14-7" href="#__codelineno-14-7"></a><span class="w"> </span><span class="nx">timeout</span><span class="o">:</span><span class="w"> </span><span class="kt">30000</span><span class="p">,</span><span class="w"> </span><span class="c1">// Longer timeout</span>
</span><span id="__span-14-8"><a id="__codelineno-14-8" name="__codelineno-14-8" href="#__codelineno-14-8"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-14-9"><a id="__codelineno-14-9" name="__codelineno-14-9" href="#__codelineno-14-9"></a><span class="p">);</span>
</span></code></pre></div>
<ol>
<li><strong>Reduce submission frequency</strong>:</li>
</ol>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-15-1"><a id="__codelineno-15-1" name="__codelineno-15-1" href="#__codelineno-15-1"></a><span class="c1">// Submit GPS points every 30s instead of 10s</span>
</span><span id="__span-15-2"><a id="__codelineno-15-2" name="__codelineno-15-2" href="#__codelineno-15-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">SUBMIT_INTERVAL_MS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">30000</span><span class="p">;</span><span class="w"> </span><span class="c1">// Was 10000</span>
</span></code></pre></div>
<ol>
<li><strong>Pause tracking during breaks</strong>:</li>
</ol>
<p>Add "Pause Tracking" button to stop GPS watchPosition:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-16-1"><a id="__codelineno-16-1" name="__codelineno-16-1" href="#__codelineno-16-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">pauseTracking</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-16-2"><a id="__codelineno-16-2" name="__codelineno-16-2" href="#__codelineno-16-2"></a><span class="w"> </span><span class="nx">navigator</span><span class="p">.</span><span class="nx">geolocation</span><span class="p">.</span><span class="nx">clearWatch</span><span class="p">(</span><span class="nx">watchId</span><span class="p">);</span>
</span><span id="__span-16-3"><a id="__codelineno-16-3" name="__codelineno-16-3" href="#__codelineno-16-3"></a><span class="w"> </span><span class="nx">setTrackingPaused</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span><span id="__span-16-4"><a id="__codelineno-16-4" name="__codelineno-16-4" href="#__codelineno-16-4"></a><span class="p">};</span>
</span><span id="__span-16-5"><a id="__codelineno-16-5" name="__codelineno-16-5" href="#__codelineno-16-5"></a>
</span><span id="__span-16-6"><a id="__codelineno-16-6" name="__codelineno-16-6" href="#__codelineno-16-6"></a><span class="kd">const</span><span class="w"> </span><span class="nx">resumeTracking</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-16-7"><a id="__codelineno-16-7" name="__codelineno-16-7" href="#__codelineno-16-7"></a><span class="w"> </span><span class="c1">// Start watchPosition again</span>
</span><span id="__span-16-8"><a id="__codelineno-16-8" name="__codelineno-16-8" href="#__codelineno-16-8"></a><span class="w"> </span><span class="nx">setTrackingPaused</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
</span><span id="__span-16-9"><a id="__codelineno-16-9" name="__codelineno-16-9" href="#__codelineno-16-9"></a><span class="p">};</span>
</span></code></pre></div>
<h2 id="performance-considerations">Performance Considerations<a class="headerlink" href="#performance-considerations" title="Permanent link">&para;</a></h2>
<h3 id="visit-recording-rate-limiting">Visit Recording Rate Limiting<a class="headerlink" href="#visit-recording-rate-limiting" title="Permanent link">&para;</a></h3>
<p><strong>Prevent Abuse:</strong></p>
<p>Rate limit prevents accidental bulk submissions:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-17-1"><a id="__codelineno-17-1" name="__codelineno-17-1" href="#__codelineno-17-1"></a><span class="c1">// api/src/middleware/rate-limit.ts</span>
</span><span id="__span-17-2"><a id="__codelineno-17-2" name="__codelineno-17-2" href="#__codelineno-17-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">canvassVisitLimiter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">RateLimiterRedis</span><span class="p">({</span>
</span><span id="__span-17-3"><a id="__codelineno-17-3" name="__codelineno-17-3" href="#__codelineno-17-3"></a><span class="w"> </span><span class="nx">storeClient</span><span class="o">:</span><span class="w"> </span><span class="kt">redis</span><span class="p">,</span>
</span><span id="__span-17-4"><a id="__codelineno-17-4" name="__codelineno-17-4" href="#__codelineno-17-4"></a><span class="w"> </span><span class="nx">keyPrefix</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;rl:canvass-visit:&#39;</span><span class="p">,</span>
</span><span id="__span-17-5"><a id="__codelineno-17-5" name="__codelineno-17-5" href="#__codelineno-17-5"></a><span class="w"> </span><span class="nx">points</span><span class="o">:</span><span class="w"> </span><span class="kt">30</span><span class="p">,</span><span class="w"> </span><span class="c1">// 30 visits</span>
</span><span id="__span-17-6"><a id="__codelineno-17-6" name="__codelineno-17-6" href="#__codelineno-17-6"></a><span class="w"> </span><span class="nx">duration</span><span class="o">:</span><span class="w"> </span><span class="kt">60</span><span class="p">,</span><span class="w"> </span><span class="c1">// per 60 seconds</span>
</span><span id="__span-17-7"><a id="__codelineno-17-7" name="__codelineno-17-7" href="#__codelineno-17-7"></a><span class="w"> </span><span class="nx">blockDuration</span><span class="o">:</span><span class="w"> </span><span class="kt">300</span><span class="p">,</span><span class="w"> </span><span class="c1">// block for 5 minutes if exceeded</span>
</span><span id="__span-17-8"><a id="__codelineno-17-8" name="__codelineno-17-8" href="#__codelineno-17-8"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>Legitimate Use Cases:</strong></p>
<ul>
<li><strong>Bulk data entry</strong>: Admin can bypass rate limit for importing historical data</li>
<li><strong>Offline sync</strong>: Mobile app queues visits offline, submits when online (batch endpoint)</li>
</ul>
<h3 id="session-cleanup-performance">Session Cleanup Performance<a class="headerlink" href="#session-cleanup-performance" title="Permanent link">&para;</a></h3>
<p><strong>Efficient Abandoned Session Query:</strong></p>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-18-1"><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a><span class="c1">-- Index for abandoned session cleanup</span>
</span><span id="__span-18-2"><a id="__codelineno-18-2" name="__codelineno-18-2" href="#__codelineno-18-2"></a><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_canvass_sessions_abandoned</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="ss">&quot;CanvassSession&quot;</span><span class="w"> </span><span class="p">(</span><span class="ss">&quot;status&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">&quot;startedAt&quot;</span><span class="p">)</span>
</span><span id="__span-18-3"><a id="__codelineno-18-3" name="__codelineno-18-3" href="#__codelineno-18-3"></a><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;ACTIVE&#39;</span><span class="p">;</span>
</span><span id="__span-18-4"><a id="__codelineno-18-4" name="__codelineno-18-4" href="#__codelineno-18-4"></a>
</span><span id="__span-18-5"><a id="__codelineno-18-5" name="__codelineno-18-5" href="#__codelineno-18-5"></a><span class="c1">-- Efficient query</span>
</span><span id="__span-18-6"><a id="__codelineno-18-6" name="__codelineno-18-6" href="#__codelineno-18-6"></a><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="ss">&quot;CanvassSession&quot;</span>
</span><span id="__span-18-7"><a id="__codelineno-18-7" name="__codelineno-18-7" href="#__codelineno-18-7"></a><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;ACTIVE&#39;</span>
</span><span id="__span-18-8"><a id="__codelineno-18-8" name="__codelineno-18-8" href="#__codelineno-18-8"></a><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="ss">&quot;startedAt&quot;</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;12 hours&#39;</span><span class="p">;</span>
</span></code></pre></div>
<h3 id="cut-completion-calculation">Cut Completion Calculation<a class="headerlink" href="#cut-completion-calculation" title="Permanent link">&para;</a></h3>
<p><strong>Avoid N+1 Queries:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-19-1"><a id="__codelineno-19-1" name="__codelineno-19-1" href="#__codelineno-19-1"></a><span class="c1">// Inefficient: query per cut</span>
</span><span id="__span-19-2"><a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">cut</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">cuts</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-19-3"><a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">visited</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassVisit</span><span class="p">.</span><span class="nx">count</span><span class="p">({</span>
</span><span id="__span-19-4"><a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">session</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">cutId</span><span class="o">:</span><span class="w"> </span><span class="kt">cut.id</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-19-5"><a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-19-6"><a id="__codelineno-19-6" name="__codelineno-19-6" href="#__codelineno-19-6"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">getAddressesInCut</span><span class="p">(</span><span class="nx">cut</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">length</span><span class="p">;</span>
</span><span id="__span-19-7"><a id="__codelineno-19-7" name="__codelineno-19-7" href="#__codelineno-19-7"></a><span class="w"> </span><span class="nx">cut</span><span class="p">.</span><span class="nx">completionPercentage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">visited</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">total</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">100</span><span class="p">;</span>
</span><span id="__span-19-8"><a id="__codelineno-19-8" name="__codelineno-19-8" href="#__codelineno-19-8"></a><span class="p">}</span>
</span><span id="__span-19-9"><a id="__codelineno-19-9" name="__codelineno-19-9" href="#__codelineno-19-9"></a>
</span><span id="__span-19-10"><a id="__codelineno-19-10" name="__codelineno-19-10" href="#__codelineno-19-10"></a><span class="c1">// Efficient: single aggregation query</span>
</span><span id="__span-19-11"><a id="__codelineno-19-11" name="__codelineno-19-11" href="#__codelineno-19-11"></a><span class="kd">const</span><span class="w"> </span><span class="nx">completionStats</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">canvassSession</span><span class="p">.</span><span class="nx">groupBy</span><span class="p">({</span>
</span><span id="__span-19-12"><a id="__codelineno-19-12" name="__codelineno-19-12" href="#__codelineno-19-12"></a><span class="w"> </span><span class="nx">by</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;cutId&#39;</span><span class="p">],</span>
</span><span id="__span-19-13"><a id="__codelineno-19-13" name="__codelineno-19-13" href="#__codelineno-19-13"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">CanvassSessionStatus.COMPLETED</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-19-14"><a id="__codelineno-19-14" name="__codelineno-19-14" href="#__codelineno-19-14"></a><span class="w"> </span><span class="nx">_count</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">visits</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-19-15"><a id="__codelineno-19-15" name="__codelineno-19-15" href="#__codelineno-19-15"></a><span class="p">});</span>
</span></code></pre></div>
<h2 id="related-documentation">Related Documentation<a class="headerlink" href="#related-documentation" title="Permanent link">&para;</a></h2>
<p><strong>Backend Modules:</strong></p>
<ul>
<li><a href="../../backend/modules/map/canvass.md">Canvass Backend Module</a> — API implementation</li>
<li><a href="../../backend/modules/map/canvass-route.md">Walking Route Service</a> — Route optimization</li>
<li><a href="../../backend/modules/map/tracking.md">Tracking Service</a> — GPS tracking</li>
</ul>
<p><strong>Frontend Pages:</strong></p>
<ul>
<li><a href="../../../frontend/pages/volunteer/volunteer-map-page/">VolunteerMapPage</a> — Full-screen canvass map</li>
<li><a href="../../frontend/pages/admin/canvass-dashboard.md">CanvassDashboardPage</a> — Admin oversight</li>
<li><a href="../../../frontend/pages/volunteer/my-activity-page/">MyActivityPage</a> — Visit history</li>
</ul>
<p><strong>Database:</strong></p>
<ul>
<li><a href="../../../database/models/canvass/#canvasssession-model">CanvassSession Model</a> — Session schema</li>
<li><a href="../../../database/models/canvass/#canvassvisit-model">CanvassVisit Model</a> — Visit records</li>
<li><a href="../../../database/models/canvass/#trackingsession-model">TrackingSession Model</a> — GPS tracking</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
<li><a href="../cuts/">Cuts</a> — Territory boundaries for canvassing</li>
<li><a href="../shifts/">Shifts</a> — Shift-based canvass scheduling</li>
<li><a href="../tracking/">Tracking</a> — GPS tracking system</li>
<li><a href="../locations/">Locations</a> — Address management</li>
</ul>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../shifts/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Shifts">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Shifts
</div>
</div>
</a>
<a href="../tracking/" class="md-footer__link md-footer__link--next" aria-label="Next: Tracking">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Tracking
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 2024 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M252.8 8C114.1 8 8 113.3 8 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C436.2 457.8 504 362.9 504 252 504 113.3 391.5 8 252.8 8M105.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.tooltips", "navigation.expand", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.prune", "navigation.sections", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../../../javascripts/home.js"></script>
<script src="../../../../javascripts/github-widget.js"></script>
<script src="../../../../javascripts/gitea-widget.js"></script>
<script src="../../../../assets/js/env-config.js"></script>
<script src="../../../../assets/js/video-player.js"></script>
</body>
</html>