Updates after initial canvass run
authorBrian Flowers <git-admn@bsflowers.net>
Sat, 28 Dec 2024 01:39:41 +0000 (20:39 -0500)
committerBrian Flowers <git-admn@bsflowers.net>
Sat, 28 Dec 2024 01:39:41 +0000 (20:39 -0500)
22 files changed:
api.php
canvass.php
css/common.css
css/login.css
css/pane1.css
css/pane2.css
data/ElectionDB.sqlite3 [new file with mode: 0755]
images/Home.png [new file with mode: 0644]
images/marker-icon-gray.png [new file with mode: 0644]
index.php
js/canvassing.js
js/common.js
js/demographics.js
js/districts.js
js/precincts.js
js/voters.js
login.php
research.php
script.json [new file with mode: 0644]
settings-api.php
settings.php
taskRunner.php

diff --git a/api.php b/api.php
index 8d382d3..51298bd 100644 (file)
--- a/api.php
+++ b/api.php
@@ -145,6 +145,7 @@ if( isset($_GET['get']) &&
 //       precincts -- csv list of precincts to restrict search to (ex: 0101,0102)
 //       minLat,minLon,maxLat,maxLon -- min/max coordinates to restrict search to
 //       pending -- show only voters not yet contacted
+//       turfId  -- show only voters within this turf
 } else if( isset($_GET['get']) && $_GET['get'] == "voterLocs") {
   $params = Array();
   $parties = Array();
@@ -200,13 +201,19 @@ if( isset($_GET['get']) &&
     array_push($params,        $_POST['maxLon']);
   }
 
+  $turf = " ";
+  if( isset($_POST['turfId']) && strlen($_POST['turfId']) > 0 ) {
+    $turf = " AND ST_WITHIN(PointFromText(CONCAT('POINT(',voters.longitude,' ',voters.latitude,')')), (select geometry from turf where id=?)) ";
+    array_push($params, $_POST['turfId']);
+  }
+
 
   // GOAL: (Optionally) remove all contacted voters -- need to join with canvassing results (currently this is done in a second query below)
   //       Return location along with count of voters of each party/type...or just icon?
   //       These may need to alter how we account for 3k max
   //         Return an object: {"count": nnn, "overflow": true, voterLocs: [ {"display": {"history": non, "party": "Democrat"}, "lat": ..., "lon": ..., "address": ..., "count": ... } ]
 
-  $query = "SELECT * FROM (SELECT voters.id as id, address.id as addressId, address.latitude,address.longitude,address.precinct,party,voters.birthyear,address.addressLine1, ".
+  $query = "SELECT DISTINCT * FROM (SELECT voters.id as id, address.id as addressId, address.latitude,address.longitude,address.precinct,party,voters.birthyear,address.addressLine1, ".
            (sizeof($lists) > 0 ? "list.icon, cv.listId, " : "").
            "(SELECT sum(voted) FROM voterHistory vh WHERE vh.voterId = voters.id) as voteCount ".
            "FROM voters ".
@@ -227,7 +234,8 @@ if( isset($_GET['get']) &&
            "))) ".
            $precincts.
            $latlon.
-           "LIMIT 3000;";
+           $turf.
+           "LIMIT 10000;";
 
 //echo $query.PHP_EOL;
 //print_r( $params );
@@ -242,28 +250,32 @@ if( isset($_GET['get']) &&
   // Reformat the results for display
   $output = new stdClass;
   $output->count = sizeOf($rows);
-  $output->overflow = $output->count==3000;
+  $output->overflow = $output->count==10000;
   $output->voterLocs = Array();
   for($i = 0; $i < sizeOf($rows); $i++) {
     $added = 0;
 
-    // Exclude any that have already been canvassed
-    if(isset($_POST['pending'])) {
-      $cquery = "SELECT * FROM canvassResults WHERE voterId = ?;";
-      $stmt   = $dbh->prepare($cquery);
-      $stmt->execute(Array($rows[$i]['id']));
-      $crows  = $stmt->fetch(PDO::FETCH_ASSOC);
+    // Flag/exclude any that have already been canvassed
+//    if(isset($_POST['pending'])) {
+    $cquery = "SELECT * FROM canvassResults WHERE voterId = ?;";
+    $stmt   = $dbh->prepare($cquery);
+    $stmt->execute(Array($rows[$i]['id']));
+    $crows  = $stmt->fetch(PDO::FETCH_ASSOC);
 
-      if($crows != null) {
+    $contacted = 0;
+    if($crows != null) {
+      if(isset($_POST['pending'])) {
         continue;
       }
+      $contacted = sizeof($crows);
     }
+//    }
 
     // Set voter history flags
     if($rows[$i]["voteCount"] >= 2) {
       $history = "likely";
     } else if($rows[$i]["voteCount"] > 0 && $rows[$i]["voteCount"] < 2) {
-      $history = "likely";
+      $history = "unlikely";
     } else if((int)$rows[$i]["birthyear"] > 2000) {
       $history = "new";
     } else {
@@ -283,13 +295,16 @@ if( isset($_GET['get']) &&
           $output->voterLocs[$j]->parties[$rows[$i]['party']] = 1;
         }
         $output->voterLocs[$j]->parties[$rows[$i]['party']]++;
-                if( !isset($output->voterLocs[$j]->histories[$history]) ) {
+        if( !isset($output->voterLocs[$j]->histories[$history]) ) {
           $output->voterLocs[$j]->histories[$history] = 1;
         }
         $output->voterLocs[$j]->histories[$history]++;
         if( $icon != "" ) {
           $output->voterLocs[$j]->icon = $icon;
         }
+        if($contacted == 0) {
+          $output->voterLocs[$j]->contacted = 0;
+        }
         $added = 1;
       }
     }
@@ -303,6 +318,7 @@ if( isset($_GET['get']) &&
       $outputObj->parties[$rows[$i]['party']] = 1;
       $outputObj->histories = Array();
       $outputObj->histories[$history] = 1;
+      $outputObj->contacted = $contacted;
       if( $icon != "" ) {
         $outputObj->icon = $icon;
       }
@@ -396,7 +412,7 @@ if( isset($_GET['get']) &&
              ") OR (list.id IN (".str_repeat("?,",sizeof($lists)-1)."?) AND list.id = cv.listId AND (cv.voterId = voters.id AND cv.addressId = voterAddresses.id) " :
              "").
            "))); ";*/
-  $query = "SELECT * FROM (SELECT voters.id as id, voters.firstName, voters.middleName, voters.lastName, voters.birthyear, party, ".
+  $query = "SELECT DISTINCT * FROM (SELECT voters.id as id, voters.firstName, voters.middleName, voters.lastName, voters.birthyear, party, ".
                            "address.id as addressId, address.latitude,address.longitude,address.precinct,address.addressLine1, address.addressLine2, ".
            (sizeof($lists) > 0 ? "list.icon, cv.listId, " : "").
            "(SELECT sum(voted) FROM voterHistory vh WHERE vh.voterId = voters.id) as voteCount ".
@@ -417,9 +433,7 @@ if( isset($_GET['get']) &&
              "").
            "))) ".
            $precincts.
-           "LIMIT 3000;";
-
-
+           "LIMIT 10000;";
 
   $stmt = $dbh->prepare($query);
   $stmt->execute($params);
@@ -569,10 +583,10 @@ if( isset($_GET['get']) &&
 //       turfPoints -- a list of latitude/longitude points defining the turf area
 } else if( isset($_GET['set']) && $_GET['set'] == "turf" ) {
   $name = $_POST['name'];
-  $points = $_POST['turfPoints'];
-  $query = "INSERT INTO turf(name, json) VALUES(?,?);";
+  $json = $_POST['turfPoints'];
+  $query = "INSERT INTO turf(name, json, geometry) VALUES(?,?, ST_GeomFromGeoJSON(?));";
   $stmt = $dbh->prepare($query);
-  $stmt->execute([$name, $points]);
+  $stmt->execute([$name, $json, $json]);
   $id = $dbh->lastInsertId();
 
   echo '{"status": "OK", "id": '.$id.'}';
@@ -606,7 +620,7 @@ if( isset($_GET['get']) &&
 
 
     // Fetch the canvasses in this turf
-    $query = "SELECT c.id, c.turfId, c.name, c.start, c.end, c.totalContacts, c.lastLoc, c.voterString, ".
+    $query = "SELECT c.id, c.turfId, c.name, c.start, c.end, c.totalContacts, c.lastLoc, c.voterString, c.script, ".
              "(SELECT count(distinct voterId) FROM canvassResults r WHERE r.canvassId = c.id) as madeContacts, ".
              "(SELECT max(timestamp) FROM canvassResults r WHERE r.canvassId = c.id) as lastActive ".
              "FROM canvasses c WHERE turfId = CAST(? AS INTEGER);";
@@ -653,8 +667,20 @@ if( isset($_GET['get']) &&
     $rows[$i]['party'] = $irow['party'];
     $rows[$i]['address'] = $irow['addressLine1']." ".$irow['addressLine2'];
     $rows[$i]['city'] = $irow['city'];
+
+    $rows[$i]['json'] = json_decode($rows[$i]['json']);
   }
 
+  $query = "SELECT script FROM canvasses WHERE id=?;";
+  $params= [$id];
+  $stmt  = $dbh->prepare($query);
+  $stmt -> execute($params);
+  $scripts  = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+
+  $output = new stdClass();
+  $output->dataset = $rows;
+  $output->script  = $scripts[0]['script'];
   if(isset($_GET['format']) && $_GET['format'] == "csv") {
     header('Content-Disposition: attachment; filename="canvassResults_'.$id.'.csv";');
     echo "result".str_replace("voterId", "voter", implode(",", array_keys($rows[0]))).PHP_EOL;
@@ -662,7 +688,7 @@ if( isset($_GET['get']) &&
       echo implode(",", $rows[$i]).PHP_EOL;
     }
   } else {
-    echo json_encode($rows);
+    echo json_encode($output);
   }
 
 // URL: ?set=canvass
@@ -746,19 +772,20 @@ if( isset($_GET['get']) &&
   $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
   $minLat = null; $minLon = null; $maxLat = null; $maxLon = null;
   $row = json_decode($rows[0]['json']);
+  $row = $row->geometry->coordinates[0];
 
   for($i = 0; $i < sizeof($row); $i++) {
     if($minLat == null || $minLat > $row[$i]->lat) {
-      $minLat = $row[$i]->lat;
+      $minLat = $row[$i][1];
     }
     if($maxLat == null || $maxLat < $row[$i]->lat) {
-      $maxLat =        $row[$i]->lat;
+      $maxLat =        $row[$i][1];
     }
     if($minLon == null || $minLon > $row[$i]->lng) {
-      $minLon =        $row[$i]->lng;
+      $minLon =        $row[$i][0];
     }
     if($maxLon == null || $maxLon < $row[$i]->lng) {
-      $maxLon =        $row[$i]->lng;
+      $maxLon =        $row[$i][0];
     }
   }
   $latlon = " AND address.latitude > ? AND address.latitude < ?".
@@ -837,13 +864,15 @@ if( isset($_GET['get']) &&
 //       state
 //       zip
 } else if( isset($_GET['set']) && $_GET['set'] == "canvassResult") {
+  // TODO: Maintain prior json?
   $corrections = $_POST["corrections"] == "true" ? 1 : 0;
   $dnc = $_POST["dnc"]  == "true" ? 1 : 0;
   $id = $_POST['id'];
   $canvassId = $_POST['canvassId'];
   $notes = $_POST['notes'];
   $priority = $_POST['priority'] == "true" ? 1 : 0;
-  $supportPct = $_POST['supportPct'];
+//  $supportPct = $_POST['supportPct'];
+  $json = $_POST['prompts'];
   if($notes == "") {
    $notes = " ";
   }
@@ -864,7 +893,8 @@ if( isset($_GET['get']) &&
     $state   = isset($_POST['state'])     ? $_POST['state']     : "";
     $zip     = isset($_POST['zip'])       ? $_POST['zip']       : "";
     $precinct= isset($_POST['precinct'])  ? $_POST['precinct']  : "";
-    $query = "INSERT INTO voterAddress(latitude, longitude, address, city, state, zip, precinct) ".
+    $byear   = date('Y', strtotime($bdate));
+    $query = "INSERT INTO voterAddresses(latitude, longitude, addressLine1, city, state, zip, precinct) ".
              "VALUES(?, ?, ?, ?, ?, ?, ?); ";
     $params = [$lat, $lon, $address, $city, $state, $zip, $precinct];
     $stmt = $dbh->prepare($query);
@@ -875,9 +905,9 @@ if( isset($_GET['get']) &&
     }
     $aid = $dbh->lastInsertId();
 
-    $query = "INSERT INTO voter(firstName, middleName, lastName, birthdate, sex, age, phone, party, addressId) ".
-             "VALUES(?,?,?,?,?,?,?,?,?); ";
-    $params = [$fname, $mname, $lname, $bdate, $sex, $age, $phone, $party, $aid];
+    $query = "INSERT INTO voters(firstName, middleName, lastName, birthyear, phone, party, addressId) ".
+             "VALUES(?,?,?,?,?,?,?); ";
+    $params = [$fname, $mname, $lname, $byear, $phone, $party, $aid];
     $stmt = $dbh->prepare($query);
     $stmt->execute($params);
     if( $dbh->errorInfo()[0] != "00000") {
@@ -888,9 +918,9 @@ if( isset($_GET['get']) &&
   }
 
   $query = "INSERT INTO canvassResults(voterId, canvassId, timestamp, userId, contactStatus, ".
-           "contactMethod, estSupportPct, notes, correctionsNeeded, priority, noContact) ".
+           "contactMethod, notes, correctionsNeeded, priority, noContact, json) ".
            "VALUES(?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?, ?);";
-  $params = [$id, $canvassId, time(), $_SESSION['userId'], "Canvass", $supportPct, $notes, $corrections, $priority, $dnc];
+  $params = [$id, $canvassId, $_SESSION['userId'], "1", "Canvass", $notes, $corrections, $priority, $dnc, $json];
   $stmt = $dbh->prepare($query);
   $stmt->execute($params);
   print_r(json_encode($dbh->errorInfo()));
index b820b68..a023b0c 100644 (file)
@@ -20,14 +20,16 @@ $dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options);
 * If not, see <https://www.gnu.org/licenses/>. 
 */
 -->
+<!DOCTYPE html>
 <HTML>
   <HEAD>
+    <link rel="stylesheet" href="css/common.css" />
     <link rel="stylesheet" href="css/pane1.css" />
     <link rel="stylesheet" href="css/pane2.css" />
     <link rel="stylesheet" href="css/canvass.css" />
+    <script src="js/voters.js"></script>
     <script src="js/common.js"></script>
     <script src="js/canvassing.js"></script>
-    <script src="js/voters.js"></script>
 
     <!-- Leaflet.js (see: https://leafletjs.com/examples/quick-start/) -->
     <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
@@ -58,9 +60,13 @@ $dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options);
         (<span id="loadingCount">0</span>)
       </div>
 
-      <span id="breadcrumb"><a href="index.php">Home</a> | <a href="canvass.php">Canvassing</a></span>
+      <span id="breadcrumb">
+        <a href="index.php">Home</a> | <a href="canvass.php">Canvassing</a>
+        <span id="username"><?php echo $_SESSION['username']; ?></span>
+      </span>
 
-      <div class="category"><span class="toggle" onclick="toggleList(event);">+</span>Canvassing
+      <div class="category"><span class="toggle" onclick="toggleList(event);">+</span>
+        <span onclick="toggleList(event);">Canvassing</span>
         <div class="list" id="canvassing-list">
           <div id="turfList">
           </div>
@@ -156,7 +162,7 @@ EOF;
       </div>
     </div>
 
-    <div id="details">
+    <div id="details" class="canvass">
       <table id="detailsTable"></table>
       <div id="detailsControls"></div>
       <button id="mapReturn" onclick="restoreMap()">Return to Map</button>
index 25f41bb..42fc720 100644 (file)
 * If not, see <https://www.gnu.org/licenses/>. 
 */
 
+/* prevent pull-to-refresh for Safari 16+ */
+@media screen and (pointer: coarse) {
+  @supports (-webkit-backdrop-filter: blur(1px)) and (overscroll-behavior-y: none)  {
+    html {
+      min-height: 100.3%;
+      overscroll-behavior-y: none;
+    }
+  }
+}
+/* prevent pull-to-refresh for Safari 9~15 */
+@media screen and (pointer: coarse) {
+  @supports (-webkit-backdrop-filter: blur(1px)) and (not (overscroll-behavior-y: none))  {
+    html {
+      height: 100%;
+      overflow: hidden;
+    }
+    body {
+      margin: 0px;
+      max-height: 100%; /* or `height: calc(100% - 16px);` if body has default margin */
+      overflow: auto;
+      -webkit-overflow-scrolling: touch;
+    }
+    /* in this case to disable pinch-zoom, set `touch-action: pan-x pan-y;` on `body` instead of `html` */
+  }
+}
 
-body {
+/* prevent pull-to-refresh for Chrome 63+ */
+body{
+  overscroll-behavior-y: none;
 }
 
-#breadcrumb {
-/*  display: block;
+/*body {
+  overscroll-behavior: none;
+}*/
+
+/*#breadcrumb {
+  font-weight:  bold;
+  color:        #FEE;
+  background-color: #000;
+  display:      block;
+  margin:       -.5em -1em;
+  padding:      .4em 2em;
+  border-bottom:1px solid #FEE;
+}*/
+
+
+/*#breadcrumb {
+  display: block;
   margin:  0.5em 0em;
   font-size: 1.5em;
   font-weight: bold;
@@ -27,10 +69,10 @@ body {
   border: 2px solid black;
   background-color: #FCC;
   border-radius: 0.5em;
-  padding: 0.5em 1em;*/
+  padding: 0.5em 1em;
   font-weight: bold;
   color:       #FEE;
-}
+}*/
 
 #breadcrub a:visited {
   color:       #CCC;
@@ -84,13 +126,13 @@ body {
   left: 0%;
 }
 
-#details {
+#details.canvass {
   display: none;
   height: 100%;
-  width: 50%;
+  width: 100%;
   position: fixed;
   top: 0px;
-  left: 50%;
+  left: 0%;
   z-index: 0;
   font-size: 2em;
   padding: 1em;
@@ -219,7 +261,60 @@ body {
   vertical-align: middle;
 }
 
-@media screen and (orientation: portrait), (max-width: 600px) {
+.category {
+  position: relative;
+  width: 100%;
+  display: inline-block;
+  text-align: left;
+  border-radius: .2em;
+  margin-top: 1em;
+  padding: 0em .5em;
+  font-weight: bold;
+  text-decoration: none;
+  color: #FEE;
+  font-size: 2em;
+/*  background-color: #844;*/
+  border-top: 2px solid white;
+  box-sizing: border-box;
+}
+
+.category .toggle {
+  font-size: 0.8em;
+  display: inline-block;
+  border: 1px solid black;
+  border-radius: 0.2em;
+  width: 1em;
+  height: 0.8em;
+  padding-bottom: 0.2em;
+  margin-right: 0.5em;
+  text-align: center;
+  line-height: 1;
+/*  background-color: #866;*/
+  background-color: #FCC;
+  color: #300;
+}
+
+.category .toggle:hover {
+  background-color: #800;
+}
+
+.category .list {
+  position: relative;
+  font-size: 0.7em;
+  margin: 0em;
+  padding: 1em;
+}
+
+.category .list input {
+  vertical-align: middle;
+}
+
+.category table, .category tr {
+  width: 100%;
+}
+
+
+@media screen and (orientation: portrait), (max-width: 600px), (max-device-width: 800px) {
   #controls {
     position: fixed;
     width: calc(100% - 6em);
@@ -246,6 +341,27 @@ body {
     height: calc(100% /*- 12em*/);
     z-index: 0;
   }
+
+  .category {
+    font-size: 5vw !important;
+  }
+
+  a.category {
+    font-size: 5vw !important;
+  }
+
+  #breadcrumb {
+    font-size: 3vw;
+  }
+
+  .loadingIndicator {
+    font-size: 3vw;
+  }
+
+  /* Limit Safari zooming on textareas */
+  textarea {
+    font-size: 16px;
+  }
 }
 
 .dataset {
@@ -275,21 +391,20 @@ th {
   background-color: #FCC;
 }*/
 
-.category {
+/*.category {
   position: relative;
   width: 100%;
   display: inline-block;
   text-align: left;
   border-radius: .2em;
-  font-size: 2em;
   margin-top: 1em;
   padding: 0em .5em;
   font-weight: bold;
   text-decoration: none;
   color: #FEE;
-  font-size: 2em;
+  font-size: 2em;*/
 /*  background-color: #844;*/
-  border-top: 2px solid white;
+/*  border-top: 2px solid white;
   box-sizing: border-box;
 }
 
@@ -303,9 +418,9 @@ th {
   padding-bottom: 0.2em;
   margin-right: 0.5em;
   text-align: center;
-  line-height: 1;
+  line-height: 1;*/
 /*  background-color: #866;*/
-  background-color: #FCC;
+/*  background-color: #FCC;
   color: #300;
 }
 
@@ -326,7 +441,7 @@ th {
 
 .category table, .category tr {
   width: 100%;
-}
+}*/
 
 .subcategory {
   margin-left: 1em;
@@ -387,28 +502,18 @@ th {
   text-align: center;
 }
 
-#canvassToolbar button {
-  max-width: 15%;
-  max-height: 15vw;
-  background-color: #FEE;
-  border-radius: 1em;
-  padding: 5px;
-  margin: 2px;
-  transition: width 1s;
-}
-
 #canvassToolbar img {
   max-width: calc(15vw - 10px);
   max-height: calc(15vw - 10px);
 }
 
-#geocoderAddrInput, #geocoderAddrInputSubmit {
+/*#geocoderAddrInput, #geocoderAddrInputSubmit {
   width: 0px;
-  height: 3em;
+  height: 8em;
   padding: 0px;
   margin: 0px;
   transition: width 1s;
-}
+}*/
 
 #canvassToolbar.addrInput button, #canvassToolbar.addrInput button img {
   width: 0px;
index a680ed8..7fee818 100644 (file)
@@ -22,6 +22,10 @@ body {
   margin-top: 1em;
 }
 
+#loginPrompt label, #loginPrompt input {
+  font-size: 3em;
+}
+
 #loginPrompt input[type=submit] {
   display: block;
   width: 50%;
index 0123b93..6a9ac5a 100644 (file)
@@ -28,6 +28,11 @@ body {
   border-bottom:1px solid #FEE;
 }
 
+#username {
+  position: absolute;
+  right:    1em;
+}
+
 a {
   color: #FEE;
 }
@@ -36,12 +41,12 @@ a:visited {
   color:       #EEF;
 }
 
-a.category {
+/*a.category {
   text-decoration: underline;
   padding-top:      0.25em;
   margin-bottom:    -0.5em;
   padding-left:     1em;
-}
+}*/
 
 #toastDiv {
   position: fixed;
@@ -123,7 +128,7 @@ a.category {
     left: 0px;
   }
 }
-
+/*
 .category {
   position: relative;
   width: 100%;
@@ -158,7 +163,7 @@ a.category {
 
 .category .toggle:hover {
   background-color: #800;
-}
+}*/
 
 .category .list {
   position: relative;
index 604bca0..7b7c1db 100644 (file)
@@ -32,7 +32,7 @@
 #details {
   display: none;
   height: 100%;
-  width: 50%;
+  width: calc(50% - 4em);
   position: fixed;
   top: 0px;
   left: 50%;
@@ -41,6 +41,7 @@
   padding: 1em;
   overflow-y: scroll;
   background-color: #f8f0f0;
+  text-align: center;
 }
 
 #details a {
 #mapReturn {
   font-size: 2em;
   text-align: center;
-  position: absolute;
+/*  position: absolute;*/
 /*  bottom: 1em;*/
-  margin-left: -5em;
-  left: 50%;
+/*  margin-left: -5em;
+  left: 50%;*/
   width: 10em;
+  margin-bottom: 2em;
 }
 
 #detailsControls {
   padding: 0em 0em 5em 0em;
+  text-align: left;
+  width: 95%;
+}
+
+#detailsControls select {
+  font-size: 2em;
+  margin-bottom: 1em;
 }
 
 #blackout {
 #voterBack {
   float: right;
   margin-right: 1em;
+  z-index: 100;
 }
 
-@media screen and (orientation: portrait), (max-width: 600px) {
+#voterSave {
+  z-index: 100;
+}
+
+@media screen and (orientation: portrait), (max-width: 800px) {
   #map, #map.full {
     width: 100%;
     left:  0%;
     height: calc(100%);
     z-index: 0;
   }
+
+  #voterSel {
+    width: 90% !important;
+    margin-left: -1em;
+  }
 }
 
 .loadingIndicator {
   border-collapse: collapse;
   display: block;
 }
-
+/*
 .category {
   position: relative;
   width: 100%;
   font-weight: bold;
   text-decoration: none;
   color: #FEE;
-  font-size: 2em;
   border-top: 2px solid white;
   box-sizing: border-box;
 }
 
 .category .toggle:hover {
   background-color: #800;
-}
+}*/
 
 .category .list {
   position: relative;
   padding: 1em;
 }
 
+
+
 .category .list input {
   vertical-align: middle;
 }
   width: 100%;
 }
 
+.category table {
+  margin-bottom: 3em;
+}
+
 .subcategory {
   margin-left: 1em;
   margin-right: 1em;
-  margin-top:   1em;
+  margin-top:   0.5em;
   margin-bottom:1em;
   color: #FEE;
   border-bottom: 1px dashed #FEE;
 }
 
+.subcategory tr td:last-child {
+  height: 100%;
+}
+
+.subcategory tr td strong {
+  font-size: 1.5em;
+}
+
 #canvassing-list .subcategory input {
   width: 100%;
+  height: 100%;
+  font-size: 1.1em;
+}
+
+#canvassing-list label {
+  margin-top: 1em;
 }
 
 #canvassing-list .subcategory tr.new {
 }
 
 #voterSel, #voterSupportRange {
