This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git

commit 2d931346200c78107f389d23da45db15ed0cfb11
Author: Greg Stein <[email protected]>
AuthorDate: Thu Feb 19 23:51:16 2026 -0600

    Revise the state transition UX.
    
    Needed to incorporate the estimated open/close dates, their date
    pickers, their conversion to static values, and clarify how the
    transitions worked for clarity.
    
    There is now an "info box" and an "action box". Pulls this all
    together cleanly into the election management page.
    
    h/t Claude Sonnet 4.6
---
 v3/server/static/css/steve.css |  22 ++++
 v3/server/templates/manage.ezt | 231 +++++++++++++++++++++++++++++++++--------
 2 files changed, 210 insertions(+), 43 deletions(-)

diff --git a/v3/server/static/css/steve.css b/v3/server/static/css/steve.css
index f29aedb..3a7445f 100644
--- a/v3/server/static/css/steve.css
+++ b/v3/server/static/css/steve.css
@@ -57,3 +57,25 @@
 .description { display: none; }
 .description.show { display: block; }
 .twiddle { cursor: pointer; }
+
+/* State stepper dots in the info box card header within manage/EID */
+.election-step-dot {
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    border-radius: 50%;
+    border: 2px solid #adb5bd;
+    background: transparent;
+    flex-shrink: 0;
+}
+
+.election-step-dot.step-current {
+    border-color: #212529;
+    border-width: 2.5px;
+    background: transparent;
+}
+
+.election-step-dot.step-done {
+    border-color: #495057;
+    background: #495057;
+}
diff --git a/v3/server/templates/manage.ezt b/v3/server/templates/manage.ezt
index 93200f0..c9864b4 100644
--- a/v3/server/templates/manage.ezt
+++ b/v3/server/templates/manage.ezt
@@ -7,27 +7,121 @@
             <h2>[e_title]</h2>
         </div>
 
-    <div class="state-diagram">
-        <div id="state-editing"
-           class="state [is e_state "editable"]current[end]">Editing</div>
-        <div id="arrow1" class="arrow">→</div>
-        <div id="state-open"
-           class="state [is e_state "open"]current[end]">Open for Voting</div>
-        <div id="arrow2" class="arrow">→</div>
-        <div id="state-closed"
-           class="state [is e_state "closed"]current[end]">Closed</div>
+    <!-- CSRF token for JS fetch calls -->
+    <input type="hidden" id="csrf-token" value="[csrf_token]">
 
