Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

472 wiersze
20KB

  1. <section style="padding: 48px 0; max-width: 640px; margin: 0 auto; text-align: center;">
  2. <% If isValid Then %>
  3. <span class="eyebrow">Order link verified</span>
  4. <h1>Your order link is valid.</h1>
  5. <p>Jurisdiction number: <strong><%= H(order("JurisdictionNumber")) %></strong></p>
  6. <% Else %>
  7. <span class="eyebrow">Link problem</span>
  8. <h1>This link is invalid or has expired.</h1>
  9. <p>
  10. Please <a href="<%= H(Routes().AppURL & "request-order") %>">request a new order</a>
  11. to receive a fresh link.
  12. </p>
  13. <% End If %>
  14. </section>
  15. <% If isValid Then
  16. Dim municipalityIsReadOnlyJs, municipalityDescription
  17. If Len(Trim(municipalityName)) > 0 Then
  18. municipalityIsReadOnlyJs = "true"
  19. municipalityDescription = "Matched automatically from your jurisdiction number."
  20. Else
  21. municipalityIsReadOnlyJs = "false"
  22. municipalityDescription = "We couldn't automatically match your jurisdiction number - please enter your municipality."
  23. End If
  24. %>
  25. <link href="https://cdn.jsdelivr.net/npm/survey-core@2.5.36/survey-core.min.css" rel="stylesheet">
  26. <div style="max-width: 760px; margin: 0 auto; padding: 0 16px;">
  27. <div class="cf-turnstile" style="margin-bottom: 24px;"
  28. data-sitekey="<%= H(turnstileSiteKey) %>"
  29. data-callback="onTurnstileVerified"></div>
  30. </div>
  31. <div id="surveyContainer" style="max-width: 760px; margin: 0 auto 64px; padding: 0 16px;"></div>
  32. <div id="colorPickerOverlay" style="display:none; position:fixed; inset:0; background:rgba(36,16,58,0.55); z-index:1000; align-items:center; justify-content:center; padding:16px;">
  33. <div style="background:#fff; border-radius:14px; max-width:640px; width:100%; max-height:85vh; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 12px 40px rgba(36,16,58,0.35);">
  34. <div style="display:flex; align-items:center; justify-content:space-between; padding:20px 24px; border-bottom:1px solid #f0e7f8;">
  35. <h3 style="margin:0; color:#201a27; font-size:18px;">Choose your colors</h3>
  36. <button type="button" id="colorPickerCloseBtn" aria-label="Close" style="background:none; border:none; font-size:22px; line-height:1; color:#6d6574; cursor:pointer; padding:4px 8px;">&times;</button>
  37. </div>
  38. <div id="colorPickerGrid" style="overflow-y:auto; padding:20px 24px; display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:12px;"></div>
  39. <div style="display:flex; justify-content:flex-end; gap:10px; padding:16px 24px; border-top:1px solid #f0e7f8;">
  40. <button type="button" id="colorPickerCancelBtn" class="button button-secondary">Cancel</button>
  41. <button type="button" id="colorPickerApplyBtn" class="button button-primary">Apply</button>
  42. </div>
  43. </div>
  44. </div>
  45. <script src="https://cdn.jsdelivr.net/npm/survey-core@2.5.36/survey.core.min.js"></script>
  46. <script src="https://cdn.jsdelivr.net/npm/survey-js-ui@2.5.36/survey-js-ui.min.js"></script>
  47. <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
  48. <script>
  49. (function () {
  50. "use strict";
  51. var submitUrl = "<%= H(Routes().AppURL & "order/submit?token=" & Server.URLEncode(token)) %>";
  52. var csrfToken = "<%= H(csrfToken) %>";
  53. var turnstileToken = "";
  54. window.onTurnstileVerified = function (token) {
  55. turnstileToken = token;
  56. };
  57. // Election-materials order form. Field (name) values match the OrderDetails table
  58. // columns 1:1 - see db/migrations/20260728145000_create_order_details_table.asp and
  59. // db/migrations/20260804153857_revise_order_details_columns.asp.
  60. // Content and branching sourced from docs/lucid/*.pdf (the "(1)" files are the current
  61. // revision - see docs/lucid for both the original and revised versions).
  62. var surveyJson = {
  63. showProgressBar: "top",
  64. showQuestionNumbers: "off",
  65. completeText: "Submit Order",
  66. checkErrorsMode: "onNextPage",
  67. completedHtml: "<h3>Thank you for submitting your order.</h3>",
  68. pages: [
  69. {
  70. name: "contact",
  71. title: "1. Contact Information",
  72. elements: [
  73. { type: "text", name: "ContactName", title: "Contact Name", isRequired: true },
  74. {
  75. type: "text", name: "Municipality", title: "Municipality", isRequired: true,
  76. defaultValue: "<%= H(municipalityName) %>",
  77. readOnly: <%= municipalityIsReadOnlyJs %>,
  78. description: "<%= H(municipalityDescription) %>"
  79. },
  80. { type: "text", name: "Phone", title: "Phone Number", inputType: "tel", isRequired: true }
  81. ]
  82. },
  83. {
  84. name: "purpleEnvelope",
  85. title: "2. Purple Ballot Envelope Order Form",
  86. elements: [
  87. {
  88. type: "boolean", name: "PurpleWantsQuote", labelTrue: "Yes", labelFalse: "No",
  89. title: "Would you like for KCI to print your purple AV envelopes?",
  90. isRequired: true
  91. },
  92. {
  93. type: "text", name: "PurplePermanentBallotListCount",
  94. title: "How many are on your permanent ballot list?",
  95. inputType: "number", min: 0,
  96. visibleIf: "{PurpleWantsQuote} = true",
  97. isRequired: true
  98. },
  99. {
  100. type: "radiogroup", name: "PurpleEnvelopeStock",
  101. title: "Are you planning to use KCI's purple envelopes or from your own stock?",
  102. visibleIf: "{PurpleWantsQuote} = true",
  103. isRequired: true,
  104. choices: [
  105. { value: "KCIStock", text: "KCI's stock" },
  106. { value: "OwnStock", text: "My own stock" }
  107. ]
  108. },
  109. {
  110. type: "radiogroup", name: "PurplePrintOption",
  111. title: "Would you like KCI to print addresses on your purple envelopes or state of Michigan permit only?",
  112. visibleIf: "{PurpleWantsQuote} = true",
  113. isRequired: true,
  114. choices: [
  115. { value: "AddressesAndPermit", text: "Addresses and state of Michigan permit" },
  116. { value: "PermitOnly", text: "State of Michigan permit only" }
  117. ]
  118. },
  119. {
  120. type: "boolean", name: "PurpleWantsBlueEnvelopes", labelTrue: "Yes", labelFalse: "No",
  121. title: "Are you planning to use KCI blue envelopes?",
  122. visibleIf: "{PurplePrintOption} = 'AddressesAndPermit'",
  123. isRequired: true
  124. },
  125. {
  126. type: "radiogroup", name: "PurpleBlueProvider",
  127. title: "Who is your blue envelope provider?",
  128. visibleIf: "{PurpleWantsBlueEnvelopes} = false",
  129. isRequired: true,
  130. choices: [
  131. { value: "ElectionSource", text: "ElectionSource" },
  132. { value: "PSI", text: "PSI" },
  133. { value: "Spectrum", text: "Spectrum" },
  134. { value: "Other", text: "Other" }
  135. ]
  136. },
  137. {
  138. type: "text", name: "PurpleBlueProviderOther",
  139. title: "Please specify your blue envelope provider",
  140. visibleIf: "{PurpleBlueProvider} = 'Other'",
  141. isRequired: true
  142. },
  143. {
  144. type: "radiogroup", name: "PurplePrintStyle",
  145. title: "Would you like your envelopes printed with color coding or black ink only?",
  146. visibleIf: "{PurplePrintOption} = 'AddressesAndPermit'",
  147. isRequired: true,
  148. choices: [
  149. { value: "ColorCoding", text: "Color Coding" },
  150. { value: "BlackOnly", text: "Black Ink Only" }
  151. ]
  152. },
  153. {
  154. type: "radiogroup", name: "PurpleColorCodingBy",
  155. title: "Color coding by precinct or by election?",
  156. visibleIf: "{PurplePrintStyle} = 'ColorCoding'",
  157. isRequired: true,
  158. choices: [
  159. { value: "Precinct", text: "Precinct" },
  160. { value: "Election", text: "Election" }
  161. ]
  162. },
  163. {
  164. type: "text", name: "PurplePrecinctCount", title: "How many precincts?",
  165. inputType: "number", min: 0,
  166. visibleIf: "{PurpleColorCodingBy} = 'Precinct'",
  167. isRequired: true
  168. },
  169. {
  170. type: "html",
  171. visibleIf: "{PurplePrintStyle} = 'ColorCoding'",
  172. html: "<button type=\"button\" id=\"openColorPickerBtn\" class=\"button button-secondary\">Choose Colors&hellip;</button>"
  173. },
  174. {
  175. type: "text", name: "PurpleColorNames", readOnly: true,
  176. title: "Which colors would you like to use?",
  177. description: "Use the \"Choose Colors…\" button above to pick from KCI's available envelope colors.",
  178. visibleIf: "{PurplePrintStyle} = 'ColorCoding'",
  179. isRequired: true
  180. },
  181. {
  182. type: "boolean", name: "PurpleTrackMIBallot", labelTrue: "Yes", labelFalse: "No",
  183. title: "Would you like to track ballots using TrackMI Ballot?",
  184. visibleIf: "{PurplePrintOption} = 'AddressesAndPermit'",
  185. isRequired: true
  186. },
  187. {
  188. type: "text", name: "PurpleExtraEnvelopeQty",
  189. title: "How many extra purple envelopes would you like? (If none, please put zero)",
  190. inputType: "number", min: 0,
  191. visibleIf: "{PurplePrintOption} = 'AddressesAndPermit'",
  192. isRequired: true
  193. },
  194. {
  195. type: "html",
  196. visibleIf: "{PurpleWantsQuote} = true",
  197. html: "<p>Purple envelopes are usually delivered or shipped 3 to 5 days after proof is approved.</p>"
  198. }
  199. ]
  200. },
  201. {
  202. name: "blueEnvelope",
  203. title: "3. Blue AV Envelope Order Form",
  204. elements: [
  205. {
  206. type: "boolean", name: "BlueWantsQuote", labelTrue: "Yes", labelFalse: "No",
  207. title: "Would you like to purchase blue AV envelopes from KCI?",
  208. isRequired: true
  209. },
  210. {
  211. type: "boolean", name: "BluePrintPermit", labelTrue: "Yes", labelFalse: "No",
  212. title: "Would you like KCI to print a permit on your blue envelope?",
  213. visibleIf: "{BlueWantsQuote} = true",
  214. isRequired: true
  215. },
  216. {
  217. type: "radiogroup", name: "BluePermitOwnership",
  218. title: "Will you be using KCI's permit or your own?",
  219. visibleIf: "{BluePrintPermit} = true",
  220. isRequired: true,
  221. choices: [
  222. { value: "KCIPermit", text: "KCI permit" },
  223. { value: "OwnPermit", text: "My organization's permit" }
  224. ]
  225. },
  226. {
  227. type: "html",
  228. visibleIf: "{BluePermitOwnership} = 'KCIPermit'",
  229. html: "<p><strong>Please note, envelopes with KCI's permit MUST be mailed from KCI.</strong></p>"
  230. },
  231. {
  232. type: "radiogroup", name: "BlueHasNonprofitStatus",
  233. title: "Does your organization have confirmed nonprofit status with USPS?",
  234. visibleIf: "{BluePrintPermit} = true",
  235. isRequired: true,
  236. choices: [
  237. { value: "Yes", text: "Yes" },
  238. { value: "No", text: "No" },
  239. { value: "Unsure", text: "Unsure (That's okay, we can help you!)" }
  240. ]
  241. },
  242. {
  243. type: "text", name: "BlueNonprofitAuthCode",
  244. title: "Please provide your nonprofit authorization code.",
  245. visibleIf: "{BlueHasNonprofitStatus} = 'Yes'",
  246. isRequired: true
  247. },
  248. {
  249. type: "text", name: "BluePermitCity", title: "City of permit holder",
  250. description: "A permit lookup can't be done from the authorization code alone, so please also provide the following information.",
  251. visibleIf: "{BluePermitOwnership} = 'OwnPermit'",
  252. isRequired: true
  253. },
  254. {
  255. type: "text", name: "BluePermitNumber", title: "Permit Number",
  256. visibleIf: "{BluePermitOwnership} = 'OwnPermit'",
  257. isRequired: true
  258. }
  259. ]
  260. },
  261. {
  262. name: "mailingService",
  263. title: "4. Ballot Mailing Service",
  264. elements: [
  265. {
  266. type: "boolean", name: "MailingWantsService", labelTrue: "Yes", labelFalse: "No",
  267. title: "Would you like to save postage and have KCI mail your ballots?",
  268. isRequired: true
  269. },
  270. {
  271. type: "radiogroup", name: "MailingPostageOption",
  272. title: "Would you like KCI to apply postage?",
  273. visibleIf: "{MailingWantsService} = true",
  274. isRequired: true,
  275. choices: [
  276. { value: "FirstClass", text: "Yes, at First Class Rate ($0.721/pc.)" },
  277. { value: "NonprofitRate", text: "Yes, at Nonprofit Rate ($0.263/pc.)" },
  278. { value: "PresortStandard", text: "Yes, at Presort Standard Rate ($0.473/pc.)" },
  279. { value: "No", text: "No" }
  280. ]
  281. },
  282. {
  283. type: "radiogroup", name: "MailingHasNonprofitStatus",
  284. title: "Do you have Nonprofit Status with the USPS?",
  285. visibleIf: "{MailingPostageOption} = 'NonprofitRate'",
  286. isRequired: true,
  287. choices: [
  288. { value: "Yes", text: "Yes" },
  289. { value: "No", text: "No" },
  290. { value: "NotSure", text: "I'm not sure (that's okay, we can help you!)" }
  291. ]
  292. },
  293. {
  294. type: "text", name: "MailingEstimatedQuantity",
  295. title: "Please provide your estimated quantity.",
  296. inputType: "number", min: 0,
  297. visibleIf: "{MailingWantsService} = true",
  298. isRequired: true
  299. },
  300. {
  301. type: "dropdown", name: "MailingPickupDate",
  302. title: "What is your preferred (first) pickup date?",
  303. visibleIf: "{MailingWantsService} = true",
  304. isRequired: true,
  305. choices: [
  306. { value: "2026-09-21", text: "September 21, 2026" },
  307. { value: "2026-09-22", text: "September 22, 2026" },
  308. { value: "2026-09-23", text: "September 23, 2026" },
  309. { value: "2026-09-24", text: "September 24, 2026" },
  310. { value: "2026-09-25", text: "September 25, 2026" },
  311. { value: "2026-09-28", text: "September 28, 2026" },
  312. { value: "2026-09-29", text: "September 29, 2026" },
  313. { value: "2026-09-30", text: "September 30, 2026" },
  314. { value: "2026-10-01", text: "October 1, 2026" },
  315. { value: "2026-10-02", text: "October 2, 2026" }
  316. ]
  317. }
  318. ]
  319. },
  320. {
  321. name: "additionalItems",
  322. title: "5. Additional Election Items Request",
  323. elements: [
  324. {
  325. type: "text", name: "SecrecySleevesQty", title: "Secrecy Sleeves (quantity)",
  326. inputType: "number", min: 0, defaultValue: 0,
  327. isRequired: true
  328. },
  329. {
  330. type: "text", name: "IVotedStickerRolls", title: "\"I Voted\" Stickers - number of rolls (rolls of 250)",
  331. inputType: "number", min: 0, defaultValue: 0,
  332. isRequired: true
  333. },
  334. {
  335. type: "text", name: "FutureVoterStickerRolls", title: "\"Future Voter\" Stickers - number of rolls (rolls of 500)",
  336. inputType: "number", min: 0, defaultValue: 0,
  337. isRequired: true
  338. },
  339. {
  340. type: "comment", name: "SpecialRequests",
  341. title: "We will send you a copy of your price quote upon completion of this order form. If you have any special requests for any of your orders, please let us know here."
  342. }
  343. ]
  344. }
  345. ]
  346. };
  347. var survey = new Survey.Model(surveyJson);
  348. // Color picker modal for the "Which colors would you like to use?" question. Swatches are
  349. // the pre-converted PNGs in public/images/purple-envelope-colors/ (converted from the
  350. // original public/images/Purple_ballot_envelopes colors/*.bmp - those BMPs were ~400KB
  351. // each; PNG compresses these flat-color swatches down to ~1-4KB each).
  352. (function () {
  353. var COLOR_FILES = [
  354. "Aqua", "Black", "Blue", "Brown", "BurlyWood", "Chocolate", "Clay", "Coral",
  355. "CornflowerBlue", "Crimson", "DarkOrange", "DeepPink", "DeepSkyBlue", "DimGray",
  356. "DodgerBlue", "FireBrick", "ForestGreen", "Fuchsia", "Gold", "Goldenrod", "Green",
  357. "HotPink", "Indigo", "LightSeaGreen", "Lime", "LimeGreen", "LtGreen", "MediumOrchid",
  358. "MediumPurple", "MediumTurquoise", "MintGreen", "Navy", "Olive", "OliveDrab", "Orange",
  359. "OrangeRed", "Orchid", "PastelBlue", "PastelPink", "PastelPurple", "PastelYellow",
  360. "Peru", "Pink", "Purple", "Red", "RoyalBlue", "SaddleBrown", "SeaGreen", "Sienna",
  361. "Silver", "SkyBlue", "SteelBlue", "Tan", "Teal", "Tomato", "Turquoise", "Violet",
  362. "White", "Yellow"
  363. ];
  364. var colorImageBaseUrl = "<%= H(Routes().AppURL & "images/purple-envelope-colors/") %>";
  365. var overlay = document.getElementById("colorPickerOverlay");
  366. var grid = document.getElementById("colorPickerGrid");
  367. function colorLabel(fileName) {
  368. return fileName.replace(/([a-z])([A-Z])/g, "$1 $2");
  369. }
  370. COLOR_FILES.forEach(function (fileName) {
  371. var label = colorLabel(fileName);
  372. var item = document.createElement("label");
  373. item.style.cssText = "display:flex; flex-direction:column; gap:6px; border:1px solid #f0e7f8; border-radius:8px; padding:8px; cursor:pointer;";
  374. item.innerHTML =
  375. '<img src="' + colorImageBaseUrl + encodeURIComponent(fileName) + '.png" alt="" ' +
  376. 'style="width:100%; height:36px; border-radius:4px; object-fit:cover; border:1px solid #e5d9f2;" />' +
  377. '<span style="display:flex; align-items:center; gap:6px; font-size:13px; color:#201a27;">' +
  378. '<input type="checkbox" class="color-picker-checkbox" value="' + label.replace(/"/g, "&quot;") + '" /> ' +
  379. label + "</span>";
  380. grid.appendChild(item);
  381. });
  382. function openColorPicker() {
  383. var current = (survey.getValue("PurpleColorNames") || "")
  384. .split(",")
  385. .map(function (s) { return s.trim(); })
  386. .filter(Boolean);
  387. var currentSet = {};
  388. current.forEach(function (c) { currentSet[c] = true; });
  389. var checkboxes = grid.querySelectorAll(".color-picker-checkbox");
  390. checkboxes.forEach(function (cb) { cb.checked = !!currentSet[cb.value]; });
  391. overlay.style.display = "flex";
  392. }
  393. function closeColorPicker() {
  394. overlay.style.display = "none";
  395. }
  396. function applyColorPicker() {
  397. var checked = grid.querySelectorAll(".color-picker-checkbox:checked");
  398. var selected = Array.prototype.map.call(checked, function (cb) { return cb.value; });
  399. survey.setValue("PurpleColorNames", selected.join(", "));
  400. closeColorPicker();
  401. }
  402. document.getElementById("colorPickerCloseBtn").addEventListener("click", closeColorPicker);
  403. document.getElementById("colorPickerCancelBtn").addEventListener("click", closeColorPicker);
  404. document.getElementById("colorPickerApplyBtn").addEventListener("click", applyColorPicker);
  405. overlay.addEventListener("click", function (event) {
  406. if (event.target === overlay) closeColorPicker();
  407. });
  408. // The "Choose Colors..." button is rendered by SurveyJS as an html question, so it
  409. // doesn't exist in the DOM until that page is shown - delegate from a stable ancestor.
  410. document.addEventListener("click", function (event) {
  411. if (event.target && event.target.id === "openColorPickerBtn") {
  412. event.preventDefault();
  413. openColorPicker();
  414. }
  415. });
  416. })();
  417. survey.onCompleting.add(function (sender, options) {
  418. if (!turnstileToken) {
  419. options.allowComplete = false;
  420. alert("Please complete the verification challenge above before submitting.");
  421. }
  422. });
  423. survey.onComplete.add(function (sender) {
  424. fetch(submitUrl, {
  425. method: "POST",
  426. headers: {
  427. "Content-Type": "application/json",
  428. "X-CSRF-Token": csrfToken,
  429. "X-Turnstile-Token": turnstileToken
  430. },
  431. body: JSON.stringify(sender.data)
  432. })
  433. .then(function (res) {
  434. return res.json().catch(function () { return {}; }).then(function (body) {
  435. return { ok: res.ok, body: body };
  436. });
  437. })
  438. .then(function (result) {
  439. if (result.ok && result.body && result.body.success) {
  440. window.location.href = "<%= H(Routes().AppURL) %>";
  441. } else {
  442. alert((result.body && result.body.error) || "Something went wrong submitting your order. Please try again.");
  443. }
  444. })
  445. .catch(function () {
  446. alert("Something went wrong submitting your order. Please check your connection and try again.");
  447. });
  448. });
  449. survey.render(document.getElementById("surveyContainer"));
  450. })();
  451. </script>
  452. <% End If %>

Powered by TurnKey Linux.