-  width:         calc(100% - 4em);
-  height:        3em;
+  width:         calc(100% - 6em);
   margin-bottom: 1em;
+  font-size:     3em;
+}
+
+#voterSel:enabled {
+  border: 2px solid #FAA;
 }
 
 #canvassToolbar {
   text-align: center;
 }
 
-#canvassToolbar button {
+#canvassToolbar button:not(#geocoderAddrInputClose) {
   max-width: 15%;
   max-height: 15vw;
   background-color: #FEE;
   max-height: calc(15vw - 10px);
 }
 
-#geocoderAddrInput, #geocoderAddrInputSubmit {
+#geocoderAddrInput.error {
+  background-color: #A00;
+  transition: background-color 1000ms linear;
+}
+
+#geocoderAddrInput, #geocoderAddrInputSubmit, #geocoderAddrInputClose {
   width: 0px;
-  height: 3em;
+  height: 2em;
   padding: 0px;
   margin: 0px;
   transition: width 1s;
+  font-size: 4em;
+  vertical-align: top;
+  display: none;
+  transition: background-color 1000ms linear;
 }
 
 #canvassToolbar.addrInput button, #canvassToolbar.addrInput button img {
-  width: 0px;
-  margin: 0px;
-  padding: 0px;
+  width: 0px !important;
+  margin: 0px !important;
+  padding: 0px !important;
+  display: none;
 }
 
 #canvassToolbar.addrInput #geocoderAddrInput {
   width: 50%;
   max-width: 80%;