-        [is e_state "editable"]
-            <button id="open-btn" class="btn btn-success action-button 
open-button">
-                <i class="bi bi-unlock me-1"></i>Open
-            </button>
-        [end]
-        [is e_state "open"]
-            <button id="close-btn" class="btn btn-danger action-button 
close-button">
-                <i class="bi bi-lock me-1"></i>Close
-            </button>
-        [end]
-    </div>
+    <!-- Election State Panel -->
+    <div class="row g-3 mb-4 mt-2">
+
+        <!-- Info Box -->
+        <div class="col-md-6">
+            <div class="card h-100 position-relative" id="info-box">
+
+                <!-- Saved toast, anchored to info box -->
+                <div class="toast-container position-absolute top-0 end-0 p-2">
+                    <div id="saved-toast" class="toast align-items-center 
text-bg-success border-0"
+                         role="alert" aria-live="assertive" aria-atomic="true"
+                         data-bs-autohide="true" data-bs-delay="2000">
+                        <div class="d-flex">
+                            <div class="toast-body">
+                                <i class="bi bi-check-lg me-1"></i>Saved
+                            </div>
+                        </div>
+                    </div>
+                </div>[# saved-toast ]
+
+                <div class="card-header">
+                    <!-- State stepper -->
+                    <div class="d-flex align-items-center gap-2 flex-wrap">
+                        <div class="d-flex align-items-center gap-1">
+                            <span class="election-step-dot [is e_state 
"editable"]step-current[end][is e_state "open"]step-done[end][is e_state 
"closed"]step-done[end]"></span>
+                            <span class="[is e_state 
"editable"]fw-bold[end][is e_state "open"]text-muted[end][is e_state 
"closed"]text-muted[end]">Editing</span>
+                        </div>
+                        <span class="text-muted">→</span>
+                        <div class="d-flex align-items-center gap-1">
+                            <span class="election-step-dot [is e_state 
"open"]step-current[end][is e_state "closed"]step-done[end]"></span>
+                            <span class="[is e_state "open"]fw-bold[end][is 
e_state "editable"]text-muted[end][is e_state "closed"]text-muted[end]">Open 
for Voting</span>
+                        </div>
+                        <span class="text-muted">→</span>
+                        <div class="d-flex align-items-center gap-1">
+                            <span class="election-step-dot [is e_state 
"closed"]step-current[end]"></span>
+                            <span class="[is e_state "closed"]fw-bold[end][is 
e_state "editable"]text-muted[end][is e_state 
"open"]text-muted[end]">Closed</span>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="card-body">
+
+                    <!-- Open date: picker when editable, static otherwise -->
+                    <div class="mb-3">
+                        [is e_state "editable"]
+                            <label for="open-at-input" class="form-label 
text-muted small mb-1">Estimated Open Date</label>
+                            <input type="date" class="form-control" 
id="open-at-input"
+                                   [if-any 
election.fmt_open_at_iso]value="[election.fmt_open_at_iso]"[end]>
+                        [else]
+                            <div class="text-muted small mb-1">Opened</div>
+                            <div 
class="fw-semibold">[election.fmt_open_at_full]</div>
+                        [end]
+                    </div>
+
+                    <!-- Close date: static when closed, picker otherwise -->
+                    <div class="mb-1">
+                        [is e_state "closed"]
+                            <div class="text-muted small mb-1">Closed</div>
+                            <div 
class="fw-semibold">[election.fmt_close_at_full]</div>
+                        [else]
+                            [# can set/extend Close date until actually closed 
]
+                            <label for="close-at-input" class="form-label 
text-muted small mb-1">Estimated Close Date</label>
+                            <input type="date" class="form-control" 
id="close-at-input"
+                                   [if-any 
election.fmt_close_at_iso]value="[election.fmt_close_at_iso]"[end]>
+                        [end]
+                    </div>
+
+                </div>[# card-body ]
+            </div>[# card info-box ]
+        </div>[# col info-box ]
+
+        <!-- Action Box -->
+        <div class="col-md-6">
+            <div class="card h-100 [is e_state 
"editable"]border-success[end][is e_state "open"]border-danger[end]">
+                <div class="card-header [is e_state 
"editable"]text-bg-success[end][is e_state "open"]text-bg-danger[end][is 
e_state "closed"]text-bg-secondary[end]">
+                    [is e_state "editable"]Next Action[end]
+                    [is e_state "open"]Next Action[end]
+                    [is e_state "closed"]Election Complete[end]
+                </div>
+                <div class="card-body">
+                    [is e_state "editable"]
+                        <p class="card-text">
+                            <strong>Open this election for voting.</strong>
+                            This action is permanent. The current date and 
time will be
+                            recorded as the official open date, and voters 
will be able
+                            to submit ballots immediately.
+                        </p>
+                        <button id="open-btn" class="btn btn-success">
+                            <i class="bi bi-unlock me-1"></i>Open for Voting
+                        </button>
+                    [end]
+                    [is e_state "open"]
+                        <p class="card-text">
+                            <strong>Close this election.</strong>
+                            This action is permanent. No further ballots will 
be accepted,
+                            and the current date and time will be recorded as 
the official
+                            close date.
+                        </p>
+                        <button id="close-btn" class="btn btn-danger">
+                            <i class="bi bi-lock me-1"></i>Close Election
+                        </button>
+                    [end]
+                    [is e_state "closed"]
+                        <p class="card-text text-muted">
+                            This election is closed. No further changes are 
possible.
+                        </p>
+                    [end]
+                </div>[# card-body ]
+            </div>[# card action-box ]
+        </div>[# col action-box ]
+
+    </div>[# row state panel ]
 
     <!-- Open Confirmation Modal -->
     <div class="modal fade" id="openConfirmModal" tabindex="-1" 
aria-labelledby="openConfirmModalLabel" aria-hidden="true">
@@ -38,11 +132,14 @@
                     <button type="button" class="btn-close" 
data-bs-dismiss="modal" aria-label="Close"></button>
                 </div>
                 <div class="modal-body">
-                    Opening this election will allow voting and is 
irreversible. Are you sure?
+                    <p>Opening this election will allow voting and cannot be 
undone.</p>
+                    <p class="mb-0">The current date and time will be recorded 
as the official open date.</p>
                 </div>
                 <div class="modal-footer">
                     <button type="button" class="btn btn-secondary" 
data-bs-dismiss="modal">Cancel</button>
-                    <button type="button" class="btn btn-success" 
id="confirm-open">Confirm</button>
+                    <button type="button" class="btn btn-success" 
id="confirm-open">
+                        <i class="bi bi-unlock me-1"></i>Open for Voting
+                    </button>
                 </div>
             </div>
         </div>
@@ -57,16 +154,39 @@
                     <button type="button" class="btn-close" 
data-bs-dismiss="modal" aria-label="Close"></button>
                 </div>
                 <div class="modal-body">
-                    Closing this election will end voting permanently. Are you 
sure?
+                    <p>Closing this election will permanently end voting. This 
cannot be undone.</p>
+                    <p class="mb-0">The current date and time will be recorded 
as the official close date.</p>
                 </div>
                 <div class="modal-footer">
                     <button type="button" class="btn btn-secondary" 
data-bs-dismiss="modal">Cancel</button>
-                    <button type="button" class="btn btn-danger" 
id="confirm-close">Confirm</button>
+                    <button type="button" class="btn btn-danger" 
id="confirm-close">
+                        <i class="bi bi-lock me-1"></i>Close Election
+                    </button>
                 </div>
             </div>
         </div>
     </div>[# id=closeConfirmModal ]
 
+    <!-- Date Save Error Modal -->
+    <div class="modal fade" id="dateSaveErrorModal" tabindex="-1" 
aria-labelledby="dateSaveErrorModalLabel" aria-hidden="true">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header text-bg-danger">
+                    <h5 class="modal-title" id="dateSaveErrorModalLabel">
+                        <i class="bi bi-exclamation-triangle me-1"></i>Save 
Failed
+                    </h5>
+                    <button type="button" class="btn-close btn-close-white" 
data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    The date could not be saved. Please check your connection 
and try again.
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" 
data-bs-dismiss="modal">Dismiss</button>
+                </div>
+            </div>
+        </div>
+    </div>[# id=dateSaveErrorModal ]
+
     <!-- Edit/Add Issue Modal -->
     <div class="modal fade" id="issueModal" tabindex="-1" 
aria-labelledby="issueModalLabel" aria-hidden="true">
         <div class="modal-dialog">
@@ -131,23 +251,6 @@
         </div>[# modal-dialog ]
     </div>[# id=deleteIssueModal ]
 
-        <div>
-          [is e_state "editable"]
-            Estimate to open: <input type="date" [if-any 
election.fmt_open_at_iso]value="[election.fmt_open_at_iso]"[end]>
-          [else]
-            Opened: [election.fmt_open_at_full]
-          [end]
-        </div>
-      
-        <div class="mb-3">
-          [is e_state "closed"]
-            Closed: [election.fmt_close_at_full]
-          [else]
-            [# can set/extend Close date, until actually closed. ]
-            Estimate to close: <input type="date" [if-any 
election.fmt_close_at_iso]value="[election.fmt_close_at_iso]"[end]>
-          [end]
-        </div>
-
         <div class="mb-4">
 
         [is e_state "editable"]
@@ -156,7 +259,7 @@
             </button>
         [end]
 
-            <button type="button" class="btn btn-outline-secondary ms-2" 
onclick="toggleAllDescriptions()">
+            <button type="button" id="toggle-all-btn" class="btn 
btn-outline-secondary ms-2" onclick="toggleAllDescriptions()">
                 <i class="bi bi-arrows-expand me-1"></i>
                 <span id="toggle-all-text">Expand All</span>
             </button>
@@ -202,6 +305,10 @@
 <script>
     const openModal = new 
bootstrap.Modal(document.getElementById('openConfirmModal'));
     const closeModal = new 
bootstrap.Modal(document.getElementById('closeConfirmModal'));
+    const dateSaveErrorModal = new 
bootstrap.Modal(document.getElementById('dateSaveErrorModal'));
+    const savedToast = new 
bootstrap.Toast(document.getElementById('saved-toast'));
+
+    const csrfToken = document.getElementById('csrf-token').value;
 
     [is e_state "editable"]
         const openBtn = document.getElementById('open-btn');
@@ -227,6 +334,44 @@
         window.location.href = '/do-close/[eid]';
     });
 
+    // Auto-save a date field on change
+    async function saveDate(field, value) {
+        try {
+            const resp = await fetch(`/do-set-${field}/[eid]`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                    'X-CSRFToken': csrfToken,
+                },
+                body: JSON.stringify({ date: value }),
+            });
+            if (resp.status === 204) {
+                savedToast.show();
+            } else {
+                dateSaveErrorModal.show();
+            }
+        } catch (e) {
+            dateSaveErrorModal.show();
+        }
+    }
+
+    [is e_state "editable"]
+        document.getElementById('open-at-input').addEventListener('change', 
function () {
+            saveDate('open_at', this.value);
+        });
+    [end]
+
+    [is e_state "editable"]
+        document.getElementById('close-at-input').addEventListener('change', 
function () {
+            saveDate('close_at', this.value);
+        });
+    [end]
+    [is e_state "open"]
+        document.getElementById('close-at-input').addEventListener('change', 
function () {
+            saveDate('close_at', this.value);
+        });
+    [end]
+
     // Toggle individual description
     function toggleDescription(issueId) {
       const desc = document.getElementById(`description-${issueId}`);
@@ -249,7 +394,7 @@
         twiddle.classList.toggle('bi-caret-right-fill', !allExpanded);
         twiddle.classList.toggle('bi-caret-down-fill', allExpanded);
       });
-      const toggleBtn = document.querySelector('.btn-outline-secondary');
+      const toggleBtn = document.getElementById('toggle-all-btn');
       const toggleIcon = toggleBtn.querySelector('i');
       const toggleText = document.getElementById('toggle-all-text');
       

Reply via email to