+  display: inline-block;
 }
 
 #canvassToolbar.addrInput #geocoderAddrInputSubmit {
   min-width: 10%;
+  width: auto;
+  display: inline-block;
+}
+
+#canvassToolbar.addrInput #geocoderAddrInputClose {
+  min-width: 1em;
+  width: auto;
+  display: inline-block;
 }
 
+
 #detailsTable .notesCell {
   border-bottom: 6px solid black;
 }
 
+#detailsTable input, #detailsTable select {
+  font-size: 2em;
+}
+
 #paginator {
   height: 2em;
   position: relative;
 #paginator a {
   padding: 0em 1em;
 }
+
+.leaflet-voterCount {
+  font-weight: 900;  
+  background:  transparent !important;
+  margin-top:  -60px;
+  border:      transparent !important;
+  box-shadow:  none !important;
+  margin-left: 10px !important;
+  color:       #0C0 !important;
+  text-shadow: -1px 1px 0 #000,
+               1px 1px 0 #000,
+               1px -1px 0 #000,
+               -1px -1px 0 #000;
+  font-size: 32px;
+}
+
+.leaflet-voterCount:before {
+  display: none;
+  border: none;
+}
+
+#voterSaveStatus {
+  font-weight: 900;
+  text-align:  center;
+}
+
+#voterSaveStatus {
+  display: inline;
+}
+
+#voterSaveStatus.unsaved::before {
+  content: 'Unsaved Changes';
+  color:   red;
+  width: 75%;
+  display: block;
+  left: 10%;
+  position: absolute;
+  margin-top: -2em;
+  text-shadow: -1px 1px 0 #000,
+               1px 1px 0 #000,
+               1px -1px 0 #000,
+               -1px -1px 0 #000;
+  z-index: 50;
+}
+
+#voterSaveStatus.error::before {
+  content: 'An error occurred.\aPlease try again or contact support.';
+  font-size: 0.8em;
+  color:   red;
+  width: 75%;
+  display: block;
+  left: 10%;
+  position: absolute;
+  margin-top: -2em;
+  white-space: pre;
+  text-shadow: -1px 1px 0 #000,
+               1px 1px 0 #000,
+               1px -1px 0 #000,
+               -1px -1px 0 #000;
+  z-index: 50;
+}
+
+
+#voterSaveStatus.saved::before {
+  content: 'Saved.';
+  color:   green;
+  width: 75%;
+  display: block;
+  left: 10%;
+  position: absolute;
+  margin-top: -2em;
+  text-shadow: -1px 1px 0 #000,
+               1px 1px 0 #000,
+               1px -1px 0 #000,
+               -1px -1px 0 #000;
+  z-index: 50;
+}
+
+
+
+/*.leaflet-marker {
+  width: 96px;
+}*/
+
+#geocoderLastBtn:disabled img {
+  opacity: 0.3;
+}
+
+.promptNotes {
+  font-style: italic;
+}
+
diff --git a/data/ElectionDB.sqlite3 b/data/ElectionDB.sqlite3
new file mode 100755 (executable)
index 0000000..4af9dc7
Binary files /dev/null and b/data/ElectionDB.sqlite3 differ
diff --git a/images/Home.png b/images/Home.png
new file mode 100644 (file)
index 0000000..426e435
Binary files /dev/null and b/images/Home.png differ
diff --git a/images/marker-icon-gray.png b/images/marker-icon-gray.png
new file mode 100644 (file)
index 0000000..f392f11
Binary files /dev/null and b/images/marker-icon-gray.png differ
index 82744c5..2555307 100644 (file)
--- a/index.php
+++ b/index.php
 * If not, see <https://www.gnu.org/licenses/>. 
 */
 -->
+<!DOCTYPE html>
 <HTML>
   <HEAD>
+    <link rel="stylesheet" href="css/common.css" />
     <link rel="stylesheet" href="css/pane1.css" />
     <link rel="stylesheet" href="css/pane2.css" />
     <script src="js/common.js"></script>
@@ -50,6 +52,6 @@
       <p><b>Phonebank</b> - Make calls to voters within a designated turf</p>
       <p><b>Settings</b> - Configure CCCP</p>
     </div>
-    <script>init();</script>
+    <script>setTimeout(init, 500);</script>
   </BODY>
 </HTML>
index c5ec03f..2b1cd35 100644 (file)
@@ -17,6 +17,7 @@
 // Enable drawing functions for cutting turf
 // (Using LeafletJS leaflet-draw plugin
 function enableDraw() {
+  debug("enableDraw()");
   if( document.getElementsByClassName("leaflet-draw").length > 0) {
     return;
   }
@@ -91,10 +92,12 @@ function enableDraw() {
       console.log("noop");
     });
   });
+  debug("End enableDraw");
 }
 
 // Disable drawing of turf
 function disableDraw(drawControl) {
+  debug("disableDraw("+drawControl+")");
   map.removeControl(drawControl);
   var layerKeys = Object.keys(map._layers);
   for(var i = 0; i < layerKeys.length; i++) {
@@ -102,12 +105,17 @@ function disableDraw(drawControl) {
       map.removeLayer(map._layers[layerKeys[i]]);
     }
   }
+  debug("End disableDraw");
 }
 
 // Show/hide a particular turf
 var turfLayer = null;
-function toggleTurf(turf, event) {
-  if( turfLayer != null ) {
+function toggleTurf(turf, value, event) {
+  debug("toggleTurf(..., ..., ...)");
+  debug(turf);
+  debug(value);
+  debug(event);
+  if( turfLayer != null && value != true) {
     map.removeLayer(turfLayer);
     turfLayer = null;
     return;
@@ -118,8 +126,11 @@ function toggleTurf(turf, event) {
   }
 
   var coordinates = [];
-  var turfjs = JSON.parse(turf.json);
-  for(var i = 0; i < turfjs.length; i++) {
+  if(turf.json == null) {
+    return;
+  }
+  var geojs = JSON.parse(turf.json);
+/*  for(var i = 0; i < turfjs.length; i++) {
     coordinates.push([turfjs[i]["lng"], turfjs[i]["lat"]]);
   }
   var geometry = Object();
@@ -133,12 +144,14 @@ function toggleTurf(turf, event) {
   featObj.properties.NAME = turf.name;
   featObj.properties.OBJECTID = 2;
 
-  var geojs = geoJsonBuilder(JSON.stringify(featObj));
+  var geojs = geoJsonBuilder(JSON.stringify(featObj));*/
   turfLayer = L.geoJSON(geojs).addTo(map);
+  debug("End toggleTurf");
 }
 
 // Load turfs from the database
 function loadTurfs(research = false) {
+  debug("loadTurfs("+research+")");
   setLoading(1);
   fetch("api.php?get=canvasses").then(data => data.json())
     .then(turfs => {
@@ -240,7 +253,7 @@ function loadTurfs(research = false) {
        var show = document.createElement("input");
        show.setAttribute("type", "button");
        show.setAttribute("value", "Show on map");
-       show.onclick = toggleTurf.bind(null, turfs[i], null);
+       show.onclick = toggleTurf.bind(null, turfs[i], true, null);
        header.appendChild(show);
        turfList.appendChild(header);
       }
@@ -262,16 +275,21 @@ function loadTurfs(research = false) {
       }
     }
     setLoading(-1);
+    debug("End Async loadTurfs");
   });
+  debug("End Sync loadTurfs");
 }
 
 // Intermediate callback to handle UI portion of saving a turf
 // Calls saveTurfToDB to actually persist it
 function saveTurf(drawControl) {
+  debug("saveTurf("+drawControl+")");
   var layerKeys = Object.keys(map._layers);
   for(var i = 0; i < layerKeys.length; i++) {
     if( typeof map._layers[layerKeys[i]].type != 'undefined' ) {
-      var turfPoints = JSON.stringify(map._layers[layerKeys[i]]._latlngs[0]);
+      var turfjson = JSON.stringify(map._layers[layerKeys[i]].toGeoJSON());
+console.log(turfjson);
+console.log("CHECK");
       var blackoutElem = document.createElement("div");
       var nameElem = document.createElement("div");
 
@@ -291,19 +309,23 @@ function saveTurf(drawControl) {
       nameButton.setAttribute("type", "button");
       nameButton.setAttribute("id", "turfNameButton");
       nameButton.setAttribute("value", "Save");
-      nameButton.onclick = saveTurfToDB.bind(null, turfPoints);
+      nameButton.onclick = saveTurfToDB.bind(null, turfjson);
       nameElem.appendChild(nameButton);
 
       document.getElementById("map").appendChild(blackoutElem);
       document.getElementById("map").appendChild(nameElem);
       disableDraw(drawControl);
+      debug("End saveTurf (Early)");
       return;
     }
   }
+  debug("End saveTurf");
 }
 
 // Write the turf data to the database
 function saveTurfToDB(turfPoints) {
+  debug("saveTurfToDB(...)");
+  debug(turfPoints);
   var name = document.getElementById("turfNameInput").value;
 
   var options = {
@@ -318,12 +340,13 @@ function saveTurfToDB(turfPoints) {
 
   document.getElementById("map").removeChild(document.getElementById("blackout"));
   document.getElementById("map").removeChild(document.getElementById("turfNameEntry"));
-
+  debug("End saveTurfToDB");
 //  loadTurfs();
 }
 
 // Start a new canvass for a given turf
 function createCanvass(turfId) {
+  debug("createCanvass("+turfId+")");
   var name = document.getElementById("new-canvass-"+turfId).value;
   if( name == "") {
     name = document.getElementById("new-canvass-"+turfId).getAttribute("placeholder");
@@ -336,49 +359,62 @@ function createCanvass(turfId) {
   fetch("api.php?set=canvass", options).then(data => data.json())
     .then(resp => {
     loadTurfs(true);
+    debug("End Async createCanvass");
   });
+  debug("End Sync createCanvass");
 }
 
 // Display a canvasses's turf
 function viewCanvass(canvass) {
+  debug("viewCanvass(...)");
+  debug(canvass);
   setLoading(1);
+  currentCanvass = canvass;
   fetch("api.php?get=canvasses&id="+canvass.turfId).then(data => data.json())
     .then(turfs => {
 
       for(var i = 0; i < turfs.length; i++) {
-        toggleTurf(turfs[i], null);
+        toggleTurf(turfs[i], null, null);
 //        document.getElementById("turf-"+turfs[i].id).checked = true;
       }
       setVoterString(canvass.voterString);
 
     setLoading(-1);
+    debug("End Async viewCanvass");
   });
+  debug("End Sync viewCanvass");
 }
 
 // Don't feel like figuring out how to bind additional params to updateLocation
 //   so let's just use a global...
+//   (This is also now used for getting canvass scripts!)
 var currentCanvass = null;
 var currentMarker = null;
 
 // Start walking a canvass
 function startCanvass(canvass, turf) {
+  debug("startCanvass(..., ...)");
+  debug(canvass);
+  debug(turf);
   setLoading(1);
-  map.on("moveend", loadVoters.bind(null, false, window.location.pathname.indexOf("research.php") == -1));
+  map.on("moveend", loadVoters.bind(null, false, false));
   loadTurfs();
   var json = turf.json;
-  var turfPoints = JSON.parse(json);
+  var turfPoints = JSON.parse(json).geometry.coordinates[0];
   var latTot = 0;
   var lonTot = 0;
   var i = 0;
   for(i = 0; i < turfPoints.length; i++) {
-    latTot += turfPoints[i].lat;
-    lonTot += turfPoints[i].lng;
+    latTot += turfPoints[i][1];
+    lonTot += turfPoints[i][0];
   }
 
   map.setZoom(19);
   map.setView([latTot/i, lonTot/i]);
   enableCanvassControls();
   currentCanvass = canvass;
+//  turfLayer = turf;
+  setTimeout(toggleTurf.bind(null, turf, true), 1000);
   viewCanvass(canvass);
 
   var options = {
@@ -389,16 +425,21 @@ function startCanvass(canvass, turf) {
   }
   fetch("api.php?set=canvass", options).then(data => data.json())
     .then(resp => {
-      loadVoters(false, window.location.pathname.indexOf("research.php") == -1);
+      loadVoters(false, false);
+      debug("End Async startCanvass");
   });
 
 
   toggleControls();
   setLoading(-1);
+  debug("End Sync startCanvass");
 }
 
 // Delete a canvass
 function deleteCanvass(canvass, turf) {
+  debug("deleteCanvass(..., ...)");
+  debug(canvass);
+  debug(turf);
   setLoading(1);
 
   var options = {
@@ -411,12 +452,16 @@ function deleteCanvass(canvass, turf) {
     console.log(resp);
     loadTurfs(true);
     setLoading(-1);
+    debug("End Async deleteCanvass");
   });
+  debug("End Sync deleteCanvass");
 }
 
 
 //
 function endCanvass(canvass) {
+  debug("endCanvass(...)");
+  debug(canvass);
   setLoading(1);
 
   var options = {
@@ -428,15 +473,19 @@ function endCanvass(canvass) {
   fetch("api.php?set=canvass", options).then(data => data.json())
     .then(resp => {
     console.log(resp);
+    debug("End Async endCanvass");
   });
   loadTurfs();
 
   setLoading(-1);
+  debug("End Sync endCanvass");
 }
 
 //
 function updateLocation(position) {
 //alert("CHECK2");
+  debug("updateLocation(...)");
+  debug(position);
   map.setView([position.coords.latitude, position.coords.longitude ])
   if(currentMarker != null) {
     map.removeLayer(currentMarker);
@@ -450,24 +499,32 @@ function updateLocation(position) {
   currentMarker = new L.marker([position.coords.latitude, position.coords.longitude], {icon: markerIcon, persist: true}).addTo(map);
   setTimeout( function() { viewCanvass(currentCanvass); }, 1000);
 //  setTimeout( function() { navigator.geolocation.getCurrentPosition(updateLocation); }, 60000);
+  debug("End updateLocation");
 }
 
 //
 function locationFailure(arg) {
+  debug("locationFailure("+arg+")");
   toastMessage("Unable to update location:<br/>"+arg.message)
   navigator.geolocation.clearWatch(geoWatcher);
   this.classList.remove("watching");
   geoWatcher = null;
+  debug("End locationFailure");
 }
 
 function toggleControls() {
+  debug("toggleControls()");
   enableCanvassControls();
+  debug("End toggleControls()");
 }
 
 //
 var geoWatcher = null;
+var addrErr = null;
 function enableCanvassControls() {
+  debug("enableCanvassControls()");
   if( document.getElementById("canvassToolbar") != null) {
+    debug("End enableCanvassControls (Early)");
     return;
   }
 
@@ -483,6 +540,11 @@ function enableCanvassControls() {
         position.coords.latitude = e.geocode.center.lat;
         position.coords.longitude = e.geocode.center.lng;
         updateLocation(position);
+
+        if(this.manual) {
+          clearTimeout(addrErr);
+          document.getElementById("canvassToolbar").classList.remove("addrInput");
+        }
       })
       .addTo(map);
     var geoctl = document.getElementsByClassName("leaflet-control-geocoder")[0];
@@ -496,7 +558,7 @@ function enableCanvassControls() {
   gpsBtn.onclick = function() {
 //    navigator.geolocation.getCurrentPosition(updateLocation, locationFailure);
     if(geoWatcher == null) {
-      geoWatcher = navigator.geolocation.watchPosition(updateLocation, locationFailure);
+      geoWatcher = navigator.geolocation.watchPosition(updateLocation, locationFailure, {maximumAge: 30000});
       this.classList.add("watching");
     } else {
       navigator.geolocation.clearWatch(geoWatcher);
@@ -531,6 +593,15 @@ function enableCanvassControls() {
   addrInput.addEventListener("keypress", function(event) {
     if (event.key === "Enter") {
       document.getElementById("geocoderAddrInputSubmit").click();
+/*      var err = function() {
+        alert("No results found");
+        document.getElementById("geocoderAddrInput").classList.add("error");
+        var unerr = function() {
+          document.getElementById("geocoderAddrInput").classList.remove("error");
+        }
+        setTimeout(unerr, 100);
+      }
+      addrErr = setTimeout(err, 1000);*/
     }
   });
   toolbar.appendChild(addrInput);
@@ -540,16 +611,40 @@ function enableCanvassControls() {
   addrInputSubmit.value = "Search";
   addrInputSubmit.setAttribute("id", "geocoderAddrInputSubmit");
   addrInputSubmit.onclick = function(cdr) {
+    // Set an error on a timer
+    // If the search hits, it will clear the timer
+    // (Can't find a better way to do this...I tried!)
+    var err = function() {
+      alert("No results found");
+/*      document.getElementById("geocoderAddrInput").classList.add("error");
+      var unerr = function() {
+        document.getElementById("geocoderAddrInput").classList.remove("error");
+      }
+      setTimeout(unerr, 100);*/
+    }
+    addrErr = setTimeout(err, 1000);
+
     cdr.setQuery(document.getElementById("geocoderAddrInput").value);
     cdr._geocode();
-    document.getElementById("canvassToolbar").classList.remove("addrInput");
+    cdr.manual = true;
+//    document.getElementById("canvassToolbar").classList.remove("addrInput");
   }.bind(null, coder);
   toolbar.appendChild(addrInputSubmit);
 
+  var addrInputClose = document.createElement("button");
+//  addrInputSubmit.setAttribute("type", "button");
+  addrInputClose.innerText = "X";
+  addrInputClose.setAttribute("id", "geocoderAddrInputClose");
+  addrInputClose.onclick = function() {
+    document.getElementById("canvassToolbar").classList.remove("addrInput");
+  }
+  toolbar.appendChild(addrInputClose);
+
+
   var addBtn = document.createElement("button");
   // Without the setTimeout here it triggers the map click *after* the button click
   // ...which drops the pin immediately, hidden behind the button
-  addBtn.onclick = function() { setTimeout(manualAdd, 10); };
+  addBtn.onclick = manualDetails.bind(null, null); //function() { setTimeout(manualAdd, 10); };
   var addImg = document.createElement("img");
   addImg.src = "images/ManualAdd.png";
   addBtn.appendChild(addImg);
@@ -558,7 +653,7 @@ function enableCanvassControls() {
 
   var refreshBtn = document.createElement("button");
   refreshBtn.onclick = function() {
-    loadVoters(true, window.location.pathname.indexOf("research.php") == -1);
+    loadVoters(true, false);
   }
   var refreshImg = document.createElement("img");
   refreshImg.src = "images/RefreshVoters.png";
@@ -571,6 +666,7 @@ function enableCanvassControls() {
     document.getElementById("details").style.display = "block";
     document.getElementById("map").style.display = "none";
   }
+  lastBtn.setAttribute("disabled", "true");
   var lastImg = document.createElement("img");
   lastImg.src = "images/LastVoter.png";
   lastBtn.appendChild(lastImg);
@@ -589,19 +685,24 @@ function enableCanvassControls() {
   }
   menuBtn.setAttribute("id", "geocoderMenuBtn");
   var menuImg = document.createElement("img");
-  menuImg.src = "images/Menu.png";
+  menuImg.src = "images/Home.png";
   menuBtn.appendChild(menuImg);
   toolbar.appendChild(menuBtn);
 
   map.invalidateSize();
   document.getElementById("map").appendChild(toolbar);
+  debug("End enableCanvassControls");
 }
 
 function manualAdd() {
+  debug("manualAdd()");
   map.on('click', manualAddClick);
+  debug("End manualAdd()");
 }
 
 function manualAddClick(e) {
+  debug("manualAddClick(...)");
+  debug(e);
   var markerIcon = L.icon({
     iconUrl: "images/marker-icon-white.png",
     iconSize: [24, 36],
@@ -617,9 +718,12 @@ function manualAddClick(e) {
   marker.on('click', manualDetails.bind(null, marker));
   map.addLayer(marker);
   map.off('click');
+  debug("End manualAddClick");
 }
 
 function manualDetails(marker) {
+  debug("manualDetails(...)");
+  debug(marker);
   var results = Object();
   results.firstName  = "unknown";
   results.middleName = "";
@@ -633,16 +737,23 @@ function manualDetails(marker) {
   results.city    = null;
   results.state   = "RI";
   results.zip     = null;
-  results.lat     = marker._latlng.lat;
-  results.lon     = marker._latlng.lng;
+  if(marker != null && marker._latlng != null) {
+    results.lat     = marker._latlng.lat;
+    results.lon     = marker._latlng.lng;
+  }
 
   showVoterInfo(results, null, null, true);
 
-  updateMarker(marker);
-  document.getElementById("voterDetailsparty").onchange = updateMarker.bind(null, marker);
+  if(marker != null) {
+    updateMarker(marker);
+    document.getElementById("voterDetailsparty").onchange = updateMarker.bind(null, marker);
+  }
+  debug("End manualDetails");
 }
 
 function updateMarker(marker) {
+  debug("updateMarker(...)");
+  debug(marker);
   var party = document.getElementById("voterDetailsparty").value;
   var url = "images/marker-icon-";
   if(party == "Democrat") {
@@ -660,14 +771,18 @@ function updateMarker(marker) {
     iconAnchor: [12,36]
   });
   marker.setIcon(markerIcon);
+  debug("End updateMarker");
 }
 
 function showCanvassStats(id, count, page=0) {
+  debug("showCanvassStats("+id+", "+count+", "+page+")");
   if( typeof page == 'object' ) {
     page = 0;
   }
   fetch("api.php?get=canvassResults&id="+id+"&page="+page+"&pageSize=50").then(data => data.json())
-    .then(resp => {
+    .then(output => {
+
+    resp = output.dataset;
     // Clear any existing voter details from the panel
     var detailsPane = document.getElementById("details");
     detailsPane.innerHTML = '<table id="detailsTable"></table>\
@@ -694,9 +809,9 @@ function showCanvassStats(id, count, page=0) {
     var cell = document.createElement("th");
     cell.innerHTML = "Contact Method";
     row.appendChild(cell);
-    var cell = document.createElement("th");
+/*    var cell = document.createElement("th");
     cell.innerHTML = "Est. Support";
-    row.appendChild(cell);
+    row.appendChild(cell);*/
     var cell = document.createElement("th");
     cell.innerHTML = "Corrections";
     row.appendChild(cell);
@@ -715,9 +830,9 @@ function showCanvassStats(id, count, page=0) {
     var cell = document.createElement("th");
     cell.innerHTML = "Birthdate";
     row.appendChild(cell);
-    var cell = document.createElement("th");
+/*    var cell = document.createElement("th");
     cell.innerHTML = "Sex";
-    row.appendChild(cell);
+    row.appendChild(cell);*/
     var cell = document.createElement("th");
     cell.innerHTML = "Party";
     row.appendChild(cell);
@@ -741,13 +856,17 @@ function showCanvassStats(id, count, page=0) {
     row.appendChild(cell);
     table.appendChild(row);
 
+    var script = output.script;
+    var row = document.createElement("tr");
+
     for(var i = 0; i < resp.length; i++) {
+console.log("CHECKING: "+i);
       var row = document.createElement("tr");
       var cell = document.createElement("td");
-      cell.innerHTML = new Date(parseInt(resp[i].timestamp)).toLocaleString();
+      cell.innerHTML = resp[i].timestamp;
       row.appendChild(cell);
       var cell = document.createElement("td");
-      cell.innerHTML = resp[i].contactUser;
+      cell.innerHTML = resp[i].userId;
       row.appendChild(cell);
       var cell = document.createElement("td");
       cell.innerHTML = resp[i].contactStatus;
@@ -755,9 +874,9 @@ function showCanvassStats(id, count, page=0) {
       var cell = document.createElement("td");
       cell.innerHTML = resp[i].contactMethod;
       row.appendChild(cell);
-      var cell = document.createElement("td");
+/*      var cell = document.createElement("td");
       cell.innerHTML = resp[i].estSupportPct;
-      row.appendChild(cell);
+      row.appendChild(cell);*/
       var cell = document.createElement("td");
       cell.innerHTML = resp[i].correctionsNeeded;
       row.appendChild(cell);
@@ -774,11 +893,11 @@ function showCanvassStats(id, count, page=0) {
       cell.innerHTML = resp[i].name;
       row.appendChild(cell);
       var cell = document.createElement("td");
-      cell.innerHTML = resp[i].birthdate;
+      cell.innerHTML = resp[i].birthyear;
       row.appendChild(cell);
-      var cell = document.createElement("td");
+/*      var cell = document.createElement("td");
       cell.innerHTML = resp[i].sex;
-      row.appendChild(cell);
+      row.appendChild(cell);*/
       var cell = document.createElement("td");
       cell.innerHTML = resp[i].party;
       row.appendChild(cell);
@@ -802,6 +921,7 @@ function showCanvassStats(id, count, page=0) {
       row.appendChild(cell);
       table.appendChild(row);
     }
+console.log("AT PAGINATOR");
 
     var paginator = document.createElement("div");
     paginator.setAttribute("id", "paginator");
@@ -838,7 +958,9 @@ function showCanvassStats(id, count, page=0) {
     paginator.appendChild(downLink);
 
     document.getElementById("detailsControls").prepend(paginator);
+    debug("End Async showCanvassStats");
   });
+  debug("End Sync showCanvassStats");
 }
 
 
index 629f924..2aecf62 100644 (file)
@@ -64,6 +64,14 @@ TODO: Comment code more, especially:
 * If not, see <https://www.gnu.org/licenses/>.
 */
 
+var _CCCP_DEBUG = true;
+function debug(msg) {
+  if(_CCCP_DEBUG) {
+//    debugger;
+    console.log(msg);
+  }
+}
+
 // Stores data associated to precincts, blocks, or block groups for display
 var displayAreas = [];
 
@@ -74,7 +82,16 @@ var colors = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF", "#FF00FF",
 
 // INITIALIZATION
 ///////////////////////////////////////////////////////////
+var initTimeout = 10;
 function init() {
+  debug("init()");
+
+  if( typeof L == "undefined" && initTimeout > 0) {
+    setTimeout(init, 500);
+    initTimeout--;
+    return;
+  }
+
   // Initialize the map -- LeafletJS boilerplate
   map = L.map('map').setView([41.5, -71.5 ], 10);
   L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png' , { maxZoom: 25}).addTo(map);
@@ -104,6 +121,7 @@ function init() {
 // Show/Hide Loading Indicator
 var loadingElems = 0;
 function setLoading(items) {
+  debug("setLoading("+items+")");
   loadingElems += items;
   var indicators = document.getElementsByClassName("loadingIndicator");
   if(loadingElems == 0) {
@@ -124,27 +142,40 @@ function setLoading(items) {
 
 // Show/hide one of the major categories
 function toggleList(e) {
+  debug("toggleList(...)");
+  debug(e);
+
+  if(e.target.class == "toggle")
+    var target = e.target;
+  else
+    var target = e.target.parentElement.getElementsByClassName("toggle")[0];
+
   var list = e.target.parentElement.getElementsByClassName("list")[0];
   if(list.style.display != "block") {
     list.style.display = "block";
-    e.target.innerHTML = "-";
+    target.innerHTML = "-";
   } else {
     list.style.display = "none";
-    e.target.innerHTML = "+";
+    target.innerHTML = "+";
   }
 }
 
 // Return to the map from stats or voter details
 function restoreMap() {
+  debug("restoreMap()");
   if(document.getElementById("paginator") != null) {
     document.getElementById("controls").style.display = "block";
   }
   document.getElementById("details").style.display = "none";
   document.getElementById("map").style.display = "block";
+  map.invalidateSize();
+  loadVoters(true, false);
 }
 
 // Utility function to complete geojsons taken from the database
 function geoJsonBuilder(jsonStr) {
+  debug("geoJsonBuilder(...)");
+  debug(jsonStr);
   var geojs = Object();
   geojs.type = "FeatureCollection";
   geojs.name = "demoArea_";
@@ -175,6 +206,8 @@ function geoJsonBuilder(jsonStr) {
 
 // Show a "toast" style notification
 function toastMessage(message) {
+  debug("toastMessage(...)");
+  debug(message);
   var toastDiv = document.createElement("div");
   toastDiv.innerHTML = message;
   toastDiv.setAttribute("id", "toastDiv");
@@ -190,6 +223,7 @@ function toastMessage(message) {
 
 // Toggle map display
 function toggleControls() {
+  debug("toggleControls()");
   var controlElem = document.getElementById("controls");
   var mapElem = document.getElementById("map");
   if( controlElem.getAttribute("class") == "min") {
index 01a9447..fe02f14 100644 (file)
@@ -16,6 +16,7 @@
 
 // Load the demographics categories from the database
 function loadDemographics() {
+  debug("loadDemographics()");
   setLoading(1);
 
   // Clear any existing displays
@@ -81,12 +82,16 @@ function loadDemographics() {
       // Re-enable the type select when done
       document.getElementById("demoType").removeAttribute("disabled");
       setLoading(-1);
+      debug("End Async loadDemographics");
   });
+  debug("End Sync loadDemographics");
 }
 
 // TODO: comment; clear or re-check when changing modes
 // Enable/disable display of demographic stats
 function toggleDemo(demoId, colorId, event) {
+  debug("toggleDemo("+demoId+", "+colorId+", ...)");
+  debug(event);
   setLoading(1);
   fetch("api.php?get=demos&id="+demoId).then(data => data.json())
     .then(demos => {
@@ -183,11 +188,14 @@ function toggleDemo(demoId, colorId, event) {
         }
       }
       setLoading(-1);
+      debug("End Async toggleDemo");
     });
+  debug("End Sync toggleDemo");
 }
 
 // Update the transparency of demographics areas (onchange event from the slider)
 function updateDemoTransparency() {
+  debug("updateDemoTransparency()");
   // Match the selected type to the displayAreas list for that type
   for(var das = 0; das < displayAreas.length; das++) {
     if(displayAreas[das][0].type == document.getElementById("demoType").value) {
@@ -219,4 +227,5 @@ function updateDemoTransparency() {
       displayAreas[das][da].mapElem.setStyle({fillOpacity: opacity, strokeOpacity: opacity});
     }
   }
+  debug("End updateDemoTransparency");
 }
index 3b5ed92..f71c28b 100644 (file)
@@ -16,6 +16,7 @@
 
 // Populate the list of district types in the menu
 function loadDistrictTypes() {
+  debug("loadDistrictTypes()");
   setLoading(1);
   fetch("api.php?get=district").then(data => data.json())
     .then(districtTypes => {
@@ -29,11 +30,14 @@ function loadDistrictTypes() {
       }
       loadDistricts();
       setLoading(-1);
+      debug("End Async loadDistrictTypes");
   });
+  debug("End Sync loadDistrictTypes");
 }
 
 // Populate the list of specific districts
 function loadDistricts() {
+  debug("loadDistricts()");
   setLoading(1);
   var type = document.getElementById("districtTypeSel").value;
   fetch("api.php?get=district&type="+type).then(data => data.json())
@@ -47,12 +51,15 @@ function loadDistricts() {
         selector.appendChild(option);
       }
       setLoading(-1);
+      debug("End Async loadDistricts");
   });
+  debug("End Sync loadDistricts");
 }
 
 // Enable/disable the display of a particular district
 displayDistricts = Array();
 function showDistrict(targetState) {
+  debug("showDistricts("+targetState+")");
   setLoading(1);
   var districtId = document.getElementById("districtSel").value;
   var type = document.getElementById("districtTypeSel").value;
@@ -98,20 +105,24 @@ function showDistrict(targetState) {
         document.getElementById("districtsTable").appendChild(row);
       }
       setLoading(-1);
+      debug("End Async showDistricts");
   });
+  debug("End Sync showDistricts");
 }
 
 function hideDistrict(districtId) {
-console.log(districtId);
+  debug("hideDistrict("+districtId+")");
   if(displayDistricts[districtId] != null) {
     map.removeLayer(displayDistricts[districtId].mapElem);
     displayDistricts[districtId].mapElem = null;
     this.parentElement.removeChild(this);
   }
+  debug("End hideDistrict");
 }
 
 // Update the transparency of district areas (onchange event from the slider)
 function updateDistrictTransparency() {
+  debug("updateDistrictTransparency()");
   var districtKeys = Object.keys(displayDistricts);
   for(var i = 0; i < districtKeys.length; i++) {
     if(displayDistricts[districtKeys[i]].mapElem != null) {
@@ -119,5 +130,6 @@ function updateDistrictTransparency() {
       displayDistricts[districtKeys[i]].mapElem.setStyle({opacity: opacity, fillOpacity: opacity, weight: 2, strokeOpacity: 1});
     }
   }
+  debug("End updateDistrictTransparency");
 }
 
index bb05d37..3cd2647 100644 (file)
@@ -17,6 +17,7 @@
 
 // Populate the list of precincts in the menu
 function loadPrecincts() {
+  debug("loadPrecincts()");
   fetch("api.php?get=precincts").then(data => data.json())
     .then(precinctList =>  {
        var cities = Object.keys(precinctList);
@@ -60,11 +61,15 @@ function loadPrecincts() {
          document.getElementById("precinct-list").appendChild(cityClr);
          document.getElementById("precinct-list").appendChild(cityList);
        }
+      debug("End Async loadPrecincts");
     });
+  debug("End Sync loadPrecincts");
 }
 
 // Enable/disable a precinct displayed on the map
 function togglePrecincts(pids, colorId=null, targetState = null) {
+  debug("togglePrecincts(..., "+colorId+", "+targetState+")");
+  debug(pids);
   setLoading(1);
   console.log("togglePrecincts");
   if(colorId != null) {
@@ -114,10 +119,12 @@ console.log(geojs);
              togglePrecincts.bind(null, pids, colorId, !targetState);
   }
   setLoading(-1);
+  debug("End togglePrecincts");
 }
 
 // Update the transparency of precinct areas (onchange event from the slider)
 function updatePrecinctTransparency() {
+  debug("updatePrecinctTransparency()");
   // Find the displayAreas list for precincts
   for(var das = 0; das < displayAreas.length; das++) {
     if(displayAreas[das][0].type == "precinct") {
@@ -131,4 +138,5 @@ function updatePrecinctTransparency() {
       displayAreas[das][i].mapElem.setStyle({opacity: opacity, fillOpacity: opacity});
     }
   }
+  debug("End updatePrecinctTransparency");
 }
index 3ba2285..cf503f2 100644 (file)
 // Load and display the selected voters
 var lastLoc = {};
 function loadVoters(force = false, pending) {
+  debug("loadVoters("+force+", "+pending+")");
   setLoading(1);
 
+  // Sometimes on mobile you can accidentally zoom the page instead of the map
+  // Then all the buttons get lost and it's a mess to restore
+  // But this call with trigger with some scrolling so that should help!
+  document.body.style.zoom = "100%";
 
+  mapHeight = Math.abs(map.getBounds()._southWest.lat - map.getBounds()._northEast.lat);
+  mapWidth  = Math.abs(map.getBounds()._southWest.lng - map.getBounds()._northEast.lng);
   if(lastLoc.lat > map.getBounds()._southWest.lat &&
      lastLoc.lat < map.getBounds()._northEast.lat &&
      lastLoc.lng > map.getBounds()._southWest.lng &&
      lastLoc.lng < map.getBounds()._northEast.lng &&
      force == false) {
-    console.log("SKIP REFRESH");
     setLoading(-1);
     return;
   }
@@ -34,7 +40,8 @@ function loadVoters(force = false, pending) {
   // Clear any existing pins on the map
   var layerKeys = Object.keys(map._layers);
   for(var i = 0; i < layerKeys.length; i++) {
-    if(typeof map._layers[layerKeys[i]].options.icon != "undefined" &&
+    if(typeof map._layers[layerKeys[i]] != "undefined" &&
+       typeof map._layers[layerKeys[i]].options.icon != "undefined" &&
               map._layers[layerKeys[i]].options.persist != true) {
       map.removeLayer(map._layers[layerKeys[i]]);
     }
@@ -42,10 +49,10 @@ function loadVoters(force = false, pending) {
 
   // Build the request parameters
   var latlon = "";
-  var minLat = map.getBounds()._southWest.lat;
-  var minLon = map.getBounds()._southWest.lng;
-  var maxLat = map.getBounds()._northEast.lat;
-  var maxLon = map.getBounds()._northEast.lng;
+  var minLat = map.getBounds()._southWest.lat - mapHeight/4;
+  var minLon = map.getBounds()._southWest.lng - mapWidth/4;
+  var maxLat = map.getBounds()._northEast.lat + mapHeight/4;
+  var maxLon = map.getBounds()._northEast.lng + mapWidth/4;
   if(turfLayer != null) {
     if(minLat < turfLayer.getBounds()._southWest.lat) {
       minLat = turfLayer.getBounds()._southWest.lat;
@@ -71,11 +78,17 @@ function loadVoters(force = false, pending) {
   if(pending) {
     pendingStr = "&pending";
   }
+
+  var turfStr = "";
+  if(currentCanvass != null) {
+    turfStr = "&turfId="+currentCanvass.turfId;
+  }
+
   // Send the request and drop the map markers
   var options = {
     method: "POST",
     headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
-    body: getVoterString()+latlon+pendingStr
+    body: getVoterString()+latlon+pendingStr+turfStr
   }
   fetch("api.php?get=voterLocs", options).then(data => data.json())
     .then(locObj => {
@@ -105,23 +118,58 @@ function loadVoters(force = false, pending) {
       if(locList[i].icon) {
         iconImg = locList[i].icon;
       }
+      if(locList[i].contacted != "0") {
+        iconImg = "marker-icon-gray.png";
+      }
       var markerIcon = L.icon({
         iconUrl: "images/"+iconImg,
-        iconSize: [24, 36],
-        iconAnchor: [12,36],
+        iconSize: [48, 72],
+        iconAnchor: [24,72],
+        className: 'leaflet-marker'
       });
      var markerTitle = locList[i].addressLine1 + " ("+locList[i].count+" voters)";
      var newMarker = new L.marker([locList[i].latitude, locList[i].longitude],
                                   {icon: markerIcon, title: markerTitle}).addTo(map);
+     if( locList[i].count > 1) {
+       newMarker.bindTooltip(locList[i].count+"",
+         {
+           permanent: true,
+           direction: 'right',
+           className: 'leaflet-voterCount'
+         }
+       );
+     }
+     /*var markerText = new L.Marker([locList[i].latitude, locList[i].longitude], {
+                                  icon: new L.DivIcon({
+        className: 'my-div-icon',
+        html: '<img src="images/marker-icon-tricolor.png"></img><span class="my-div-span">---'+locList[i].count+'-------</span>'
+    })
+});*/
      newMarker.on('click', voterList.bind(null, locList[i].latitude, locList[i].longitude));
     }
 
     setLoading(-1);
+    debug("End Async loadVoters");
   });
+
+  // Ensure any turf is displaying
+  function showTurf() {
+    if(turfLayer != null) {
+      toggleTurf(turfLayer, true, null);
+    }
+  }
+
+  // Race condition mostly seen on mobile means this doesn't always have the turf right away
+//  showTurf();
+  setTimeout(showTurf, 1000);
+  debug("End Sync loadVoters");
 }
 
+
 // Prepare a list of voters for the details screen
 function voterList(lat, lon, event) {
+  debug("voterList("+lat+", "+lon+", ...)");
+  debug(event);
   setLoading(1);
   var options = {
     method: "POST",
@@ -152,12 +200,22 @@ function voterList(lat, lon, event) {
         var id = document.getElementById("voterSel").value;
         voterDetails(id);
       };
+
+      if( details.length == 1 ) {
+        select.disabled = true;
+      } else {
+        select.disabled = null;
+      }
+      debug("End Async voterList");
   });
   setLoading(-1);
+  debug("End Sync voterList");
 }
 
 // Show details of a voter when clicking their map pin
 function voterDetails(id, event) {
+  debug("voterDetails("+id+", ...)");
+  debug(event);
   setLoading(1);
   showVoterInfo(null, null, null, false);
   fetch("api.php?get=voterDetails&id="+id).then(data => data.json())
@@ -171,11 +229,22 @@ function voterDetails(id, event) {
       custom = details.custom;
       showVoterInfo(voter, results, custom, false);
       setLoading(-1);
+      debug("End Async voterDetails");
   });
+  debug("End Sync voterDetails");
 }
 
 //
 function showVoterInfo(details, results, custom, editable) {
+  debug("showVoterInfo(...,...,...,...)");
+  debug(details);
+  debug(results);
+  debug(custom);
+  debug(editable);
+
+  if(document.getElementById("geocoderLastBtn") != null) {
+    document.getElementById("geocoderLastBtn").removeAttribute("disabled");
+  }
       document.getElementById("details").style.display = "block";
       document.getElementById("map").style.display = "none";
 
@@ -274,13 +343,13 @@ function showVoterInfo(details, results, custom, editable) {
       voterId.value = details.id;
       controls.appendChild(voterId);
 
-      var value = results.estSupportPct;
+/*      var value = results.estSupportPct;
       if(value == undefined) {
         value = 0;
       }
       var supportLabel = document.createElement("label");
       supportLabel.setAttribute("for", "voterSupportRange");
-      supportLabel.innerHTML = "Estimated Support Level:";
+      supportLabel.innerHTML = "Estimated Support Level:";*/
 /*      var supportRange = document.createElement("input");
       supportRange.setAttribute("id", "voterSupportRange");
       supportRange.setAttribute("type", "range");
@@ -299,15 +368,16 @@ function showVoterInfo(details, results, custom, editable) {
       controls.appendChild(supportLabel);
       controls.appendChild(supportRange);
       controls.appendChild(supportText);*/
-      var supportList = document.createElement("select");
+
+/*      var supportList = document.createElement("select");
       supportList.setAttribute("id", "voterSupportRange");
 
       var supportOpt = document.createElement("option");
-      supportOpt.innerHTML = "Extremely Opposed";
+      supportOpt.innerHTML = "Definitely Opposed";
       supportOpt.value = -100;
       supportList.appendChild(supportOpt);
       supportOpt = document.createElement("option");
-      supportOpt.innerHTML = "Very Opposed";
+      supportOpt.innerHTML = "Somewhat Opposed";
       supportOpt.value = -60;
       supportList.appendChild(supportOpt);
       supportOpt = document.createElement("option");
@@ -315,7 +385,7 @@ function showVoterInfo(details, results, custom, editable) {
       supportOpt.value = -30;
       supportList.appendChild(supportOpt);
       supportOpt = document.createElement("option");
-      supportOpt.innerHTML = "Neutral";
+      supportOpt.innerHTML = "Unconvinced";
       supportOpt.value = 0;
       supportList.appendChild(supportOpt);
       supportOpt = document.createElement("option");
@@ -323,11 +393,11 @@ function showVoterInfo(details, results, custom, editable) {
       supportOpt.value = 30;
       supportList.appendChild(supportOpt);
       supportOpt = document.createElement("option");
-      supportOpt.innerHTML = "Very Supportive";
+      supportOpt.innerHTML = "Somewhat Support";
       supportOpt.value = 60;
       supportList.appendChild(supportOpt);
       supportOpt = document.createElement("option");
-      supportOpt.innerHTML = "Extremely Supportive";
+      supportOpt.innerHTML = "Definite Support";
       supportOpt.value = 100;
       supportList.appendChild(supportOpt);
       supportList.onchange = function() {
@@ -340,6 +410,73 @@ function showVoterInfo(details, results, custom, editable) {
         if(supportList.children[j].value <= value) {
           supportList.value = supportList.children[j].value;
         }
+      }*/
+
+      if( results.json != null ) {
+        var responses = JSON.parse(results.json);
+      } else {
+        var responses = [];
+      }
+
+      if( currentCanvass != null && currentCanvass.script != null ) {
+        var script = JSON.parse(currentCanvass.script);
+        var introLabel = document.createElement("label");
+        introLabel.setAttribute("for", "intro");
+        introLabel.innerHTML = "Intro:";
+        var introElem = document.createElement("p");
+        introElem.setAttribute("id", "intro");
+        introElem.innerText = script.intro;
+        controls.appendChild(introLabel);
+        controls.appendChild(introElem);
+        for(var i = 0; i < script.prompts.length; i++) {
+          var promptLabel = document.createElement("label");
+          promptLabel.setAttribute("for", "prompt"+i);
+          promptLabel.setAttribute("id", "promptLabel"+i);
+          promptLabel.innerText = script.prompts[i].prompt;
+          promptLabel.setAttribute("class", "promptLabel");
+          var promptNotes = document.createElement("p");
+          promptNotes.innerText = script.prompts[i].notes;
+          promptNotes.setAttribute("class", "promptNotes");
+          controls.appendChild(promptLabel);
+          controls.appendChild(promptNotes);
+          if(script.prompts[i].input.type == "text") {
+            var promptElem = document.createElement("textarea");
+            promptElem.setAttribute("id", "prompt"+i);
+            promptElem.setAttribute("title", script.prompts[i].promptId);
+            promptElem.oninput = setVoterSaveStatus.bind(null,"unsaved");
+            var val = null;
+            for(var j = 0; j < responses.length; j++) {
+              if(responses[j].promptId == script.prompts[i].promptId) {
+                val = responses[j].value;
+              }
+            }
+            promptElem.value = val;
+            controls.appendChild(promptElem);
+          } else if(script.prompts[i].input.type == "select") {
+            var promptElem = document.createElement("select");
+            promptElem.setAttribute("id", "prompt"+i);
+            promptElem.setAttribute("title", script.prompts[i].promptId);
+            promptElem.oninput = setVoterSaveStatus.bind(null,"unsaved");
+            var val = null;
+            for(var j = 0; j < responses.length; j++) {
+              if(responses[j].promptId == script.prompts[i].promptId) {
+                val = responses[j].value;
+              }
+            }
+            var opts = script.prompts[i].input.options;
+            for(var j = 0; j < opts.length; j++) {
+              var optElem = document.createElement("option");
+              optElem.innerText = opts[j].text;
+              optElem.value     = opts[j].value;
+              if(opts[j].value == val) {
+                optElem.selected = true;
+              }
+              promptElem.appendChild(optElem);
+            }
+            opts.value = val;
+            controls.appendChild(promptElem);
+          }
+        }
       }
 
       var notesLabel = document.createElement("label");
@@ -347,12 +484,12 @@ function showVoterInfo(details, results, custom, editable) {
       notesLabel.innerHTML = "Additional Notes:";
       var notes = document.createElement("textarea");
       notes.setAttribute("id", "voterNotes");
-      if(results.notes == "undefined") {
+      if(results.notes == "undefined" || typeof results.notes == "undefined") {
         notes.innerHTML = "";
       } else {
         notes.innerHTML = results.notes;
       }
-      notes.onchange = function() { document.getElementById("voterSave").classList.add("unsaved"); };
+      notes.oninput = setVoterSaveStatus.bind(null,"unsaved");
       controls.appendChild(notesLabel);
       controls.appendChild(notes);
 
@@ -362,7 +499,7 @@ function showVoterInfo(details, results, custom, editable) {
       var checkbox = document.createElement("input");
       checkbox.setAttribute("type", "checkbox");
       checkbox.id = "correctionsCheck";
-      checkbox.onchange = function() { document.getElementById("voterSave").classList.add("unsaved"); };
+      checkbox.onchange = setVoterSaveStatus.bind(null,"unsaved");
       if(results.corrections == 1) {
         checkbox.setAttribute("checked", true);
       }
@@ -381,7 +518,7 @@ function showVoterInfo(details, results, custom, editable) {
       var cell = document.createElement("td");
       var checkbox = document.createElement("input");
       checkbox.setAttribute("type", "checkbox");
-      checkbox.onchange = function() { document.getElementById("voterSave").classList.add("unsaved"); };
+      checkbox.onchange = setVoterSaveStatus.bind(null,"unsaved");
       if(results.priority == 1) {
                checkbox.setAttribute("checked", true);
       }
@@ -402,7 +539,7 @@ function showVoterInfo(details, results, custom, editable) {
       var checkbox = document.createElement("input");
       checkbox.setAttribute("type", "checkbox");
       checkbox.id = "noContactCheck";
-      checkbox.onchange = function() { document.getElementById("voterSave").classList.add("unsaved"); };
+      checkbox.onchange = setVoterSaveStatus.bind(null,"unsaved");
       if(results.dnc == 1) {
                checkbox.setAttribute("checked", true);
       }
@@ -426,6 +563,10 @@ function showVoterInfo(details, results, custom, editable) {
 //      cell.appendChild(save);
 //      row.appendChild(cell);
 
+      var status = document.createElement("div");
+      status.innerHTML = "";
+      status.setAttribute("id", "voterSaveStatus");
+
 //      var cell = document.createElement("td");
       var back = document.createElement("button");
       back.innerHTML = "back";
@@ -438,23 +579,47 @@ function showVoterInfo(details, results, custom, editable) {
 
       controls.appendChild(flagTable);
       controls.appendChild(save);
+      controls.appendChild(status);
       controls.appendChild(back);
       var mr = document.getElementById("mapReturn");
 
       if(mr != null) {
         mr.parentElement.removeChild(mr);
       }
+  debug("End showVoterInfo");
+}
+
+// Called on any input change to set the status
+function setVoterSaveStatus(status) {
+console.log(status);
+  document.getElementById("voterSaveStatus").classList.remove("unsaved");
+  document.getElementById("voterSaveStatus").classList.remove("saved");
+  document.getElementById("voterSaveStatus").classList.remove("error");
+
+  document.getElementById("voterSaveStatus").classList.add(status);
 }
 
 //
 function saveVoterDetails() {
+  debug("saveVoterDetails()");
   setLoading(1);
   var id = document.getElementById("voterId").value;
-  var supportPct = document.getElementById("voterSupportRange").value;
+//  var supportPct = document.getElementById("voterSupportRange").value;
   var notes = document.getElementById("voterNotes").value;
   var corrections = document.getElementById("correctionsCheck").checked;
   var priority = document.getElementById("priorityCheck").checked;
   var dnc = document.getElementById("noContactCheck").checked;
+
+  var prompts = [];
+  var i = 0;
+  while( document.getElementById("prompt"+i) != null && i < 20) {
+    var promptObj = {};
+    promptObj.promptId = document.getElementById("prompt"+i).title;
+    promptObj.value = document.getElementById("prompt"+i).value;
+    prompts.push(promptObj);
+    i++;
+  }
+
   var newEntryBody = "";
   var newEntryItems = document.getElementsByClassName("newVoterDetails");
   if(newEntryItems.length > 0) {
@@ -474,21 +639,33 @@ function saveVoterDetails() {
   var options = {
     method: "POST",
     headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
-    body: "id="+id+"&supportPct="+supportPct+"&notes="+encodeURIComponent(notes)+
+    body: "id="+id+"&notes="+encodeURIComponent(notes)+"&prompts="+encodeURIComponent(JSON.stringify(prompts))+
           "&corrections="+corrections+"&priority="+priority+"&dnc="+dnc+"&canvassId="+canvassId+
           newEntryBody
   };
   fetch("api.php?get=set&set=canvassResult", options).then(data => data.json())
     .then(resp => {
-      console.log(resp);
+      if(resp[0] == "00000") {
+        setVoterSaveStatus("saved");
+      } else {
+        setVoterSaveStatus("error");
+      }
+      map.invalidateSize();
+    })
+    .catch(error => {
+      setVoterSaveStatus("error");
+      document.getElementById("voterSaveStatus").classList.add("error");
+      console.log(error);
     });
 
-  document.getElementById("voterSave").classList.remove("unsaved");
+  //document.getElementById("voterSave").classList.remove("unsaved");
   setLoading(-1);
+  debug("End saveVoterDetails");
 }
 
 // Generate a string representing the voter selection
 function getVoterString() {
+  debug("getVoterString()");
   var parties = "";
   if(document.getElementById("voters-dem").checked) {
     parties += "Democrat";
@@ -541,11 +718,14 @@ function getVoterString() {
   }
   lists = lists.replace(/^,|,$/g, '');
 
+  debug("End getVoterString");
   return "parties="+parties+"&history="+history+"&affiliations="+affs+"&precincts="+precincts+"&lists="+lists;
 }
 
 // Configure voter selection from an existing voter string
 function setVoterString(voterStr) {
+  debug("setVoterString(...)");
+  debug(voterStr);
   // Clear all precincts so we can update
   var precinctChecks = document.getElementsByClassName("precinct-check");
   for(var i = 0; i < precinctChecks.length; i++) {
@@ -589,4 +769,5 @@ function setVoterString(voterStr) {
     }
   }
   loadVoters(true);
+  debug("End setVoterString");
 }
index 5b95547..56be889 100644 (file)
--- a/login.php
+++ b/login.php
@@ -36,6 +36,10 @@ $options = [
 $dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options);
 
 // Check if password is valid
+if( isset($_GET['username']) && isset($_GET['password']) ) {
+  $_POST['username'] = $_GET['username'];
+  $_POST['password'] = $_GET['password'];
+}
 if( isset($_POST) && sizeof($_POST) > 0 ) {
   $_POST['username'];
   $_POST['password'];
@@ -50,7 +54,13 @@ if( isset($_POST) && sizeof($_POST) > 0 ) {
     $_SESSION['userId']   = $row[0]['id'];
     $_SESSION['authtime'] = time();
     $_SESSION['permissions'] = $row[0]['permissions'];
+
+    if( isset($_GET['target']) && $_GET['target'] == "canvass" ) {
+      header("Location: ./canvass.php");
+      return;
+    }
     header("Location: ./index.php");
+    return;
   }
 
   $auth_error = "Unable to login.";
index fcdf870..29980df 100644 (file)
@@ -27,8 +27,10 @@ $dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options);
 
 ?>
 
+<!DOCTYPE html>
 <HTML>
   <HEAD>
+    <link rel="stylesheet" href="css/common.css" />
     <link rel="stylesheet" href="css/pane1.css" />
     <link rel="stylesheet" href="css/pane2.css" />
     <link rel="stylesheet" href="css/research.css" />
@@ -126,7 +128,7 @@ $dbh = new PDO("mysql:host=localhost;dbname=CCCP", "root", "yix", $options);
         <div class="list" id="voter-list">
           <p>Use the checkboxes below to display pins on the map for each category of voter.</p>
           <p>To load fewer pins (which may load faster), select a turf first or zoom in. 
-             A maximum of 3000 voters will be shown at once.</p>
+             A maximum of 10000 voters will be shown at once.</p>
           <input type="checkbox" id="voters-party" onchange="loadVoters(true);"></input>
           <label for="voters-party">Party</label>
           <table class="subcategory">
diff --git a/script.json b/script.json
new file mode 100644 (file)
index 0000000..f2efec0
--- /dev/null
@@ -0,0 +1,30 @@
+{
+  "intro": "Hi, my name is ___, I’m with the Rhode Island Democratic Socialists. We’re out here talking to people because we’re tired of how our one-party government prioritizes corporate profits over working-class Rhode Islanders. We want to build a new party that actually represents us.",
+  "prompts": [
+    {
+      "promptId": "000",
+      "prompt": "What do you think are the main problems/failures of our local government?",
+      "notes": "Take note of any concerns that connect to our platform, and use those points to transition into talking about our goals or demands",
+      "input": {"type": "text", "default": null},
+      "resources": [
+        {
+          "href": "http://...",
+          "title": "Platform"
+        }
+      ]
+    },
+    {
+      "promptId": "001",
+      "prompt": "Would you vote for an independent candidate who runs on a platform like the one on our palm card?",
+      "notes": "Use their response here to adjust the support drop-down above.",
+      "input": {"type": "select",
+                "options": [{"text": "Definite Support (1)", "value": 100},
+                            {"text": "Somewhat Support (2)", "value": 50},
+                            {"text": "Unconvinced (3)", "value": 0},
+                            {"text": "Somewhat Opposed (4)", "value": -50},
+                            {"text": "Definite Opposed (5)", "value": -100}]
+               },
+      "resources": null
+    }
+  ]
+}
index fd7fbd7..d6bbbc7 100644 (file)
@@ -20,7 +20,7 @@
 
 
 <?php
-if( $_SESSION['permissions'] <= CCCP_PERM_ADMIN ) {
+if( $_SESSION['permissions'] > CCCP_PERM_ADMIN ) {
   echo "Requires admin access.".PHP_EOL;
   die();
 }
index b543b18..96280ec 100644 (file)
@@ -116,22 +116,39 @@ if( sizeof($rows) == 0) {
 } else {
   $STATUS['geocoder.remaining'] = $rows[0]['cnt'];
 }
+$query = "select count(*) cnt FROM (select addressId, count(*) c FROM (select distinct addressId, engine from geocodeResults) gr group by addressId) gc WHERE c > 2;";
+$stmt  = $dbh->prepare($query);
+$stmt->execute();
+$rows  = $stmt->fetchAll();
+if( sizeof($rows) == 0) {
+  $STATUS['geocoder.verified'] = "N/A";
+} else {
+  $STATUS['geocoder.verified'] = $rows[0]['cnt'];
+}
+$query = "select count(distinct addressId) cnt FROM voters;";
+$stmt  = $dbh->prepare($query);
+$stmt->execute();
+$rows  = $stmt->fetchAll();
+if( sizeof($rows) == 0) {
+  $STATUS['geocoder.total'] = "N/A";
+} else {
+  $STATUS['geocoder.total'] = $rows[0]['cnt'];
+}
+
 
-/*$STATUS['geocoder.maxDaily'] = 1;
-$STATUS['geocoder.maxDaily'] += $STATUS['geocoder.geoapify'] == "ON" ? $CONFIG['geocoder.geoapify.maxdaily'] : 0;
-$STATUS['geocoder.maxDaily'] += $STATUS['geocoder.maps.co']  == "ON" ? $CONFIG['geocoder.maps.co.maxdaily'] : 0;
-$STATUS['geocoder.maxDaily'] += $STATUS['geocoder.tomtom']  == "ON" ? $CONFIG['geocoder.tomtom.maxdaily'] : 0;*/
 $query = "select count(*) cnt from geocodeResults where latitude IS NOT NULL ".
          "AND updateDate > DATE_SUB(CURDATE(), INTERVAL 1 DAY);";
 $stmt  = $dbh->prepare($query);
 $stmt->execute();
 $rows  = $stmt->fetchAll();
-//print_r($rows[0]['cnt']);
-if( sizeof($rows) == 0) {
+if( sizeof($rows) == 0 || $rows[0]['cnt'] == 0) {
   $STATUS['geocoder.maxDaily'] = "N/A";
 } else {
   $STATUS['geocoder.maxDaily'] = $rows[0]['cnt'];
 }
+if($STATUS['geocoder.maxDaily'] == 0) {
+  $STATUS['geocoder.maxDaily'] = 1;
+}
 
 
 
@@ -139,6 +156,7 @@ if( sizeof($rows) == 0) {
 
 <HTML>
   <HEAD>
+    <link rel="stylesheet" href="css/common.css" />
     <link rel="stylesheet" href="css/pane1.css" />
     <link rel="stylesheet" href="css/pane2.css" />
     <link rel="stylesheet" href="css/settings.css" />
@@ -273,10 +291,16 @@ for($i = $STATUS['cv.list.min']; $i <= $STATUS['cv.list.max']; $i++) {
                   <div>
                     <?php
 $files = scandir('images/');
-$lastCat = str_replace(".png", "", explode("-", $files[0])[2]);
+
+//print_r($files);
+if( sizeof(explode("-", $files[0])) > 2) {
+  $lastCat = str_replace(".png", "", explode("-", $files[0])[2]);
+}
+//$lastCat = "";
 foreach($files as $file) {
   if( strpos($file, "marker-icon-") === 0 ) {
     $category = str_replace(".png", "", explode("-", $file)[2]);
+//    if($lastCat == "") { $lastCat = $category; }
     if($category != $lastCat) {
       if($file != $files[0]) {
         echo "</div>";
@@ -444,11 +468,18 @@ for($i = 0; $i < sizeof($rows); $i++) {
                 <td>Records Remaining: </td>
                 <td>
                   <?php echo $STATUS['geocoder.remaining']; ?>
-                  <?php echo $STATUS['geocoder.remaining'] != "N/A" ?
+                  <?php echo $STATUS['geocoder.remaining'] != "N/A" && $STATUS['geocoder.maxDaily'] != "N/A" ?
                              " (".(int)($STATUS['geocoder.remaining']/($STATUS['geocoder.maxDaily']))." days )" :
                              "" ?>
                 </td>
               </tr>
+              <tr/>
+                <td>Records Verified: </td>
+                <td>
+                  <?php echo $STATUS['geocoder.verified']; ?> / <?php echo $STATUS['geocoder.total']; ?>
+                </td>
+              </tr>
+
             </table>
 
           </form>
index 12456af..90b07ee 100644 (file)
@@ -757,17 +757,37 @@ for($i = 0; $i < sizeof($tasks); $i++) {
     // Run (approximately) one hour's worth of geocodes
     $updated = $tasks[$i]['taskStep'];
     // Select the next max addresses; just don't worry about parents being in the same batch
+      $mode     = "scan";
       $query    = "select va.id as id, va.addressLine1, va.addressLine2, va.city, va.state, va.zip ".
-                  "from voterAddresses va, voters v WHERE v.addressId = va.id AND va.latitude IS NULL ".
+                  "from voterAddresses va WHERE va.latitude IS NULL ".
                   "AND (SELECT id FROM geocodeResults WHERE engine=? AND addressId=va.id LIMIT 1) IS NULL ".
                   "order by va.id desc limit ?;";
       $stmt     = $dbh->prepare($query);
       $stmt->execute( Array($tasks[$i]['taskName'], $maxGeocodes) );
       $geocodes = $stmt->fetchAll();
+      // If there's no addresses with NO lat/lon data, re-scan any that were scanned with another engine
+      if( sizeof($geocodes) <= 100 ) {
+        echo "No urgent geocodes.".PHP_EOL;
+        $mode = "verify";
+        $query   = "select va.id as id, va.addressLine1, va.addressLine2, va.city, va.state, va.zip ".
+                  "from voterAddresses va WHERE ".
+                  "(SELECT id FROM geocodeResults WHERE engine=? AND addressId=va.id LIMIT 1) IS NULL ".
+                  "order by va.id desc limit ?;";
+        $stmt     = $dbh->prepare($query);
+        $stmt->execute( Array($tasks[$i]['taskName'], $maxGeocodes) );
+        $geocodes = array_merge($geocodes, $stmt->fetchAll());
+      }
       if( sizeof($geocodes) == 0 ) {
-        echo "No pending geocodes";
+        echo "No pending geocodes".PHP_EOL;
         continue;
       }
+    // Close the task if we will be over 100%
+    if( sizeof($geocodes) < $maxCount ) {
+      $query = "UPDATE CCCP.tasks SET taskComplete=CURRENT_TIMESTAMP, taskLastStep=? WHERE id = ?;";
+      $params = Array(sizeof($geocodes), $tid);
+      $stmt = $dbh->prepare($query);
+      $stmt->execute($params);
+    }
 
     // Run the geocode
     for($g = 0; $g < $maxCount && $g < sizeof($geocodes); $g++) {
@@ -794,6 +814,12 @@ for($i = 0; $i < sizeof($tasks); $i++) {
         if( !isset($resultsObj->features[0]->properties->lat) ) {
           echo "ERROR: Invalid response returned from ".$tasks[$i]['taskName'].PHP_EOL;
           print_r($resultsObj);
+
+          // Log the geocode result
+          $query = "INSERT INTO geocodeResults(engine, addressId, latitude, longitude, response) VALUES(?,?,?,?,?);";
+          $params= Array($tasks[$i]['taskName'], $geocodes[$g]['id'], null, null, json_encode($resultsObj));
+          $stmt  = $dbh->prepare($query);
+          $stmt->execute($params);
           continue 1;
         }
 
@@ -822,12 +848,24 @@ for($i = 0; $i < sizeof($tasks); $i++) {
         // Maps.co is less effective at parsing certain addresses, so it's likely to get no results
         if( !is_array($resultsObj) || sizeof($resultsObj) == 0) {
           echo "No Maps.co results for address ID: ".$geocodes[$g]['id'].PHP_EOL;
+
+          // Log the geocode result
+          $query = "INSERT INTO geocodeResults(engine, addressId, latitude, longitude, response) VALUES(?,?,?,?,?);";
+          $params= Array($tasks[$i]['taskName'], $geocodes[$g]['id'], null, null, json_encode($resultsObj));
+          $stmt  = $dbh->prepare($query);
+          $stmt->execute($params);
           continue;
         }
 
         if( !isset($resultsObj[0]->lat) ) {
           echo "ERROR: Invalid response returned from ".$tasks[$i]['taskName'].PHP_EOL;
           print_r($resultsObj);
+
+          // Log the geocode result
+          $query = "INSERT INTO geocodeResults(engine, addressId, latitude, longitude, response) VALUES(?,?,?,?,?);";
+          $params= Array($tasks[$i]['taskName'], $geocodes[$g]['id'], null, null, json_encode($resultsObj));
+          $stmt  = $dbh->prepare($query);
+          $stmt->execute($params);
           continue;
         }
 
@@ -848,6 +886,12 @@ for($i = 0; $i < sizeof($tasks); $i++) {
         // Maps.co is less effective at parsing certain addresses, so it's likely to get no results
         if( !is_array($resultsObj->results) || sizeof($resultsObj->results) == 0) {
           echo "No TomTom results for address ID: ".$geocodes[$g]['id'];
+
+          // Log the geocode result
+          $query = "INSERT INTO geocodeResults(engine, addressId, latitude, longitude, response) VALUES(?,?,?,?,?);";
+          $params= Array($tasks[$i]['taskName'], $geocodes[$g]['id'], null, null, json_encode($resultsObj));
+          $stmt  = $dbh->prepare($query);
+          $stmt->execute($params);
           continue;
         }
 
@@ -855,6 +899,12 @@ for($i = 0; $i < sizeof($tasks); $i++) {
           echo "ERROR: Invalid response returned from ".$tasks[$i]['taskName'].PHP_EOL;
           print_r($resultsObj);
           print_r($geocodes[$g]);
+
+          // Log the geocode result
+          $query = "INSERT INTO geocodeResults(engine, addressId, latitude, longitude, response) VALUES(?,?,?,?,?);";
+          $params= Array($tasks[$i]['taskName'], $geocodes[$g]['id'], null, null, json_encode($resultsObj));
+          $stmt  = $dbh->prepare($query);
+          $stmt->execute($params);
           continue;
         }
 
@@ -862,6 +912,12 @@ for($i = 0; $i < sizeof($tasks); $i++) {
           echo "ERROR: Invalid address returned from ".$tasks[$i]['taskName'].PHP_EOL;
           print_r($resultsObj);
           print_r($geocodes[$g]);
+
+          // Log the geocode result
+          $query = "INSERT INTO geocodeResults(engine, addressId, latitude, longitude, response) VALUES(?,?,?,?,?);";
+          $params= Array($tasks[$i]['taskName'], $geocodes[$g]['id'], null, null, json_encode($resultsObj));
+          $stmt  = $dbh->prepare($query);
+          $stmt->execute($params);
           continue;
         }