1<!doctype html> 2<html lang="en"> 3<head> 4 <meta charset="utf-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 <link rel="icon" type="image/svg+xml" href="assets/favicon.svg" /> 7 <title>RRCC â Board Members</title> 8 <link rel="stylesheet" href="styles.css?v=5b11c888b4" /> 9 <style> 10 .progtabs{display:flex;gap:6px;max-width:1320px;margin:12px auto 0;padding:0 14px;flex-wrap:wrap} 11 .progtab{display:inline-flex;align-items:center;text-decoration:none;font-weight:800;font-size:14.5px;color:#1f2a44;background:#fff;border:1px solid #e1e6ef;border-radius:12px 12px 0 0;padding:11px 22px;box-shadow:0 1px 4px rgba(20,20,40,.06);position:relative;top:1px} 12 .progtab:hover{color:#b8232a;border-color:#f0b8b8} 13 .progtab.active{background:#b8232a;border-color:#b8232a;color:#fff} 14 .topnav{display:flex;align-items:center;gap:18px;background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:10px 16px;margin-bottom:14px;box-shadow:0 1px 4px rgba(20,20,40,.06);flex-wrap:wrap;position:sticky;top:0;z-index:200} 15 #featBtn{flex:none;white-space:nowrap;cursor:pointer} 16 .navlink{display:inline-flex;align-items:center;text-decoration:none;font-weight:700;font-size:13px;color:#1f2a44;background:#f3f5f9;border:1px solid #e1e6ef;border-radius:10px;padding:8px 11px} 17 .navlink:hover{background:#fdecec;border-color:#f0b8b8;color:#b8232a} 18 19 .wrap{max-width:1140px;margin:0 auto;padding:0 22px 30px} 20 .head h1{margin:0 0 3px;font-size:22px;color:#1f2a44} 21 .head p{margin:0 0 10px;color:#6b7280;font-size:13px;max-width:860px} 22 .countbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px} 23 .chip{font-size:12px;font-weight:700;color:#475569;background:#eef1f5;border:1px solid #e1e6ef;border-radius:999px;padding:4px 11px} 24 .toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px} 25 .toolbar input[type=search]{font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:9px;background:#fff;min-width:220px;flex:1} 26 .toolbar label{font-size:12.5px;color:#475569;display:inline-flex;align-items:center;gap:5px;font-weight:600} 27 .actionbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px;background:#fff;border:1px solid #e6e8ec;border-radius:12px;padding:9px 12px} 28 .actionbar .sel{font-size:13px;font-weight:700;color:#1f2a44} 29 .btn{font:inherit;font-weight:700;font-size:12.5px;padding:7px 12px;border-radius:9px;border:1px solid #d7dbe0;background:#fff;color:#1f2a44;cursor:pointer;white-space:nowrap} 30 .btn:hover{border-color:#b8232a;color:#b8232a} 31 .btn.primary{background:#1f2a44;border-color:#1f2a44;color:#fff}.btn.primary:hover{background:#0f1830;color:#fff} 32 .btn.sm{padding:5px 9px;font-size:12px} 33 .btn[disabled]{opacity:.5;cursor:default;border-color:#e1e6ef;color:#9aa1ab} 34 35 .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:12px} 36 .mcard{background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:14px;box-shadow:0 1px 3px rgba(20,20,40,.05);display:flex;flex-direction:column;gap:9px} 37 .mcard.sel{border-color:#b8232a;box-shadow:0 0 0 2px #fdecec} 38 .mtop{display:flex;gap:11px;align-items:flex-start} 39 .ava{width:54px;height:54px;border-radius:50%;flex:none;object-fit:cover;background:#eef1f5;display:flex;align-items:center;justify-content:center;font-weight:800;color:#9aa1ab;font-size:18px;border:1px solid #e1e6ef;overflow:hidden} 40 .ava img{width:100%;height:100%;object-fit:cover} 41 .mname{font-weight:800;color:#1f2a44;font-size:16px;line-height:1.2} 42 .mpos{display:inline-block;font-size:11px;font-weight:800;border-radius:999px;padding:2px 9px;margin-top:3px} 43 .p-chair{color:#7c2d12;background:#fde9d6}.p-vice{color:#92600a;background:#fef3c7}.p-treasurer{color:#065f46;background:#cdeede}.p-secretary{color:#5b21b6;background:#ede9fe}.p-dir{color:#1e40af;background:#dbeafe} 44 .mmeta{font-size:12.5px;color:#475569;line-height:1.5} 45 .mmeta a{color:#b8232a;text-decoration:none}.mmeta a:hover{text-decoration:underline} 46 .mbio{font-size:12.5px;color:#334155;background:#f7f8fa;border:1px solid #eef0f3;border-radius:8px;padding:7px 9px;white-space:pre-wrap;overflow-wrap:anywhere}
47 .mbio.clamp{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} 48 .mbiomore{display:block;margin:4px 0 0;background:none;border:0;color:#1d4ed8;font-weight:700;font-size:11.5px;cursor:pointer;padding:2px 0} 49 .mterm{font-size:11.5px;color:#9aa1ab} 50 .pick{margin-left:auto;flex:none} 51 .mactions{display:flex;gap:6px;flex-wrap:wrap;border-top:1px solid #f1f3f6;padding-top:9px} 52 .updated{font-size:11px;color:#15803d} 53 .nolink{font-size:11px;color:#92400e;font-weight:600} 54 .msigned{font-size:11px;color:#475569} 55 .msigned.notyet{color:#92400e;font-weight:600} 56 .mrev{font-size:11.5px;color:#1d4ed8;margin-top:6px;font-weight:600} 57 .empty{color:#9aa1ab;text-align:center;padding:40px 0} 58 /* approval rotation panel */ 59 .rotation{background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(20,20,40,.05)} 60 .rothead{font-weight:800;color:#1f2a44;font-size:14px;margin-bottom:3px} 61 .rotsub{font-size:12.5px;color:#6b7280;margin-bottom:10px}.rotsub b{color:#1d4ed8} 62 .rotlist{display:flex;flex-wrap:wrap;gap:6px} 63 /* board activity log */ 64 .actlog{background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:12px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(20,20,40,.05)} 65 .actlog>summary{cursor:pointer;font-weight:800;color:#1f2a44;font-size:14px;list-style:none;display:flex;align-items:center;gap:8px} 66 .actlog>summary::-webkit-details-marker{display:none} 67 .actlog>summary::after{content:"\25BE";color:#9aa1ab;font-size:12px;transition:transform .15s} 68 .actlog[open]>summary::after{transform:rotate(180deg)} 69 .actlist{margin-top:10px;max-height:340px;overflow-y:auto} 70 .actrow{display:flex;gap:12px;align-items:baseline;padding:7px 0;border-bottom:1px solid #f2f4f7;font-size:13px;color:#1f2a44} 71 .actrow:last-child{border-bottom:0} 72 .actwhen{flex:none;width:118px;color:#9aa1ab;font-size:11.5px;font-weight:700;white-space:nowrap} 73 .actempty{color:#9aa1ab;font-size:12.5px;margin:0} 74 .rotrow{display:inline-flex;align-items:center;gap:7px;padding:6px 12px;border:1px solid #eef0f3;border-radius:999px;background:#f7f8fa;cursor:grab;user-select:none;-webkit-user-select:none} 75 .rmove{margin-left:auto;display:flex;gap:4px} 76 .rbtn{border:1px solid #d7dbe3;background:#fff;color:#334155;border-radius:8px;width:34px;height:34px;font:inherit;font-size:14px;line-height:1;cursor:pointer} 77 .rbtn:hover:not(:disabled){border-color:#b8232a;color:#b8232a} 78 .rbtn:disabled{opacity:.35;cursor:default} 79 .rotrow:active{cursor:grabbing} 80 .rotrow.dragging{opacity:.45;border-style:dashed} 81 .rotrow.next{border-color:#1d4ed8;background:#eef3ff} 82 .rotrow .rnum{font-weight:800;color:#9aa1ab;font-size:11px} 83 .rotrow .rname{font-weight:700;color:#1f2a44;font-size:12.5px;white-space:nowrap} 84 .rotrow .rnext{font-size:9px;font-weight:800;text-transform:uppercase;color:#fff;background:#1d4ed8;border-radius:999px;padding:1px 6px;margin-left:5px} 85 86 .ov{position:fixed;inset:0;background:rgba(15,20,35,.5);display:flex;align-items:center;justify-content:center;z-index:500;padding:16px} 87 .ov[hidden]{display:none} 88 .modal{background:#fff;border-radius:16px;padding:20px 22px;max-width:460px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,.3);max-height:90vh;overflow:auto} 89 .modal h3{margin:0 0 12px;color:#1f2a44;font-size:18px} 90 .modal.wide{max-width:560px} 91 .pf-head{display:flex;align-items:center;gap:14px;margin-bottom:12px} 92 .pf-ava{width:60px;height:60px;border-radius:50%;flex:none;background:#eef1f5;border:1px solid #e1e6ef;display:flex;align-items:center;justify-content:center;color:#9aa1ab;font-weight:800;font-size:20px;overflow:hidden} 93 .pf-ava img{width:100%;height:100%;object-fit:cover} 94 .pf-name{font-size:18px;font-weight:800;color:#1f2a44} 95 .modal h3.pf-name{margin:0 0 3px} 96 .pf-pos{font-size:12.5px;color:#475569;font-weight:700} 97 .pf-meta{font-size:13px;color:#475569;margin-bottom:5px} 98 .pf-meta a{color:#1d4ed8} 99 .pf-bio{font-size:13px;color:#334155;background:#f7f8fa;border:1px solid #eef0f3;border-radius:8px;padding:9px 11px;margin:8px 0 2px;white-space:pre-wrap;overflow-wrap:anywhere} 100 .pf-sech{font-weight:800;color:#1d4ed8;font-size:14px;margin:16px 0 9px;border-top:1px solid #eef0f3;padding-top:14px} 101 .pf-app{background:#fff;border:1px solid #e6ebf5;border-radius:9px;padding:9px 11px;margin-bottom:7px} 102 .pf-app-top{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap} 103 .pf-org{font-weight:700;color:#1f2a44;font-size:13.5px} 104 .pf-score{font-weight:700;color:#1d4ed8;font-size:12px;margin-left:6px} 105 .pf-status{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;border-radius:999px;padding:2px 8px;white-space:nowrap} 106 .pf-status.pending{color:#92400e;background:#fef3c7} 107 .pf-status.approved{color:#065f46;background:#d1fae5} 108 .pf-link{font-size:12px;color:#1d4ed8;font-weight:700;text-decoration:none;margin-top:6px;display:inline-block} 109 @media (max-width:640px){ .pf-link{display:inline-flex;align-items:center;min-height:44px;margin-top:0} } 110 .pf-note{font-size:12px;color:#475569;font-style:italic;margin-top:5px} 111 .pf-sub{font-weight:700;font-size:12.5px;margin-bottom:6px} 112 .pf-empty{font-size:13px;color:#9aa1ab} 113 .fld{margin-bottom:10px} 114 .updlist{display:flex;flex-direction:column;gap:8px} 115 .updrow{display:flex;align-items:center;gap:12px;background:#fff;border:1px solid #e3e7ef;border-radius:10px;padding:10px 13px} 116 .updrow-main{flex:1;min-width:0} 117 .updrow-t{font-weight:700;color:#1f2a44;font-size:14px} 118 .updtag{font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;border-radius:999px; 119 padding:2px 7px;background:#fdecec;color:#b8232a;border:1px solid #f5c9c9;vertical-align:1px} 120 .updtag.draft{background:#eef1f5;color:#6b7280;border-color:#dfe3ea} 121 .fld label{display:block;font-size:12px;font-weight:700;color:#475569;margin-bottom:3px} 122 .fld input,.fld select{width:100%;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box} 123 .fhint{font-size:11.5px;color:#64748b;margin-top:4px}
124 .modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:6px} 125 .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:#1f2a44;color:#fff;padding:10px 16px;border-radius:10px;font-size:13px;font-weight:600;box-shadow:0 8px 24px rgba(0,0,0,.25);opacity:0;transition:opacity .2s;z-index:600;pointer-events:none} 126 .toast.show{opacity:1} 127 128 .rsvpbox{background:#fff;border:1px solid var(--line,#e3e7ef);border-radius:16px;box-shadow:0 6px 20px rgba(20,25,45,.06);padding:16px 18px;margin:0 0 18px} 129 .rsvphead{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap} 130 .rsvpbox h2{margin:0;font-size:15px;color:#1f2a44} 131 .rsvpwhen{font-size:13px;color:#6b7280;margin-top:2px} 132 .rsvpacts{display:flex;gap:7px;flex-wrap:wrap} 133 .rsvpcounts{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0 10px} 134 .rsvpchip{font-size:12px;font-weight:700;border-radius:999px;padding:5px 12px;background:#eef1f5;color:#6b7280} 135 .rsvpchip.yes{background:#dcfce7;color:#166534} 136 .rsvpchip.no{background:#fee2e2;color:#991b1b} 137 .rsvpchip.maybe{background:#fef3c7;color:#92400e} 138 .rsvplist{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:7px} 139 .rsvprow{display:flex;justify-content:space-between;align-items:center;gap:8px;border:1px solid #eef1f7; 140 border-radius:10px;padding:8px 11px;font-size:13px} 141 .rsvprow b{font-weight:600;color:#1f2a44} 142 .rsvptag{font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;border-radius:999px;padding:2px 8px} 143 .rsvptag.yes{background:#dcfce7;color:#166534} 144 .rsvptag.no{background:#fee2e2;color:#991b1b} 145 .rsvptag.maybe{background:#fef3c7;color:#92400e} 146 .rsvptag.none{background:#eef1f5;color:#8a93a0} 147 .rsvpnote{font-size:12px;color:#6b7280;font-style:italic;margin-top:2px} 148 @media(max-width:560px){ .rsvplist{grid-template-columns:1fr} } 149 /* Board items (the board room's staff panel). Every control is a 44px target; the dialogs scroll inside themselves. */ 150 #biBox .rsvpacts .btn,#biBox .bi-rowacts .btn,.bi-modal .btn{min-height:44px;font-size:13px;padding:8px 14px} 151 .bi-row{flex-wrap:wrap} 152 .bi-row .updrow-main{flex:1 1 260px} 153 .bi-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;color:#475569;font-weight:700} 154 .bi-rowacts{display:flex;gap:7px;flex-wrap:wrap} 155 .bi-msg{font-size:13px;font-weight:600;border-radius:10px;padding:10px 12px;margin:10px 0;overflow-wrap:anywhere} 156 .bi-msg.bi-ok{background:#e9f4ee;border:1px solid #c2e0d0;color:#14532d} 157 .bi-msg.bi-bad{background:#fdecec;border:1px solid #f3c3c3;color:#8d1c1c} 158 .bi-msg.bi-info{background:#edf2fd;border:1px solid #c8d7f5;color:#1b3a78} 159 .bi-modal{max-width:640px} 160 .bi-modal .modal-actions,.bi-rowacts{flex-wrap:wrap} 161 .bi-modal .btn,#biBox .btn{white-space:normal;text-align:center} 162 .bi-modal fieldset{border:0;padding:0;margin:0 0 12px;min-width:0} 163 .bi-modal legend{font-size:13px;font-weight:800;color:#1f2a44;padding:0;margin:0 0 6px} 164 .bi-choice{display:flex;align-items:center;gap:9px;min-height:44px;padding:6px 10px;border:1px solid #e3e7ef;border-radius:10px;margin:0 0 6px;font-size:13.5px;color:#1f2a44;cursor:pointer} 165 .bi-choice input{width:18px;height:18px;margin:0;flex:none} 166 .bi-rule{font-size:13.5px;color:#1b3a78;background:#edf2fd;border:1px solid #c8d7f5;border-radius:10px;padding:10px 12px;margin:0 0 12px} 167 .bi-modal textarea{width:100%;font:inherit;font-size:13.5px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box;min-height:88px} 168 .bi-modal input[type=datetime-local],.bi-modal input[type=date]{width:100%;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box;min-height:44px} 169 .bi-modal h4{margin:16px 0 6px;font-size:13.5px;color:#1f2a44} 170 .bi-preview{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px;margin:12px 0} 171 @media(max-width:620px){ .bi-preview{grid-template-columns:minmax(0,1fr)} } 172 .bi-cap{font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#64748b;margin:0 0 6px} 173 .bi-phone{border:2px solid #1f2a44;border-radius:22px;padding:12px;max-width:340px;background:#f7f5f4;font-size:13px;line-height:1.45;overflow-wrap:anywhere} 174 .bi-phone p{margin:0 0 6px} 175 .bi-ph-head{background:#fff;border-left:4px solid #b4232b;border-radius:10px;padding:8px 10px;margin:0 0 8px} 176 .bi-ph-title{font-weight:800;font-size:15px;color:#1f2a44} 177 .bi-ph-words,.bi-mailtext{white-space:pre-wrap;background:#fff;border:1px solid #e3e7ef;border-radius:8px;padding:8px 10px;margin:0 0 6px} 178 .bi-mail{border:1px solid #e3e7ef;border-radius:12px;padding:12px;background:#fff;font-size:13px;line-height:1.45;overflow-wrap:anywhere;min-width:0} 179 .bi-mail p{margin:0 0 6px} 180 .bi-sure{border-top:1px solid #eef0f3;margin-top:12px;padding-top:12px} 181 .bi-sure p{font-size:14px;line-height:1.5;color:#1f2a44;margin:0 0 10px} 182 .bi-facts{font-size:13px;color:#475569;line-height:1.55;margin:0 0 10px} 183 .bi-facts p{margin:0 0 3px} 184 .bi-docs{margin:0 0 10px;padding-left:18px;font-size:13px} 185 .bi-comment{border-top:1px solid #f1f3f6;padding:8px 0;font-size:13px} 186 .bi-comment .bi-ctext{white-space:pre-wrap;overflow-wrap:anywhere} 187 .bi-comment summary{cursor:pointer;min-height:44px;display:flex;align-items:center;color:#6b7280;font-weight:700} 188 .bi-entries{font-size:12.5px;color:#475569;padding-left:18px;margin:6px 0} 189 .bi-every>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:700;color:#1f2a44;font-size:13px} 190 </style> 191
191<script src="auth.js?v=73daad4c41"></script>
191 192
192<script defer src="navbadge.js?v=8bba535fd2"></script>
192 193 <link rel="stylesheet" href="rrcc-ui.css?v=60e872b858" /> 194
194<script defer src="rrcc-ui.js?v=5e7ec569a3"></script>
194 195 <link rel="stylesheet" href="board-room.css?v=6abd6c4839"> 196
196<script defer src="board-decisions-page.js?v=c8b41735db"></script>
196 197</head> 198<body> 199 <header class="sitehdr"><a class='logolink' href='/'><img class="sitelogo" src="assets/rrcc-logo.png" alt="Red River Community Corps"></a><button id="featBtn" type="button" class="hdrcta">Request a feature</button></header> 200 <nav class="progtabs"> 201 <a class='progtab' href='/'>Oklahoma Opportunities</a> 202 <a class='progtab' href='/vista-dashboard'>VISTA</a> 203 <a class='progtab active' href='/board-members'>Board Members</a> 204 <a class='feattab' href='/jobs'>Service Opportunities</a> 205 </nav> 206 <nav class="topnav"> 207 <div class="fitline" style="font-weight:800;color:#1f2a44">RRCC Board of Directors</div> 208 </nav> 209 210 <main class="wrap"> 211 <!-- Board items: the board room's staff panel (DESIGN.md "pagesAndEmails" page 4). Staff prepare, preview and open items 212 for the board room and see where each stands. Items about the Executive Director, Executive Committee items and 213 items for directors only never appear here (the board-items route leaves them out, titles included). --> 214 <section class="rsvpbox" id="biBox" aria-labelledby="biHead"> 215 <div class="rsvphead"> 216 <div> 217 <h2 id="biHead">Board items</h2> 218 <div class="rsvpwhen">Items for the board room, the official record of what directors read, said and decided. Once an item is open it cannot be changed or deleted, only added to. Items about the Executive Director never appear here.</div> 219 </div> 220 <div class="rsvpacts"> 221 <button class="btn sm primary" id="biNew" type="button">Prepare an item</button> 222 <button class="btn sm" id="biCheck" type="button">Open the address check for all directors</button> 223 </div> 224 </div> 225 <div id="biMsg"></div> 226 <div class="updlist" id="biList"><div class="rsvpwhen">Loading.</div></div> 227 </section> 228 229 <section class="rsvpbox" id="updBox"> 230 <div class="rsvphead"> 231 <div> 232 <h2>Updates for the board</h2> 233 <div class="rsvpwhen">Posted to every board member's own dashboard. They see these the moment you publish.</div> 234 </div> 235 <div class="rsvpacts"> 236 <button class="btn sm" id="updNew" type="button">Post an update</button> 237 </div> 238 </div> 239 <div class="updlist" id="updList"><div class="rsvpwhen">Loading.</div></div> 240 </section> 241 242 <section class="rsvpbox" id="minBox"> 243 <div class="rsvphead"> 244 <div> 245 <h2>Meeting minutes</h2> 246 <div class="rsvpwhen">Once the board approves a set of minutes, record it here and send it to the Secretary to sign in DocuSign. When the envelope completes this marks itself signed and files the signed copy on every board member's dashboard. While anything sits unsigned you get an email once a week.</div> 247 </div> 248 <div class="rsvpacts"> 249 <button class="btn sm" id="minNew" type="button">Record approved minutes</button> 250 </div> 251 </div> 252 <div class="updlist" id="minList"><div class="rsvpwhen">Loading.</div></div> 253 <div class="rsvpwhen" id="minUnmatched" style="margin-top:10px"></div> 254 </section> 255 256 <div class="ov" id="minOv" hidden> 257 <div class="modal" role="dialog" aria-modal="true" aria-labelledby="minModalTitle"> 258 <h3 id="minModalTitle">Record approved minutes</h3> 259 <div class="fld"><label for="minDate">Which meeting</label> 260 <input id="minDate" type="text" placeholder="e.g. May 26, 2026" maxlength="80"> 261 <div class="rsvpwhen" style="margin-top:4px">The meeting the minutes cover, not today's date. This is what gets matched to the DocuSign envelope, so a month and a year need to be in there.</div> 262 </div> 263 <div class="fld"><label for="minTitle">Title (optional)</label> 264 <input id="minTitle" type="text" placeholder="May 2026 Board Meeting Minutes" maxlength="200"></div> 265 <div class="fld"><label for="minNote">Note (optional)</label> 266 <input id="minNote" type="text" placeholder="Anything worth remembering about this approval" maxlength="2000"></div> 267 <div id="minMsg" style="display:none;font-size:13px;font-weight:600;margin-bottom:8px"></div> 268 <div class="modal-actions"> 269 <button class="btn" id="minCancel" type="button">Cancel</button> 270 <button class="btn primary" id="minSave" type="button">Record it</button> 271 </div> 272 </div> 273 </div> 274 275 <div class="ov" id="updOv" hidden> 276 <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="updModalTitle"> 277 <h3 id="updModalTitle">Post an update</h3> 278 <div class="fld"><label for="updTitle">Title</label><input id="updTitle" type="text" placeholder="e.g. 2025-2026 Annual Report" maxlength="200"></div> 279 <div class="fld"><label for="updBody">Message</label>
279<textarea id="updBody" rows="7" style="width:100%;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box" placeholder="What the board should know." aria-describedby="updPrivacy"></textarea><p id="updPrivacy" style="margin:6px 0 0;font-size:12.5px;color:#5f5650">Board members open updates by their link, without a password. Leave out member names, contact details and case notes.</p></div> 280 <div class="fld"><label for="updLink">Link (optional)</label> 281 <input id="updLink" type="url" placeholder="https://rrccdashboard.netlify.app/board-docs/report.pdf"> 282 <div class="rsvpwhen" style="margin-top:4px">Use this for anything large. A PDF hosted on this site opens right on their page. No size limit, unlike an upload.</div> 283 </div> 284 <div class="fld"><label for="updLinkLabel">What to call the link (optional)</label> 285 <input id="updLinkLabel" type="text" placeholder="2025 Annual Report" maxlength="120"></div> 286 <div class="fld"><label for="updFile">Attachment (optional, about 4 MB max)</label> 287 <input id="updFile" type="file" accept=".pdf,.doc,.docx,.xlsx,.ppt,.pptx,.png,.jpg,.jpeg"> 288 <div class="rsvpwhen" id="updFileNow" style="margin-top:4px"></div> 289 </div> 290 <label style="display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:#475569;margin:4px 0 10px"> 291 <input type="checkbox" id="updPin"> Pin to the top of their page 292 </label> 293 <div id="updMsg" style="display:none;font-size:13px;font-weight:600;margin-bottom:8px"></div> 294 <div class="modal-actions"> 295 <button class="btn" id="updDelete" type="button" hidden style="margin-right:auto;color:#b8232a;border-color:#e7b9b9">Remove</button> 296 <button class="btn" id="updCancel" type="button">Cancel</button> 297 <button class="btn primary" id="updSave" type="button">Publish</button> 298 </div> 299 </div> 300 </div> 301 <section class="rsvpbox" id="rsvpBox" hidden> 302 <div class="rsvphead"> 303 <div> 304 <h2 id="rsvpTitle">Meeting RSVPs</h2> 305 <div class="rsvpwhen" id="rsvpWhen"></div> 306 </div> 307 <div class="rsvpacts"> 308 <button class="btn sm" id="rsvpCopy" type="button">Copy RSVP link</button> 309 <button class="btn sm" id="rsvpSet" type="button">Set the meeting</button> 310 </div> 311 </div> 312 <div class="rsvpcounts" id="rsvpCounts"></div>
313 <div class="rsvplist" id="rsvpList"></div> 314 </section> 315 316 <div class="head"> 317 <h1>Board Members</h1> 318 <p>The RRCC Board of Directors. Email a member or a selected group (opens a draft in your own email app, everyone in To:), and send each member a private link to confirm their info and add a headshot. Click a member to edit their record.</p> 319 </div> 320 <div class="countbar" id="counts"></div> 321 <div class="toolbar"> 322 <input type="search" id="q" placeholder="Search name, position, or emailâ¦" /> 323 <button class="btn" id="addBtn" type="button">+ Add member</button> 324 <label><input type="checkbox" id="showArchived" /> Show archived</label> 325 </div> 326 <div class="actionbar"> 327 <label><input type="checkbox" id="selAll" /> <span style="font-weight:700">Select all</span></label> 328 <span class="sel" id="selCount">0 selected</span> 329 <button class="btn primary" id="emailSel" type="button" disabled>Email selected</button> 330 <button class="btn" id="linkSel" type="button" disabled>Send profile links to selected</button> 331 </div> 332 <div id="rotation"></div> 333 <details class="actlog" id="actlog"><summary>Board activity log</summary><div class="actlist" id="actlist"><p class="actempty">Loading.</p></div></details> 334 <div class="grid" id="list"><p class="empty">Loadingâ¦</p></div> 335 </main> 336 337 <div class="ov" id="editOv" hidden> 338 <div class="modal" role="dialog" aria-modal="true" aria-labelledby="editTitle"> 339 <h3 id="editTitle">Edit member</h3> 340 <div class="fld"><label>Name</label><input id="f_name" type="text" /></div> 341 <div class="fld"><label>Position</label> 342 <select id="f_position"> 343 <option>Board Chair</option><option>Vice Chair</option><option>Board Treasurer</option><option>Board Secretary</option><option>Board of Directors</option> 344 </select></div> 345 <div class="fld"><label>Term <span style="font-weight:600;color:#9aa1ab">(auto â every 2 years from the start date)</span></label><input id="f_term" type="text" disabled style="background:#f3f5f9;color:#9aa1ab" /></div> 346 <div class="fld"><label>Initial start</label><input id="f_start" type="text" placeholder="Jan 2024" /></div> 347 <div class="fld"><label>Final term end</label><input id="f_end" type="text" placeholder="Jan 2030" /></div> 348 <div class="fld"><label>Email</label><input id="f_email" type="email" /> 349 <div class="fhint" id="f_email_hint" hidden>Changing this email does not change where this director's sign-in codes go.</div></div> 350 <div class="modal-actions"> 351 <button class="btn" id="editCancel" type="button">Cancel</button> 352 <button class="btn primary" id="editSave" type="button">Save</button> 353 </div> 354 </div> 355 </div> 356 357 <!-- Profile: a STAFF-SIDE view of the member, built from the roster (2026-09-30). It used to open the member's own 358 dashboard with their private link, which is the member's key to the portal; staff never hold that link now. --> 359 <div class="ov" id="profileOv" hidden> 360 <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="pfTitle"> 361 <div id="pfBody"></div> 362 <div class="modal-actions"> 363 <button class="btn" id="pfClose" type="button">Close</button> 364 </div> 365 </div> 366 </div> 367 368 <!-- Board items: Prepare (the two questions, the rule, the words, the documents, the deadline, the help contact), 369 Preview (the phone view and the email) and Open; then one item's later steps. --> 370 <div class="ov" id="biPrepOv" hidden> 371 <div class="modal wide bi-modal" role="dialog" aria-modal="true" aria-labelledby="biPrepTitle"> 372 <h3 id="biPrepTitle">Prepare a board item</h3> 373 <fieldset> 374 <legend>What do you need from the board?</legend> 375 <label class="bi-choice"><input type="radio" name="biNeed" value="read"> Just read it</label> 376 <label class="bi-choice"><input type="radio" name="biNeed" value="comments"> Comments</label> 377 <label class="bi-choice"><input type="radio" name="biNeed" value="meeting"> A decision at the next meeting</label> 378 <label class="bi-choice"><input type="radio" name="biNeed" value="vote"> A vote before the meeting</label> 379 </fieldset> 380 <fieldset> 381 <legend>What is it about?</legend> 382 <label class="bi-choice"><input type="radio" name="biAbout" value="budget"> Budget</label> 383 <label class="bi-choice"><input type="radio" name="biAbout" value="policy"> Policy</label> 384 <label class="bi-choice"><input type="radio" name="biAbout" value="minutes"> Minutes</label> 385 <label class="bi-choice"><input type="radio" name="biAbout" value="bylaws"> Bylaws</label> 386 <label class="bi-choice"><input type="radio" name="biAbout" value="other"> Other</label> 387 </fieldset> 388 <p class="bi-rule" id="biRule">Answer both questions, and the rule for this item shows here in one sentence.</p> 389 <fieldset id="biBudgetFields" hidden> 390 <legend>Budget pay checks</legend> 391 <p>
391Does this budget change the Executive Director's pay or benefits?</p> 392 <label class="bi-choice"><input type="radio" name="biBudgetPay" value="yes"> Yes</label> 393 <label class="bi-choice"><input type="radio" name="biBudgetPay" value="no"> No</label> 394 <p class="fhint">A budget that changes that pay must exclude the Executive Director's pay from its motion. The separate private pay decision handles it.</p> 395 <div class="fld"><label for="biStaffPayBasis">Basis for staff pay in this budget</label><textarea id="biStaffPayBasis" maxlength="1000" rows="3" placeholder="Briefly explain the basis for staff pay."></textarea></div> 396 </fieldset> 397 <div class="fld"><label for="biTitle">Title</label><input id="biTitle" type="text" maxlength="200" placeholder="e.g. Read the 2027 board calendar"></div> 398 <div class="fld"><label for="biWords" id="biWordsLabel">The exact words</label><textarea id="biWords" rows="5" maxlength="4000" placeholder="What directors read. For a decision, the motion word for word."></textarea></div> 399 <div class="fld"><label for="biFiles">Documents (optional)</label><input id="biFiles" type="file" multiple accept=".pdf,.docx,.xlsx,.png,.jpg,.jpeg"><div class="fhint" id="biFilesNow">PDF, Word, Excel, PNG or JPEG. Up to 10 files, 4 MB each.</div></div> 400 <div class="fld"><label for="biDeadline" id="biDeadlineLabel">Deadline, Central time</label><input id="biDeadline" type="datetime-local"></div> 401 <div class="fld" id="biMeetingFld" hidden><label for="biMeeting">Meeting date</label><input id="biMeeting" type="date"></div> 402 <div class="fld"><label for="biHelp">Help contact (optional)</label><input id="biHelp" type="email" maxlength="254" placeholder="Leave blank for Nicky Kazerooni"><div class="fhint">A staff address from People and Routing. Directors are told to ask this person for help.</div></div> 403 <div id="biPrepMsg"></div> 404 <div id="biPreview"></div> 405 <div class="modal-actions" id="biPrepActs"> 406 <button class="btn" id="biPrepCancel" type="button">Cancel</button> 407 <button class="btn" id="biPreviewBtn" type="button">Preview</button> 408 <button class="btn primary" id="biOpenBtn" type="button">Open the item</button> 409 </div> 410 <div class="bi-sure" id="biSure" hidden> 411 <p id="biSureText"></p> 412 <div class="modal-actions"> 413 <button class="btn" id="biSureBack" type="button">Go back</button> 414 <button class="btn primary" id="biSureOpen" type="button">Open it</button> 415 </div> 416 </div> 417 </div> 418 </div> 419 420 <div class="ov" id="biItemOv" hidden> 421 <div class="modal wide bi-modal" role="dialog" aria-modal="true" aria-labelledby="biItemTitle"> 422 <h3 id="biItemTitle">Board item</h3> 423 <div id="biItemBody"></div> 424 <div class="modal-actions"> 425 <button class="btn" id="biItemClose" type="button">Close</button> 426 </div> 427 </div> 428 </div> 429 430 <div class="toast" id="toast"></div> 431 432
432<script> 433 (function(){ 434 var FN="/.netlify/functions"; 435 var MEM=[], SEL={}, ROT={order:[],nextId:null,nextName:""}, editingId=null; 436 function $(s){ return document.querySelector(s); } 437 function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){return {"&":"&","<":"<",">":">","\"":""","'":"'"}[c];}); } 438 // ---- Meeting minutes, on their way to the Secretary's signature ------------ 439 var MIN=[]; 440 function minMsg(t,err){ var m=document.getElementById("minMsg"); if(!m) return; 441 m.textContent=t||""; m.style.display=t?"block":"none"; m.style.color=err?"#b8232a":"#177245"; } 442 function minWhen(at){ return at?new Date(at).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):""; } 443 444 function renderMinutes(){ 445 var box=document.getElementById("minList"); if(!box) return; 446 if(!MIN.length){ box.innerHTML='<div class="rsvpwhen">Nothing recorded yet. After the board votes to approve a set of minutes, record it here so it does not get forgotten.</div>'; return; } 447 box.innerHTML=MIN.map(function(m){ 448 var pill = m.signedAt 449 ? '<span class="updtag" style="background:#e6f4ea;color:#177245">Signed</span>' 450 : (m.status==="sent" 451 ? '<span class="updtag" style="background:#fff4e0;color:#8a5a00">Out for signature</span>' 452 : '<span class="updtag" style="background:#fdecec;color:#b8232a">Not sent yet</span>'); 453 var sub = m.signedAt 454 ? ("Signed "+esc(minWhen(m.signedAt))+(m.signedBy?(" by "+esc(m.signedBy)):"")) 455 : ("Approved "+esc(minWhen(m.approvedAt))+(m.approvedBy?(" · "+esc(m.approvedBy)):"") 456 + (m.remindCount?(" · reminded "+m.remindCount+"x"):"")); 457 var acts = m.signedAt ? "" : 458 '<button class="btn sm" data-minsent="'+esc(m.id)+'" type="button">'+(m.status==="sent"?"Not sent after all":"Mark sent for signature")+'</button>'; 459 return '<div class="updrow"><div class="updrow-main">' 460 + '<div class="updrow-t">'+esc(m.title||m.meetingDate)+' '+pill+'</div>' 461 + '<div class="rsvpwhen">'+sub+'</div></div>' 462 + acts 463 + '<button class="btn sm" data-minrm="'+esc(m.id)+'" type="button" style="color:#b8232a;border-color:#e7b9b9">Remove</button></div>'; 464 }).join(""); 465 box.querySelectorAll("[data-minsent]").forEach(function(b){ 466 b.onclick=function(){ var id=b.getAttribute("data-minsent"); 467 var rec=MIN.filter(function(x){return x.id===id;})[0]; 468 minPost({action:(rec&&rec.status==="sent")?"markMinutesUnsent":"markMinutesSent", id:id}); }; 469 }); 470 box.querySelectorAll("[data-minrm]").forEach(function(b){ 471 b.onclick=function(){ var id=b.getAttribute("data-minrm"); 472 var rec=MIN.filter(function(x){return x.id===id;})[0]; 473 if(!confirm("Remove the record for "+((rec&&(rec.title||rec.meetingDate))||"these minutes")+"? This does not touch anything in DocuSign.")) return; 474 minPost({action:"removeMinutes", id:id}); }; 475 }); 476 } 477 478 function applyMinutes(d){ 479 var box=document.getElementById("minList"); 480 if(!d||!d.ok){ if(box) box.innerHTML='<div class="rsvpwhen">'+esc((d&&d.error)||"Could not load the minutes.")+'</div>'; return; } 481 MIN=d.minutes||[]; renderMinutes(); 482 var u=document.getElementById("minUnmatched"); 483 if(u) u.innerHTML=(d.unmatched&&d.unmatched.length) 484 ? ('A signed envelope came back that could not be matched to one meeting: ' 485 + esc(d.unmatched[0].subject||"(no subject)") 486 + '. Record those minutes above, then re-send, or ask for it to be attached by hand.') 487 : ""; 488 } 489 function loadMinutes(){ 490 fetch(FN+"/board-members?minutes=1",{cache:"no-store"}) 491 .then(function(r){return r.json();}) 492 .then(applyMinutes) 493 .catch(function(){ var box=document.getElementById("minList"); if(box) box.innerHTML='<div class="rsvpwhen">Could not load the minutes.</div>'; }); 494 } 495 496 function minPost(body){ 497 return post(body).then(function(d){
498 if(!d||!d.ok){ alert((d&&d.error)||"That did not save."); return d; } 499 loadMinutes(); return d; 500 }); 501 } 502 503 function wireMinutes(){ 504 var nb=document.getElementById("minNew"), ov=document.getElementById("minOv"); 505 if(nb) nb.onclick=function(){ 506 ["minDate","minTitle","minNote"].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=""; }); 507 minMsg(""); if(ov) ov.hidden=false; var f=document.getElementById("minDate"); if(f) f.focus(); 508 }; 509 var cancel=document.getElementById("minCancel"); 510 if(cancel) cancel.onclick=function(){ if(ov) ov.hidden=true; }; 511 var save=document.getElementById("minSave"); 512 if(save) save.onclick=function(){ 513 var date=(document.getElementById("minDate")||{}).value||""; 514 if(!date.trim()){ minMsg("Put in which meeting these minutes are for.",true); return; } 515 save.disabled=true; minMsg("Saving."); 516 minPost({action:"recordMinutes", meetingDate:date, 517 title:(document.getElementById("minTitle")||{}).value||"", 518 note:(document.getElementById("minNote")||{}).value||""}) 519 .then(function(d){ save.disabled=false; 520 if(d&&d.ok){ if(ov) ov.hidden=true; minMsg(""); } else { minMsg((d&&d.error)||"That did not save.",true); } }) 521 .catch(function(){ save.disabled=false; minMsg("That did not save.",true); }); 522 }; 523 } 524 525 // ---- Updates for the board ------------------------------------------------- 526 var UPD=[], UPDEDIT=null, UPDFILE=null; 527 function updMsg(t,err){ var m=document.getElementById("updMsg"); if(!m) return; 528 m.textContent=t||""; m.style.display=t?"block":"none"; m.style.color=err?"#b8232a":"#177245"; } 529 function updWhen(at){ return at?new Date(at).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):""; } 530 531 function renderUpdates(){ 532 var box=document.getElementById("updList"); if(!box) return; 533 if(!UPD.length){ box.innerHTML='<div class="rsvpwhen">Nothing posted yet. The board sees this section on their own dashboard as soon as you publish something.</div>'; return; } 534 var list=UPD.slice().sort(function(a,b){ 535 if(!!b.pinned !== !!a.pinned) return b.pinned?1:-1; 536 return (b.at||0)-(a.at||0); 537 }); 538 box.innerHTML=list.map(function(u){ 539 return '<div class="updrow" data-upd="'+esc(u.id)+'">' 540 + '<div class="updrow-main"><div class="updrow-t">'+esc(u.title) 541 + (u.pinned?' <span class="updtag">Pinned</span>':'') 542 + (u.draft?' <span class="updtag draft">Draft</span>':'')+'</div>' 543 + '<div class="rsvpwhen">'+esc(updWhen(u.at))+(u.by?(" · "+esc(u.by)):"") 544 + (u.doc?(" · "+esc(u.doc.filename)):"")+'</div></div>' 545 + '<button class="btn sm" data-updedit="'+esc(u.id)+'" type="button">Edit</button></div>'; 546 }).join(""); 547 box.querySelectorAll("[data-updedit]").forEach(function(b){ 548 b.onclick=function(){ openUpd(UPD.filter(function(x){return x.id===b.getAttribute("data-updedit");})[0]); }; 549 }); 550 } 551 552 function applyUpdates(d){ 553 if(!d||!d.ok){ document.getElementById("updList").innerHTML='<div class="rsvpwhen">'+esc((d&&d.error)||"Could not load the updates.")+'</div>'; return; } 554 UPD=d.updates||[]; renderUpdates(); 555 } 556 function loadUpdates(){ 557 fetch(FN+"/board-members?updates=1",{cache:"no-store"}) 558 .then(function(r){return r.json();}) 559 .then(applyUpdates) 560 .catch(function(){ document.getElementById("updList").innerHTML='<div class="rsvpwhen">Could not load the updates.</div>'; }); 561 } 562 563 function openUpd(u){ 564 UPDEDIT=u||null; UPDFILE=null; updMsg(""); 565 document.getElementById("updModalTitle").textContent=u?"Edit update":"Post an update"; 566 document.getElementById("updTitle").value=u?(u.title||""):""; 567 document.getElementById("updLink").value=u?(u.link||""):""; 568 document.getElementById("updLinkLabel").value=u?(u.linkLabel||""):""; 569 document.getElementById("updBody").value=u?(u.body||""):""; 570 document.getElementById("updPin").checked=u?!!u.pinned:false; 571 document.getElementById("updFile").value=""; 572 document.getElementById("updFileNow").textContent=(u&&u.doc)?("Attached: "+u.doc.filename+". Choosing a new file replaces it."):""; 573 document.getElementById("updDelete").hidden=!u; 574 document.getElementById("updSave").textContent=u?"Save changes":"Publish"; 575 document.getElementById("updOv").hidden=false; 576 } 577 function closeUpd(){ document.getElementById("updOv").hidden=true; UPDEDIT=null; UPDFILE=null; } 578 579 function saveUpd(){ 580 var title=document.getElementById("updTitle").value.trim(); 581 if(!title){ updMsg("Give the update a title.",true); return; } 582 var btn=document.getElementById("updSave"); btn.disabled=true; btn.textContent="Savingâ¦"; 583 var payload={ action: UPDEDIT?"editUpdate":"publishUpdate", id: UPDEDIT?UPDEDIT.id:undefined, 584 title:title, body:document.getElementById("updBody").value, pinned:document.getElementById("updPin").checked, 585 link:document.getElementById("updLink").value, linkLabel:document.getElementById("updLinkLabel").value }; 586 if(UPDFILE){ payload.docBase64=UPDFILE.b64; payload.filename=UPDFILE.name; } 587 post(payload).then(function(d){
588 if(!d||!d.ok){ updMsg((d&&d.error)||"Could not save that.",true); btn.disabled=false; btn.textContent=UPDEDIT?"Save changes":"Publish"; return; } 589 closeUpd(); loadUpdates(); 590 }).catch(function(){ updMsg("Could not save that.",true); btn.disabled=false; btn.textContent=UPDEDIT?"Save changes":"Publish"; }); 591 } 592 593 function wireUpdates(){ 594 var nb=document.getElementById("updNew"); if(nb) nb.onclick=function(){ openUpd(null); }; 595 var cb=document.getElementById("updCancel"); if(cb) cb.onclick=closeUpd; 596 var sv=document.getElementById("updSave"); if(sv) sv.onclick=saveUpd; 597 var del=document.getElementById("updDelete"); 598 if(del) del.onclick=function(){ 599 if(!UPDEDIT) return; 600 if(!confirm("Remove \""+UPDEDIT.title+"\"? The board will no longer see it.")) return; 601 del.disabled=true; 602 post({action:"removeUpdate",id:UPDEDIT.id}).then(function(d){ 603 del.disabled=false; 604 if(!d||!d.ok){ updMsg((d&&d.error)||"Could not remove that.",true); return; } 605 closeUpd(); loadUpdates(); 606 }).catch(function(){ del.disabled=false; updMsg("Could not remove that.",true); }); 607 }; 608 var f=document.getElementById("updFile"); 609 if(f) f.onchange=function(){ 610 var file=f.files&&f.files[0]; if(!file){ UPDFILE=null; return; } 611 // ~5 MB decoded is the server cap; catch it here so nobody waits on an upload 612 // that is going to be refused. 613 if(file.size>5*1024*1024){ updMsg("That file is larger than 5 MB. Try a smaller export.",true); f.value=""; UPDFILE=null; return; } 614 var rd=new FileReader(); 615 rd.onload=function(){ 616 UPDFILE={ name:file.name, b64:String(rd.result).split(",")[1]||"" }; 617 document.getElementById("updFileNow").textContent="Ready to attach: "+file.name; 618 updMsg(""); 619 }; 620 rd.readAsDataURL(file); 621 }; 622 } 623 624 function post(payload){ return fetch(FN+"/board-members",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload||{})}).then(function(r){return r.json();}); } 625 var toastT; function toast(m,ms){ var t=$("#toast"); t.textContent=m; t.classList.add("show"); clearTimeout(toastT); toastT=setTimeout(function(){t.classList.remove("show");},ms||2800); } 626 // A roster change is also written to the board record and the officers are emailed (board room, 2026-10-01). The 627 // change is saved either way; this says when the record is behind or someone was not told. 628 function rosterNote(d){ 629 var out=[]; 630 if(d&&d.recordWarning) out.push(String(d.recordWarning)); 631 if(d&&d.notice&&d.notice.of&&d.notice.sent<d.notice.of&&d.notice.message) out.push(String(d.notice.message)); 632 return out.join(" "); 633 } 634 function initials(n){ n=String(n||"").trim().split(/\s+/); return ((n[0]||"")[0]||"")+((n[1]||"")[0]||""); } 635 function posClass(p){ p=String(p||"").toLowerCase(); if(p.indexOf("chair")>=0&&p.indexOf("vice")<0)return"p-chair"; if(p.indexOf("vice")>=0)return"p-vice"; if(p.indexOf("treasurer")>=0)return"p-treasurer"; if(p.indexOf("secretary")>=0)return"p-secretary"; return"p-dir"; } 636 // Term is derived from the start date: every 2 years = one term (capped at the final-term-end span). 637 function parseDate(s){ if(!s) return null; var d=new Date(s); return isNaN(d.getTime())?null:d; } 638 function computeTerm(m){ 639 var s=parseDate(m.start); if(!s) return m.term||"â"; 640 var yrs=(Date.now()-s.getTime())/(365.25*864e5); if(yrs<0) return 1; 641 var t=Math.floor(yrs/2)+1; 642 var e=parseDate(m.end); if(e){ var total=Math.max(1,Math.round((e.getTime()-s.getTime())/(365.25*864e5)/2)); if(t>total) t=total; } 643 return t; 644 } 645 function photoUrl(m){ 646 var u=FN+"/board-members?photo=1&id="+encodeURIComponent(m.id); 647 return m.photoVersion ? (u+"&v="+encodeURIComponent(m.photoVersion)) : u; 648 } 649 650 function mget(id){ return MEM.find(function(m){return m.id===id;}); } 651 function visible(){ var q=($("#q").value||"").toLowerCase().trim(); var showRem=$("#showArchived").checked; 652 return MEM.filter(function(m){ if(m.removed&&!showRem) return false; if(!q) return true; 653 return (m.name+" "+m.position+" "+m.email).toLowerCase().indexOf(q)>=0; }); } 654 655 function card(m){ 656 var ava = m.hasPhoto ? ('<span class="ava"><img src="'+esc(photoUrl(m))+'" alt="'+esc(m.name)+'" loading="lazy" decoding="async"/></span>') : ('<span class="ava">'+esc(initials(m.name).toUpperCase())+'</span>'); 657 var meta=[]; 658 meta.push('<a href="mailto:'+esc(m.email)+'">'+esc(m.email)+'</a>'); 659 if(m.phone) meta.push(esc(m.phone)); 660 if(m.title) meta.push(esc(m.title)); 661 var bio = m.bio ? ('<div class="mbiowrap"><div class="mbio clamp">'+esc(m.bio)+'</div><button class="mbiomore" type="button" data-act="biomore" hidden>Show more</button></div>') : ""; 662 var upd = m.updatedBy==="self" ? '<div class="updated">Member updated their info</div>' : "";
663 var plsent = m.profileEmailAt ? ('<div class="updated">Profile link sent '+new Date(m.profileEmailAt).toLocaleDateString(undefined,{month:"short",day:"numeric"})+'</div>') : ""; 664 // hasLink is all staff are told about a member's private link (true, false, or null when it could not be checked). 665 var nolink = (m.hasLink===false && !m.removed) ? '<div class="nolink">No dashboard link yet. Send profile link makes one and emails it.</div>' : ""; 666 // Signed in to the board room with a code (true / false / null when the record could not be read). Never an address. 667 var signed = '<div class="msigned'+(m.signedIn===false?' notyet':'')+'">Signed in: '+(m.signedIn===true?"yes":(m.signedIn===false?"not yet":"not known"))+'</div>'; 668 var rv = m.review||{}; var rev = rv.assigned ? ('<div class="mrev">Application reviews: <b>'+rv.assigned+'</b> assigned · '+(rv.pending||0)+' pending · '+(rv.approved||0)+' approved</div>') : ""; 669 return '<div class="mcard'+(SEL[m.id]?" sel":"")+'" data-id="'+esc(m.id)+'">'+ 670 '<div class="mtop">'+ava+ 671 '<div style="flex:1;min-width:0">'+ 672 '<div class="mname">'+esc(m.name||"(no name)")+'</div>'+ 673 '<span class="mpos '+posClass(m.position)+'">'+esc(m.position||"Board of Directors")+'</span>'+ 674 '<div class="mterm">Term '+computeTerm(m)+' · '+esc(m.start||"?")+' â '+esc(m.end||"?")+'</div>'+ 675 '</div>'+ 676 '<input class="pick" type="checkbox" data-act="pick" data-id="'+esc(m.id)+'"'+(SEL[m.id]?" checked":"")+' title="Select" />'+ 677 '</div>'+ 678 '<div class="mmeta">'+meta.join("<br>")+'</div>'+ 679 bio+upd+plsent+nolink+signed+rev+ 680 '<div class="mactions">'+ 681 '<button class="btn sm" data-act="profile" data-id="'+esc(m.id)+'">Profile</button>'+ 682 '<button class="btn sm" data-act="email" data-id="'+esc(m.id)+'">Email</button>'+ 683 '<button class="btn sm" data-act="sendprofile" data-id="'+esc(m.id)+'">Send profile link</button>'+ 684 '<button class="btn sm" data-act="resetlink" data-id="'+esc(m.id)+'">Reset link</button>'+ 685 '<button class="btn sm" data-act="edit" data-id="'+esc(m.id)+'">Edit</button>'+ 686 (m.removed?'<button class="btn sm" data-act="unarchive" data-id="'+esc(m.id)+'">Restore</button>':'<button class="btn sm" data-act="archive" data-id="'+esc(m.id)+'">Archive</button>')+ 687 '</div>'+ 688 '</div>'; 689 } 690 691 function renderCounts(){ 692 var active=MEM.filter(function(m){return !m.removed;}); 693 var officers=active.filter(function(m){return posClass(m.position)!=="p-dir";}).length; 694 var photos=active.filter(function(m){return m.hasPhoto;}).length; 695 var archived=MEM.filter(function(m){return m.removed;}).length; 696 $("#counts").innerHTML=[ 697 '<span class="chip">'+active.length+' members</span>', 698 '<span class="chip">Officers: '+officers+'</span>', 699 '<span class="chip">Headshots on file: '+photos+'</span>' 700 ].concat(archived?['<span class="chip">Archived: '+archived+'</span>']:[]).join(""); 701 } 702 function selectedMembers(){ return MEM.filter(function(m){return SEL[m.id]&&!m.removed;}); } 703 function refreshSel(){ 704 var n=selectedMembers().length; 705 $("#selCount").textContent=n+" selected"; 706 $("#emailSel").disabled=!n; $("#linkSel").disabled=!n; 707 } 708 // Move one member one place and save the whole order - the same shape the drag 709 // handler posts, so the two paths can never disagree about what an order is. 710 function nudgeRotation(id,dir){ 711 var order=((ROT&&ROT.order)||[]).map(function(o){return o.id;}); 712 var i=order.indexOf(id), j=i+dir; 713 if(i<0||j<0||j>=order.length) return; 714 order.splice(j,0,order.splice(i,1)[0]); 715 post({action:"setRotation",order:order}).then(function(d){ 716 if(d&&d.ok&&d.rotation){ ROT=d.rotation; } 717 renderRotation(); 718 }).catch(function(){ renderRotation(); }); 719 } 720 721 function renderRotation(){ 722 var el=$("#rotation"); if(!el) return; 723 var order=(ROT&&ROT.order)||[]; 724 if(!order.length){ el.innerHTML=""; return; } 725 // Each row carries move-up / move-down buttons as well as being draggable. 726 // The arrows were removed in July when dragging came in, which left reordering 727 // impossible on a phone - HTML5 drag never fires on a touch screen. They are back, 728 // and they drive the SAME setRotation POST the drag does. 729 var rows=order.map(function(o,i){ 730 var isNext=o.id===ROT.nextId; 731 return '<div class="rotrow'+(isNext?" next":"")+'" draggable="true" data-id="'+esc(o.id)+'" title="Drag to reorder, or use the arrows">'+ 732 '<span class="rnum">'+(i+1)+'.</span>'+ 733 '<span class="rname">'+esc(o.name)+(isNext?'<span class="rnext">next</span>':'')+'</span>'+ 734 '<span class="rmove">'+ 735 '<button type="button" class="rbtn" data-up="'+esc(o.id)+'" aria-label="Move '+esc(o.name)+' up"'+(i===0?" disabled":"")+'>↑</button>'+ 736 '<button type="button" class="rbtn" data-down="'+esc(o.id)+'" aria-label="Move '+esc(o.name)+' down"'+(i===order.length-1?" disabled":"")+'>↓</button>'+ 737 '</span>'+ 738 '</div>'; 739 }).join(""); 740 el.className="rotation"; 741 el.innerHTML='<div class="rothead">Application approval rotation</div><div class="rotsub">Applications auto-assign to the next member in this order. Next up: <b>'+esc(ROT.nextName||"-")+'</b>. Use the arrows to reorder, or drag a name on a computer.</div><div class="rotlist">'+rows+'</div>'; 742 // Bound directly, not delegated: this panel is re-rendered wholesale on every 743 // save, so a delegated handler on a parent that also re-renders would be lost. 744 Array.prototype.forEach.call(el.querySelectorAll("[data-up],[data-down]"),function(b){ 745 b.addEventListener("click",function(){ 746 var id=b.getAttribute("data-up")||b.getAttribute("data-down"); 747 var dir=b.hasAttribute("data-up")?-1:1; 748 nudgeRotation(id,dir); 749 }); 750 }); 751 } 752 function render(){ 753 var rows=visible(); 754 var list=$("#list"); 755 list.innerHTML = rows.length ? rows.map(card).join("") : '<p class="empty">No members match.</p>'; 756 renderRotation(); renderCounts(); refreshSel();
756 clampBios(); 757 } 758 // Show the "Show more" toggle only on bios that actually overflow the clamped lines. 759 function clampBios(){ 760 var ws=document.querySelectorAll("#list .mbiowrap"); 761 for(var i=0;i<ws.length;i++){ 762 var bx=ws[i].querySelector(".mbio"), btn=ws[i].querySelector(".mbiomore"); if(!bx||!btn) continue; 763 bx.classList.add("clamp"); btn.textContent="Show more"; 764 btn.hidden = bx.scrollHeight <= bx.clientHeight + 1; 765 } 766 } 767 // Drag a pill to reorder the rotation; the order saves on drop. 768 (function(){ 769 var wrap=$("#rotation"), dragEl=null; 770 wrap.addEventListener("dragstart",function(e){ var r=e.target.closest(".rotrow"); if(!r) return; dragEl=r; r.classList.add("dragging"); e.dataTransfer.effectAllowed="move"; try{ e.dataTransfer.setData("text/plain", r.getAttribute("data-id")); }catch(_){ } }); 771 wrap.addEventListener("dragover",function(e){ if(!dragEl) return; var list=e.target.closest(".rotlist"); if(!list) return; e.preventDefault(); e.dataTransfer.dropEffect="move"; 772 var over=e.target.closest(".rotrow"); 773 if(over&&over!==dragEl){ var rect=over.getBoundingClientRect(); var before=e.clientX < rect.left + rect.width/2; list.insertBefore(dragEl, before?over:over.nextSibling); } }); 774 wrap.addEventListener("drop",function(e){ if(dragEl) e.preventDefault(); }); 775 wrap.addEventListener("dragend",function(){ if(!dragEl) return; dragEl.classList.remove("dragging"); dragEl=null; 776 var order=[].map.call(wrap.querySelectorAll(".rotrow"),function(r){return r.getAttribute("data-id");}); 777 var cur=((ROT&&ROT.order)||[]).map(function(o){return o.id;}); 778 if(order.join("|")===cur.join("|")){ renderRotation(); return; } // dropped back where it started 779 post({action:"setRotation",order:order}).then(function(d){ if(d&&d.ok&&d.rotation){ ROT=d.rotation; } renderRotation(); }); }); 780 })(); 781 782 // Board activity log: fetch on first open, refresh on every open after that. 783 (function(){ 784 var box=document.getElementById("actlog"), list=document.getElementById("actlist"); if(!box||!list) return; 785 function fmtAt(ms){ try{ return new Date(ms).toLocaleDateString(undefined,{month:"short",day:"numeric"})+" "+new Date(ms).toLocaleTimeString(undefined,{hour:"numeric",minute:"2-digit"}); }catch(e){ return ""; } } 786 box.addEventListener("toggle",function(){ 787 if(!box.open) return; 788 list.innerHTML='<p class="actempty">Loading.</p>'; 789 fetch(FN+"/board-members?activity=1&t="+Date.now(),{cache:"no-store"}).then(function(r){return r.json();}).then(function(d){ 790 if(!d||!d.ok){ list.innerHTML='<p class="actempty">Could not load the activity log.</p>'; return; } 791 var ev=d.events||[]; 792 list.innerHTML = ev.length ? ev.map(function(e){ return '<div class="actrow"><span class="actwhen">'+esc(fmtAt(e.at))+'</span><span>'+esc(e.what)+'</span></div>'; }).join("") : '<p class="actempty">No board activity recorded yet.</p>'; 793 }).catch(function(){ list.innerHTML='<p class="actempty">Could not load the activity log.</p>'; }); 794 }); 795 })(); 796 function announceRsvpBootstrap(value){ 797 try{ window.dispatchEvent(new CustomEvent("rrcc:board-rsvp-bootstrap",{detail:value||null})); }catch(e){} 798 } 799 function load(){ 800 fetch(FN+"/board-members?bootstrap=1",{cache:"no-store"}).then(function(r){return r.json();}).then(function(d){ 801 if(!d||!d.ok){ $("#list").innerHTML='<p class="empty">'+((d&&d.error)?esc(d.error):"Could not load board members.")+'</p>'; loadUpdates(); loadMinutes(); announceRsvpBootstrap(null); return; } 802 MEM=d.members||[]; ROT=d.rotation||ROT; render(); 803 if(d.bootstrap){ 804 applyUpdates(d.bootstrap.updatesError 805 ? {ok:false,error:d.bootstrap.updatesError} 806 : {ok:true,updates:d.bootstrap.updates||[]}); 807 applyMinutes(d.bootstrap.minutesError 808 ? {ok:false,error:d.bootstrap.minutesError} 809 : {ok:true,minutes:d.bootstrap.minutes||[],unmatched:d.bootstrap.unmatched||[]}); 810 announceRsvpBootstrap(d.bootstrap.rsvps||null); 811 }else{ 812 // Safe rolling-deploy fallback if the HTML reaches a browser before the 813 // matching function bundle does. 814 loadUpdates(); loadMinutes(); announceRsvpBootstrap(null); 815 } 816 }).catch(function(){ $("#list").innerHTML='<p class="empty">Could not load (functions only run on the deployed site).</p>'; loadUpdates(); loadMinutes(); announceRsvpBootstrap(null); }); 817 } 818 819 // ---- list interactions ---- 820 $("#list").addEventListener("click",function(e){ 821 var btn=e.target.closest("[data-act]"); if(!btn) return; 822 var id=btn.getAttribute("data-id"), act=btn.getAttribute("data-act"); 823 if(act==="biomore"){ var bx=btn.previousElementSibling; var clamped=bx.classList.toggle("clamp"); btn.textContent=clamped?"Show more":"Show less"; return; } 824 var m=mget(id); if(!m) return; 825 if(act==="pick"){ if(btn.checked) SEL[id]=true; else delete SEL[id]; var c=btn.closest(".mcard"); if(c)c.classList.toggle("sel",!!SEL[id]); refreshSel(); return; } 826 if(act==="email"){ window.location.href="mailto:"+m.email; return; } 827 if(act==="sendprofile"){ 828 btn.disabled=true; btn.textContent="Sending..."; 829 post({action:"sendProfileLink",id:id}).then(function(d){ 830 btn.disabled=false; btn.textContent="Send profile link"; 831 if(d&&d.ok){ m.profileEmailAt=d.at||Date.now(); m.hasLink=true; toast("Profile link emailed to "+(m.name||d.to)); render(); } 832 else toast((d&&d.error)||"Could not send."); 833 }).catch(function(){ btn.disabled=false; btn.textContent="Send profile link"; toast("Could not send."); }); 834 return; 835 } 836 // Reset: the old link stops working and the server emails the new one straight to the member. The link itself never 837 // comes back to this page. An archived member's reset only turns the old link off (that is how a departed member is cut off). 838 if(act==="resetlink"){ 839 var who=m.name||"this member"; 840 var ask = m.removed ? ("Turn off "+who+"'s dashboard link? Their current link stops working. They are archived, so no new link is sent.") 841 : !m.email ? ("Reset "+who+"'s dashboard link? Their current link stops working. There is no email on file, so no new link can be sent until you add one with Edit.") 842 : ("Reset "+who+"'s dashboard link? Their current link stops working, and the new link is emailed to them at "+m.email+"."); 843 if(!confirm(ask)) return; 844 btn.disabled=true; 845 post({action:"resetToken",id:id}).then(function(d){ 846 btn.disabled=false; 847 if(d&&d.ok){ 848 m.hasLink=true; 849 if(d.emailed){ m.profileEmailAt=d.at||Date.now(); toast("New link emailed to "+(d.to||m.email)+". The old link no longer works."); } 850 else toast("Link reset. The old link no longer works. "+(d.reason||"")); 851 render(); 852 } else toast((d&&d.error)?d.error:"Could not reset."); 853 }).catch(function(){ btn.disabled=false; toast("Could not reset."); }); return; } 854 if(act==="profile"){ openProfile(m,btn); return; } 855 if(act==="edit"){ openEdit(m); return; } 856 if(act==="archive"){ if(!confirm('Archive "'+m.name+'"? They\'ll be hidden from the active roster and the approval rotation, but kept on file. Restore them anytime with "Show archived".')) return; 857 post({action:"archive",id:id}).then(function(d){ if(d&&d.ok){ m.removed=true; delete SEL[id]; render(); var n=rosterNote(d); if(n) toast("Archived. "+n,9000); } }); return; } 858 if(act==="unarchive"){ post({action:"unarchive",id:id}).then(function(d){ if(d&&d.ok){ m.removed=false; render(); var n=rosterNote(d); if(n) toast("Restored. "+n,9000); } }); return; } 859 }); 860 861 $("#selAll").addEventListener("change",function(){ var on=this.checked;
861 visible().forEach(function(m){ if(m.removed) return; if(on)SEL[m.id]=true; else delete SEL[m.id]; }); render(); }); 862 ["q","showArchived"].forEach(function(id){ var el=document.getElementById(id); el.addEventListener(el.type==="search"?"input":"change",render); }); 863 864 function groupMailto(members, withSubjectBody){ 865 if(!members.length) return; 866 var to=members.map(function(m){return encodeURIComponent(m.email);}).filter(Boolean).join(","); 867 var href="mailto:"+to; if(withSubjectBody) href+="?subject="+encodeURIComponent("RRCC Board"); 868 window.location.href=href; 869 } 870 $("#emailSel").addEventListener("click",function(){ var s=selectedMembers(); if(!s.length) return; groupMailto(s,false); }); 871 $("#linkSel").addEventListener("click",function(){ 872 var s=selectedMembers().filter(function(m){return !!m.email;}); if(!s.length){ toast("No selected members with an email on file."); return; } 873 if(!confirm("Email "+s.length+" member"+(s.length===1?"":"s")+" their personal dashboard link?")) return; 874 var okN=0, chain=Promise.resolve(); 875 s.forEach(function(m){ chain=chain.then(function(){ return post({action:"sendProfileLink",id:m.id}).then(function(d){ if(d&&d.ok){ okN++; m.profileEmailAt=d.at||Date.now(); } }).catch(function(){}); }); }); 876 chain.then(function(){ toast("Sent "+okN+" of "+s.length+" profile link emails."); render(); }); 877 }); 878 879 // ---- profile: a staff-side view of the member ---- 880 // What RRCC holds about the member and the applications handed to them. It deliberately does NOT open the member's own 881 // dashboard: that needs their private link, and a link staff can open is a link staff can use as that member. 882 var profileOv=$("#profileOv"), profileOpener=null; 883 function shortDate(t){ try{ return new Date(t).toLocaleDateString(undefined,{month:"short",day:"numeric",year:"numeric"}); }catch(e){ return ""; } } 884 function openProfile(m,opener){ 885 profileOpener=opener||null; 886 var ava = m.hasPhoto ? ('<span class="pf-ava"><img src="'+esc(photoUrl(m))+'" alt=""/></span>') : ('<span class="pf-ava">'+esc(initials(m.name).toUpperCase())+'</span>'); 887 var meta=[]; 888 if(m.email) meta.push('<a href="mailto:'+esc(m.email)+'">'+esc(m.email)+'</a>'); else meta.push("No email on file"); 889 if(m.phone) meta.push(esc(m.phone)); 890 if(m.title) meta.push(esc(m.title)); 891 var link; 892 if(m.removed) link="Archived. Reset link turns their old link off without sending a new one."; 893 else if(m.hasLink===true) link="They have a private dashboard link."+(m.profileEmailAt?(" Last emailed "+shortDate(m.profileEmailAt)+"."):" It has not been emailed from this page yet."); 894 else if(m.hasLink===false) link="No dashboard link yet. Send profile link makes one and emails it to them."; 895 else link="Their link could not be checked right now."; 896 var apps=Array.isArray(m.reviewApps)?m.reviewApps:[]; 897 var appHtml = apps.length ? apps.map(function(a){ 898 var score=(a.finalScore!=null)?'<span class="pf-score">'+esc(a.finalScore)+'/20</span>':""; 899 var pill=a.approved?'<span class="pf-status approved">Approved</span>':'<span class="pf-status pending">'+esc(a.status||"Waiting on them")+'</span>'; 900 var stamp=(a.approved&&a.boardApprovedAt)?('<div class="pf-meta">Approved '+esc(shortDate(a.boardApprovedAt))+(a.boardApprovedBy?(" by "+esc(a.boardApprovedBy)):"")+'</div>'):""; 901 var note=a.boardNote?('<div class="pf-note">'+esc(a.boardNote)+'</div>'):""; 902 return '<div class="pf-app"><div class="pf-app-top"><span><span class="pf-org">'+esc(a.org||"Application")+'</span>'+score+'</span>'+pill+'</div>'+stamp+note+ 903 '<a class="pf-link" href="app-review.html?q='+encodeURIComponent(a.org||"")+'">Open in Application Review</a></div>'; 904 }).join("") : '<div class="pf-empty">Nothing assigned to them yet.</div>'; 905 $("#pfBody").innerHTML= 906 '<div class="pf-head">'+ava+'<div><h3 class="pf-name" id="pfTitle">'+esc(m.name||"(no name)")+'</h3>'+ 907 '<div class="pf-pos">'+esc(m.position||"Board of Directors")+' · Term '+esc(computeTerm(m))+'</div></div></div>'+ 908 '<div class="pf-meta">'+meta.join(" ·
908 ")+'</div>'+ 909 (m.bio?'<div class="pf-bio">'+esc(m.bio)+'</div>':"")+ 910 '<div class="pf-sech">Board dashboard link</div>'+ 911 '<div class="pf-meta">'+esc(link)+'</div>'+ 912 '<div class="pf-meta">Only the member opens their dashboard. Send profile link and Reset link email it to them; the link is never shown here.</div>'+ 913 '<div class="pf-sech">Applications assigned to them</div>'+appHtml; 914 profileOv.hidden=false; 915 $("#pfClose").focus(); 916 } 917 function closeProfile(){ profileOv.hidden=true; if(profileOpener&&profileOpener.focus) profileOpener.focus(); profileOpener=null; } 918 $("#pfClose").addEventListener("click",closeProfile); 919 profileOv.addEventListener("click",function(e){ if(e.target===profileOv) closeProfile(); }); 920 document.addEventListener("keydown",function(e){ if(e.key==="Escape"&&!profileOv.hidden) closeProfile(); }); 921 922 // ---- edit / add modal ---- 923 var editOv=$("#editOv"); 924 function openEdit(m){ editingId=m?m.id:null; 925 $("#editTitle").textContent=m?("Edit "+(m.name||"member")):"Add board member"; 926 $("#f_name").value=m?(m.name||""):""; $("#f_position").value=m?(m.position||"Board of Directors"):"Board of Directors"; 927 $("#f_term").value=m?computeTerm(m):""; $("#f_start").value=m?(m.start||""):""; $("#f_end").value=m?(m.end||""):""; $("#f_email").value=m?(m.email||""):""; 928 $("#f_email_hint").hidden=!m; // only when editing someone: a new director has no sign-in address yet 929 editOv.hidden=false; $("#f_name").focus(); 930 } 931 function closeEdit(){ editOv.hidden=true; editingId=null; } 932 $("#editCancel").addEventListener("click",closeEdit); 933 editOv.addEventListener("click",function(e){ if(e.target===editOv) closeEdit(); }); 934 document.addEventListener("keydown",function(e){ if(e.key==="Escape"&&!editOv.hidden) closeEdit(); }); 935 $("#addBtn").addEventListener("click",function(){ openEdit(null); }); 936 $("#editSave").addEventListener("click",function(){ 937 var payload={ action: editingId?"edit":"add", name:$("#f_name").value.trim(), position:$("#f_position").value, start:$("#f_start").value.trim(), end:$("#f_end").value.trim(), email:$("#f_email").value.trim() }; 938 if(editingId) payload.id=editingId; 939 if(!payload.name){ toast("Name is required."); return; } 940 post(payload).then(function(d){ if(d&&d.ok&&d.member){ closeEdit(); 941 var i=MEM.findIndex(function(x){return x.id===d.member.id;}); if(i>=0) MEM[i]=Object.assign(MEM[i],d.member); else MEM.push(d.member); 942 render(); var n=rosterNote(d); toast(n?("Saved. "+n):"Saved.", n?9000:0); } else toast((d&&d.error)?d.error:"Could not save."); }); 943 }); 944 945 var fb=$("#featBtn"); if(fb) fb.addEventListener("click",function(){ var msg=prompt("Describe the feature or change you'd like:"); if(msg){ fetch(FN+"/feature-request",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({staff:"Board Members",message:msg})}).then(function(){alert("Thanks! Sent.");}).catch(function(){}); } }); 946 947 wireUpdates(); 948 wireMinutes(); 949 load(); 950 })(); 951 952 /* ---- Meeting RSVPs ------------------------------------------------------- 953 One public link (board-rsvp.html) that every member opens; they pick their 954 own name. No token per person, which is what was asked for, so anyone with 955 the link could answer as anyone. Fine for a meeting headcount, and every 956 change is written to the board log with what it replaced. */ 957 (function(){ 958 var BOX=document.getElementById("rsvpBox"); 959 if(!BOX) return; 960 var LABEL={yes:"Yes",no:"No",maybe:"Not sure"}; 961 // Own copy: the page's esc() lives in another closure and is not reachable here. 962 function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){ return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]; }); } 963 964 function renderData(d){ 965 if(!d||!d.ok){ 966 BOX.hidden=false; 967 document.getElementById("rsvpTitle").textContent="Meeting RSVPs"; 968 document.getElementById("rsvpWhen").textContent=(d&&d.error)||"The RSVP list is unavailable right now."; 969 document.getElementById("rsvpCounts").innerHTML=""; 970 document.getElementById("rsvpList").innerHTML=""; 971 return; 972 } 973 BOX.hidden=false; 974 var m=d.meeting; 975 document.getElementById("rsvpTitle").textContent = m&&m.title ? m.title : "Meeting RSVPs"; 976 document.getElementById("rsvpWhen").textContent = m
977 ? [m.date, m.time, m.place].filter(Boolean).join(" · ") 978 + (m.from?(" (moved from "+m.from+")"):"") 979 + (d.meetingSet===false?" [default, not saved yet]":"") 980 : "No meeting set yet. Use Set the meeting to add one."; 981 var rows=d.rows||[]; 982 var c={yes:0,no:0,maybe:0,none:0}; 983 rows.forEach(function(r){ c[r.answer||"none"]=(c[r.answer||"none"]||0)+1; }); 984 document.getElementById("rsvpCounts").innerHTML= 985 '<span class="rsvpchip yes">Yes '+c.yes+'</span>' 986 + '<span class="rsvpchip no">No '+c.no+'</span>' 987 + '<span class="rsvpchip maybe">Not sure '+c.maybe+'</span>' 988 + '<span class="rsvpchip">No reply '+c.none+'</span>'; 989 document.getElementById("rsvpList").innerHTML = rows.map(function(r){ 990 var a=r.answer||"none"; 991 var how = r.answer==="yes" ? (r.how==="virtual"?" · virtual":" · in person") : ""; 992 return '<div class="rsvprow"><div><b>'+esc(r.name)+'</b>' 993 + (r.note?'<div class="rsvpnote">'+esc(r.note)+'</div>':'') 994 + '</div><span class="rsvptag '+a+'">'+(LABEL[a]||"No reply")+esc(how)+'</span></div>'; 995 }).join(""); 996 } 997 998 var fallbackStarted=false; 999 function load(){ 1000 fallbackStarted=true; 1001 fetch("/.netlify/functions/board-members?rsvps=1") 1002 .then(function(r){ return r.json(); }) 1003 .then(renderData).catch(function(){}); 1004 } 1005 1006 window.addEventListener("rrcc:board-rsvp-bootstrap",function(e){ 1007 if(e.detail){ renderData(e.detail); } 1008 else if(!fallbackStarted) load(); 1009 },{once:true}); 1010 // If the main request stalls rather than failing, the RSVP panel still gets a 1011 // bounded independent fallback instead of sitting blank indefinitely. 1012 setTimeout(function(){ if(BOX.hidden&&!fallbackStarted) load(); },6000); 1013 1014 document.getElementById("rsvpCopy").onclick=function(){ 1015 var url=location.origin+"/board-rsvp.html"; 1016 if(navigator.clipboard) navigator.clipboard.writeText(url); 1017 this.textContent="Link copied"; 1018 var b=this; setTimeout(function(){ b.textContent="Copy RSVP link"; },1800); 1019 }; 1020 1021 document.getElementById("rsvpSet").onclick=function(){ 1022 var date=prompt("Meeting date, as you want it to read:","Tuesday, August 25, 2026"); 1023 if(date===null) return; 1024 var time=prompt("Time:","5:30 PM"); 1025 if(time===null) return; 1026 var place=prompt("Where:","In person, our complex conference room"); 1027 if(place===null) return; 1028 var from=prompt("If this replaces an earlier date, what was it? (blank if not a reschedule)","Tuesday, August 4, 2026"); 1029 if(from===null) return; 1030 var why=from?prompt("Why did it move? (one line, shown on the RSVP page)","Weaver needs about another week and a half to finish the audit."):""; 1031 if(why===null) return; 1032 var note=prompt("Anything else to tell them? (optional)","Weaver will present the completed audit at this meeting."); 1033 if(note===null) return; 1034 // MUST be asked. setMeeting rebuilds the whole record, so omitting this field 1035 // here would silently WIPE the join link every time somebody pressed Save. 1036 // Shown only on each member's own dashboard, never on the public RSVP page. 1037 var zoom=prompt("Video link to join (https://... , blank for none). Only board members see this, not the public RSVP page:","https://us06web.zoom.us/j/89739185621"); 1038 if(zoom===null) return; 1039 if(!confirm("Save this meeting?\n\n"+date+" at "+time+"\n"+place 1040 +(from?("\nMoved from "+from):"") 1041 +"\n\nThis clears any answers already given, so they are not read as replies to this meeting.")) return; 1042 fetch("/.netlify/functions/board-members",{method:"POST",headers:{"content-type":"application/json"}, 1043 body:JSON.stringify({action:"setMeeting",title:"RRCC Board Meeting",date:date,time:time,place:place,from:from,why:why,note:note,zoom:zoom})}) 1044 .then(function(r){ return r.json(); }) 1045 .then(function(d){ if(d&&d.ok) load(); else alert((d&&d.error)||"Could not save the meeting."); }) 1046 .catch(function(){ alert("Could not save the meeting."); }); 1047 }; 1048 1049 })(); 1050</script>
vendor: 1 bytes, line 1050
1050
1051<script> 1052 /* ---- Board items: the board room's staff panel ------------------------------------------------------------------ 1053 Staff prepare an item with two questions (the server picks the type and the rule and says it in one sentence), 1054 preview it as a director sees it on a phone and as the email reads, and open it. For the 15 minutes before the 1055 emails go it can be withdrawn without a reason; after that a newer document, the deadline extended once, a 1056 withdrawal with a reason and a comment as RRCC staff are each added to the record. Staff never vote, consent, 1057 sign, confirm the voters or record a result: the route has no such action. Every call goes to board-items, which 1058 auth.js signs with the dashboard session; nothing here ever handles a board member's link. */ 1059 (function(){ 1060 var BOX=document.getElementById("biBox"); 1061 if(!BOX) return; 1062 var FN="/.netlify/functions/board-items"; 1063 var TYPES={"application/pdf":1,"application/vnd.openxmlformats-officedocument.wordprocessingml.document":1,"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":1,"image/png":1,"image/jpeg":1}; 1064 var MAX_BYTES=4*1024*1024, MAX_FILES=10; 1065 var ITEMS=[], PREP={need:"",about:"",docs:[],preview:null,needs:null}, CUR=null, ADDFILE=null, ITEM_EPOCH=0, FILE_EPOCH=0; 1066 function $(id){ return document.getElementById(id); } 1067 function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){ return {"&":"&","<":"<",">":">","\"":""","'":"'"}[c]; }); } 1068 function post(body){ 1069 return fetch(FN,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body||{}),cache:"no-store"}) 1070 .then(function(r){ return Promise.resolve(r.json()).catch(function(){ return null; }); }) 1071 .then(function(d){ return d&&typeof d==="object"?d:{ok:false,error:"The answer could not be read. Look again before trying once more."}; }) 1072 .catch(function(){ return {ok:false,error:"The board items could not be reached. Nothing was changed. Try again in a minute."}; }); 1073 } 1074 function msg(id,kind,text){ 1075 var el=$(id); if(!el) return; 1076 el.innerHTML=text?('<div class="bi-msg bi-'+kind+'" role="'+(kind==="bad"?"alert":"status")+'">'+esc(text)+'</div>'):""; 1077 } 1078 function lines(arr){ return arr.filter(Boolean).map(function(t){ return '<p>'+esc(t)+'</p>'; }).join(""); } 1079 1080 // ---- the list ---- 1081 function load(){ 1082 return post({action:"list"}).then(function(d){ 1083 if(!d.ok){ $("biList").innerHTML='<div class="rsvpwhen">'+esc(d.error||"Could not load the board items.")+'</div>'; return; } 1084 ITEMS=Array.isArray(d.items)?d.items:[]; 1085 var list=ITEMS.slice().sort(function(a,b){ return String(b.id).localeCompare(String(a.id)); }); 1086 var h=list.length?list.map(rowHtml).join(""):'<div class="rsvpwhen">No board items yet. Prepare one to put a document, a question or a decision before the board.</div>'; 1087 if(d.unreadable) h+='<div class="rsvpwhen">Some items could not be read just now, so this list may be missing them. Try again in a minute.</div>'; 1088 $("biList").innerHTML=h; 1089 }); 1090 } 1091 var TAG={open:"Open",pending:"Not open yet",closed:"Closed",withdrawn:"Withdrawn"}; 1092 function rowHtml(x){ 1093 var sub=[x.kind,x.due,x.progress,x.status==="closed"&&x.outcome?("Result: "+x.outcome):""].filter(Boolean).join(" · "); 1094 var acts=(x.canWithdrawEarly?'<button class="btn sm" type="button" data-bi-early="'+esc(x.id)+'">Withdraw before anyone is emailed</button>':"")+ 1095 '<button class="btn sm" type="button" data-bi-open="'+esc(x.id)+'">Open</button>'; 1096 return '<div class="updrow bi-row" data-id="'+esc(x.id)+'"><div class="updrow-main">'+ 1097 '<div class="updrow-t"><span class="bi-id">'+esc(x.id)+'</span> '+esc(x.title)+' <span class="updtag'+(x.status==="open"?"":" draft")+'">'+esc(TAG[x.status]||x.status)+'</span></div>'+ 1098 '<div class="rsvpwhen">'+esc(sub)+'</div>'+ 1099 (x.status==="closed"||x.status==="withdrawn"?'<div class="rsvpwhen">'+esc(x.statusWords)+'.</div>':"")+ 1100 (x.canWithdrawEarly?'<div class="rsvpwhen">Emails go at '+esc(x.emailAtWords)+'. Until then you can withdraw it without a reason.</div>':"")+ 1101 '</div><div class="bi-rowacts">'+acts+'</div></div>'; 1102 } 1103 $("biList").addEventListener("click",function(e){ 1104 var b=e.target.closest("button"); if(!b) return; 1105 if(b.hasAttribute("data-bi-open")) return openItemDlg(b.getAttribute("data-bi-open")); 1106 if(b.hasAttribute("data-bi-early")) return withdrawEarly(b.getAttribute("data-bi-early"),b); 1107 }); 1108 function withdrawEarly(id,btn){ 1109 var epoch=ITEM_EPOCH; 1110 if(!confirm("Withdraw "+id+" before anyone is emailed? It stays in the record as withdrawn, and no director is emailed.")) return; 1111 if(btn) btn.disabled=true; 1112 post({action:"withdrawEarly",id:id}).then(function(d){ 1113 if(btn) btn.disabled=false; 1114 if(!d.ok){ msg("biMsg","bad",d.error||"That did not go through."); return; } 1115 msg("biMsg","ok",id+" is withdrawn. No director was emailed."); 1116 load(); 1117 if(CUR===id&&epoch===ITEM_EPOCH) refreshItem(); 1118 }); 1119 } 1120 1121 // ---- Prepare, Preview, Open ---- 1122 function radio(name){ var r=document.querySelector('#biPrepOv input[name="'+name+'"]:checked'); return r?r.value:""; } 1123 function fields(){ 1124 var f={need:radio("biNeed"),about:radio("biAbout"),title:$("biTitle").value.trim(),motion:$("biWords").value.trim(),docs:PREP.docs.map(function(d){ return {name:d.name,type:d.type,size:d.size,b64:d.b64}; })}; 1125 if(f.about==="budget"){ 1126 var pay=radio("biBudgetPay"); 1127 if(pay) f.budgetChangesEdPay=pay==="yes"; 1128 f.staffPayBasis=$("biStaffPayBasis").value.trim(); 1129 } 1130 if(PREP.ratifyItem) f.ratifyItem=PREP.ratifyItem; 1131 if($("biDeadline").value) f.deadlineLocal=$("biDeadline").value; 1132 if(!$("biMeetingFld").hidden&&$("biMeeting").value) f.meetingDate=$("biMeeting").value; 1133 if($("biHelp").value.trim()) f.help=$("biHelp").value.trim(); 1134 return f; 1135 } 1136 function stale(){ PREP.rev=(PREP.rev||0)+1; PREP.preview=null; PREP.previewFields=null; $("biPreview").innerHTML=""; $("biSure").hidden=true; $("biPrepActs").hidden=false; } 1137 function openPrep(){ 1138 PREP={need:"",about:"",docs:[],preview:null,needs:null}; 1139 document.querySelectorAll('#biPrepOv input[type="radio"]').forEach(function(r){ r.checked=false; }); 1140 ["biTitle","biWords","biDeadline","biMeeting","biHelp","biFiles","biStaffPayBasis"].forEach(function(id){ $(id).value=""; }); 1141 $("biBudgetFields").hidden=true; 1142 $("biRule").textContent="Answer both questions, and the rule for this item shows here in one sentence."; 1143 $("biWordsLabel").textContent="The exact words"; 1144 $("biDeadlineLabel").textContent="Deadline, Central time"; 1145 $("biMeetingFld").hidden=true; 1146 $("biFilesNow").textContent="PDF, Word, Excel, PNG or JPEG. Up to 10 files, 4 MB each."; 1147 $("biPreview").innerHTML=""; msg("biPrepMsg","",""); 1148 stale(); 1149 $("biPrepOv").hidden=false; 1150 var first=document.querySelector('#biPrepOv input[name="biNeed"]'); if(first) first.focus(); 1151 } 1152 function closePrep(){ stale(); $("biPrepOv").hidden=true; $("biSureOpen").disabled=false; } 1153 function ask(){ 1154 var need=radio("biNeed"), about=radio("biAbout"); 1155 $("biBudgetFields").hidden=about!=="budget"; 1156 if(!need||!about) return; 1157 var prep=PREP, rev=PREP.rev; 1158 post({action:"prepare",need:need,about:about}).then(function(d){ 1159 if(prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden) return; 1160 if(!d.ok){ PREP.needs=null; $("biRule").textContent=d.error||"That combination cannot be used."; return; } 1161 PREP.needs=d.needs||{}; 1162 $("biRule").textContent="The rule: "+d.sentence+" ("+d.kind+")"; 1163 $("biWordsLabel").textContent=PREP.needs.motion?"The exact words of the motion (required)":"The exact words"; 1164 $("biDeadlineLabel").textContent=PREP.needs.deadline?"Deadline, Central time (required)":"Deadline, Central time (optional)"; 1165 $("biMeetingFld").hidden=!PREP.needs.meetingDate; 1166 }); 1167 } 1168 function readFile(file){ 1169 return new Promise(function(resolve,reject){ 1170 var rd=new FileReader(); 1171 rd.onload=function(){ resolve({name:file.name,type:file.type,size:file.size,b64:String(rd.result).split(",")[1]||""}); }; 1172 rd.onerror=function(){ reject(new Error("read")); }; 1173 rd.readAsDataURL(file); 1174 }); 1175 } 1176 function fileProblem(f){ 1177 if(!TYPES[f.type]) return f.name+" is not a PDF, Word, Excel, PNG or JPEG file."; 1178 if(f.size>MAX_BYTES) return f.name+" is larger than 4 MB."; 1179 return ""; 1180 } 1181 $("biFiles").addEventListener("change",function(){ 1182 var list=Array.prototype.slice.call($("biFiles").files||[]); 1183 stale(); 1184 var prep=PREP, fileEpoch=++FILE_EPOCH; 1185 if(list.length>MAX_FILES){ PREP.docs=[]; $("biFilesNow").textContent="Attach at most 10 files."; return; }
1186 var bad=list.map(fileProblem).filter(Boolean); 1187 if(bad.length){ PREP.docs=[]; $("biFilesNow").textContent=bad[0]; return; } 1188 Promise.all(list.map(readFile)).then(function(docs){ 1189 if(prep!==PREP||fileEpoch!==FILE_EPOCH||$("biPrepOv").hidden) return; 1190 stale(); 1191 PREP.docs=docs; 1192 $("biFilesNow").textContent=docs.length?("Ready to attach: "+docs.map(function(d){ return d.name; }).join(", ")):"PDF, Word, Excel, PNG or JPEG. Up to 10 files, 4 MB each."; 1193 }).catch(function(){ if(prep!==PREP||fileEpoch!==FILE_EPOCH||$("biPrepOv").hidden) return; PREP.docs=[]; $("biFilesNow").textContent="A file could not be read. Choose it again."; }); 1194 }); 1195 $("biPrepOv").addEventListener("change",function(e){ 1196 if(e.target&&e.target.type==="radio"){ stale(); ask(); } 1197 }); 1198 $("biPrepOv").addEventListener("input",function(e){ if(e.target&&e.target.type!=="radio"&&e.target.type!=="file") stale(); }); 1199 function phoneHtml(it){ 1200 var me=it.me||{}, help=it.help||{}; 1201 var h='<div><p class="bi-cap">How a director sees it</p><div class="bi-phone">'; 1202 h+='<div class="bi-ph-head">'+lines(["Your part: "+(me.part||""),it.due,it.progress,it.ifNot])+'</div>'; 1203 h+=lines([it.id])+'<p class="bi-ph-title">'+esc(it.title)+'</p>'+lines([it.kind,it.rule&&it.rule.sentence]); 1204 if(it.motion) h+='<p><b>The exact words</b></p><div class="bi-ph-words">'+esc(it.motion)+'</div>'; 1205 if(it.docs&&it.docs.length) h+='<p><b>Documents</b></p>'+lines(it.docs.map(function(d){ return d.name; })); 1206 if(help.name) h+=lines(["Questions? Ask "+help.name+(help.role?", "+help.role:"")+"."]); 1207 return h+'</div></div>'; 1208 } 1209 function mailHtml(m){ 1210 return '<div class="bi-mail"><p class="bi-cap">The email each director gets</p><p><b>Subject:</b> '+esc(m.subject)+'</p><div class="bi-mailtext">'+esc(m.text)+'</div></div>'; 1211 } 1212 function preview(){ 1213 msg("biPrepMsg","",""); 1214 var prep=PREP, rev=PREP.rev, exact=fields(); 1215 return post({action:"preview",fields:exact}).then(function(d){ 1216 if(prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden||JSON.stringify(exact)!==JSON.stringify(fields())) return null; 1217 if(!d.ok){ PREP.preview=null; msg("biPrepMsg","bad",d.error||"The preview could not be made."); return null; } 1218 PREP.preview=d; 1219 PREP.previewFields=exact; 1220 $("biPreview").innerHTML='<div class="bi-preview">'+phoneHtml(d.item||{})+mailHtml(d.email||{})+'</div>'; 1221 return d; 1222 }); 1223 } 1224 $("biPreviewBtn").addEventListener("click",function(){ preview(); }); 1225 $("biOpenBtn").addEventListener("click",function(){ 1226 var prep=PREP, rev=PREP.rev; 1227 (PREP.preview?Promise.resolve(PREP.preview):preview()).then(function(d){ 1228 if(!d||prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden) return; 1229 $("biSureText").textContent=d.openDialog||"Once open, nothing in this item can be changed or deleted."; 1230 $("biPrepActs").hidden=true; 1231 $("biSure").hidden=false; 1232 $("biSureOpen").focus(); 1233 }); 1234 }); 1235 $("biSureBack").addEventListener("click",function(){ $("biSure").hidden=true; $("biPrepActs").hidden=false; }); 1236 $("biSureOpen").addEventListener("click",function(){ 1237 if(!PREP.preview||!PREP.previewFields||JSON.stringify(PREP.previewFields)!==JSON.stringify(fields())){ stale(); msg("biPrepMsg","bad","Preview the current words before opening this item."); return; } 1238 var prep=PREP, rev=PREP.rev; 1239 var btn=$("biSureOpen"); btn.disabled=true; 1240 post({action:"open",fields:PREP.previewFields}).then(function(d){ 1241 if(prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden){ load(); return; } 1242 btn.disabled=false; 1243 if(!d.ok){ $("biSure").hidden=true; $("biPrepActs").hidden=false; msg("biPrepMsg","bad",d.error||"The item did not open."); return; } 1244 closePrep(); 1245 msg("biMsg","ok","Opened "+d.id+". Emails go at "+d.emailAtWords+". Until then you can withdraw it without a reason."); 1246 load(); 1247 }); 1248 }); 1249 $("biPrepCancel").addEventListener("click",closePrep); 1250 $("biNew").addEventListener("click",openPrep); 1251
1252 // ---- the address check (the dry run) ---- 1253 $("biCheck").addEventListener("click",function(){ 1254 if(!confirm("Open one address-check item for every director in office? Each director uses a personal sign-in link and presses I have read this. It closes in 14 days.")) return; 1255 var btn=$("biCheck"); btn.disabled=true; 1256 post({action:"openAddressCheck"}).then(function(d){ 1257 btn.disabled=false; 1258 if(!d.ok){ msg("biMsg","bad",d.error||"The address check did not open."); return; } 1259 msg("biMsg","ok","The address check is open: "+d.id+". Emails go at "+d.emailAtWords+"."); 1260 load(); 1261 }); 1262 }); 1263 1264 // ---- one item ---- 1265 function itemPost(body){ 1266 var id=CUR, epoch=ITEM_EPOCH; 1267 return post(body).then(function(d){ return id!==CUR||epoch!==ITEM_EPOCH?{stale:true}:d; }); 1268 } 1269 function openItemDlg(id){ 1270 ITEM_EPOCH++; 1271 CUR=id; ADDFILE=null; 1272 $("biItemTitle").textContent=id; 1273 $("biItemBody").innerHTML='<div class="rsvpwhen">Loading.</div>'; 1274 $("biItemOv").hidden=false; 1275 refreshItem(); 1276 } 1277 function closeItemDlg(){ ITEM_EPOCH++; $("biItemOv").hidden=true; CUR=null; ADDFILE=null; } 1278 function refreshItem(note){ 1279 var id=CUR, epoch=ITEM_EPOCH; 1280 return itemPost({action:"item",id:id}).then(function(d){ if(d.stale) return; 1281 if(CUR!==id||epoch!==ITEM_EPOCH) return; 1282 if(!d.ok){ $("biItemBody").innerHTML='<div class="bi-msg bi-bad" role="alert">'+esc(d.error||"The item could not be read.")+'</div>'; return; } 1283 if(d.item) return renderItem(d.item,note); 1284 $("biItemBody").innerHTML='<div class="bi-facts">'+lines(["This item was marked as about the Executive Director, so RRCC staff see only where it stands.","Status: "+d.status+".","Next: "+d.nextStep])+'</div>'; 1285 }); 1286 } 1287 function commentHtml(c){ 1288 var corr=(c.corrections||[]).map(function(x){ return '<p class="bi-ctext"><b>Correction:</b> '+esc(x.text==null?"Hidden.":x.text)+'</p>'; }).join(""); 1289 var who='<b>'+esc(c.who)+'</b> · '+esc(new Date(c.at).toLocaleString("en-US",{timeZone:"America/Chicago",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}))+' Central'; 1290 if(c.withdrawn) return '<div class="bi-comment"><details><summary>'+esc(c.withdrawn)+'</summary><p>'+who+'</p><p class="bi-ctext">'+esc(c.text==null?"Hidden.":c.text)+'</p>'+corr+'</details></div>'; 1291 return '<div class="bi-comment"><p>'+who+'</p><p class="bi-ctext">'+esc(c.text==null?"This comment was hidden by the Board Chair and the Secretary.":c.text)+'</p>'+corr+'</div>'; 1292 } 1293 function renderItem(it,note){ 1294 var can=it.can||{}, h=""; 1295 $("biItemTitle").textContent=it.id+": "+it.title; 1296 h+='<div id="biItemMsg">'+(note?'<div class="bi-msg bi-ok" role="status">'+esc(note)+'</div>':"")+'</div>'; 1297 h+='<div class="bi-facts">'+lines([it.statusWords+".",it.kind+".",it.rule&&it.rule.sentence,it.due+".",it.progress,it.status==="closed"&&it.outcome?("Result: "+it.outcome+"."):"","Opened by "+it.openedBy+".",it.help&&it.help.name?("Help contact: "+it.help.name+(it.help.role?", "+it.help.role:"")+"."):"",it.canWithdrawEarly?("Emails go at "+it.emailAtWords+"."):""])+'</div>'; 1298 if(it.motion) h+='<h4>The exact words</h4><div class="bi-mailtext">'+esc(it.motion)+'</div>'; 1299 var docs=it.docs||[]; 1300 if(docs.length) h+='<h4>Documents</h4><ul class="bi-docs">'+docs.map(function(d){ return '<li>'+esc(d.name)+' (version '+esc(d.version)+(d.active?"":", replaced by a newer version")+')'+(d.whatChanged?'. What changed: '+esc(d.whatChanged):"")+'</li>'; }).join("")+'</ul>'; 1301 h+='<h4>Discussion</h4>'+((it.comments||[]).length?(it.comments||[]).map(commentHtml).join(""):'<p class="rsvpwhen">No comments yet.</p>'); 1302 var t=it.votes&&it.votes.totals; 1303 if(t&&(it.rule&&["majority","straw","uwc","ecWriting"].indexOf(it.rule.kind)>=0)) h+='<p class="rsvpwhen">Answers so far: Yes '+t.yes+', No '+t.no+', Abstain '+t.abstain+', not answered '+t.notVoted+(t.steppedOut?(", stepped out "+t.steppedOut):"")+'.</p>'; 1304 if(it.canWithdrawEarly) h+='<div class="modal-actions"><button class="btn" type="button" data-bi-act="early">Withdraw before anyone is emailed</button></div>'; 1305 if(can.comment) h+='<h4>Comment as RRCC staff</h4><textarea id="biComment" maxlength="4000" placeholder="Directors see this as a comment from RRCC staff."></textarea><div class="modal-actions"><button class="btn primary" type="button" data-bi-act="comment">Comment as RRCC staff</button></div>'; 1306 if(can.addDoc){ 1307 var active=docs.filter(function(d){ return d.active; }); 1308 h+='<h4>Add a newer document</h4><div class="fld"><label for="biAddFile">The newer file</label><input id="biAddFile" type="file" accept=".pdf,.docx,.xlsx,.png,.jpg,.jpeg"></div>'+ 1309 (active.length?'<div class="fld"><label for="biSupersedes">It replaces</label><select id="biSupersedes"><option value="">Nothing, it is an extra document</option>'+active.map(function(d){ return '<option value="'+esc(d.sha256)+'">'+esc(d.name)+'</option>'; }).join("")+'</select></div>':"")+ 1310 '<div class="fld"><label for="biWhatChanged">What changed</label><input id="biWhatChanged" type="text" maxlength="300" placeholder="One line directors read before they open it"></div>'+ 1311 '<div class="modal-actions"><button class="btn" type="button" data-bi-act="addDoc">Add a newer document</button></div>'; 1312 } 1313 if(can.extend) h+='<h4>Extend the deadline</h4><p class="rsvpwhen">You can extend it once. Directors are told the new deadline.</p><div class="fld"><label for="biExtendTo">New deadline, Central time</label><input id="biExtendTo" type="datetime-local"></div><div class="fld"><label for="biExtendWhy">Why</label><input id="biExtendWhy" type="text" maxlength="300"></div><div class="modal-actions"><button class="btn" type="button" data-bi-act="extend">Extend the deadline</button></div>'; 1314 if(can.withdraw&&!it.canWithdrawEarly) h+='<h4>Withdraw it</h4><div class="fld"><label for="biWithdrawWhy">Why it is withdrawn</label><input id="biWithdrawWhy" type="text" maxlength="300"></div><div class="modal-actions"><button class="btn" type="button" data-bi-act="withdraw">Withdraw it</button></div>'; 1315 if(can.draftMeetingResult&&window.RRCC_BOARD_DECISIONS) h+='<h4>Meeting results</h4>'+window.RRCC_BOARD_DECISIONS.meetingForm(it.meetingMembers,true); 1316 if((it.meetingDrafts||[]).length) h+='<p class="rsvpwhen">'+it.meetingDrafts.length+' meeting draft'+(it.meetingDrafts.length===1?'':'s')+' saved for an officer. A draft is not a board action.</p>'; 1317 if(can.offerRatification) h+='<div class="bi-msg bi-warn"><p>No quorum was recorded. This is not a board action. You can prepare a new majority vote to ratify the same motion. It will still need Preview and Open.</p><button class="btn" type="button" data-bi-act="ratify">Prepare ratification vote</button></div>'; 1318 h+='<h4>The record</h4><div class="modal-actions"><button class="btn" type="button" data-bi-act="record">Download the record</button><button class="btn" type="button" data-bi-act="verify">Confirm nothing was changed</button></div><div id="biCheckMsg"></div>'; 1319 var rec=it.record||[]; 1320 if(rec.length) h+='<details class="bi-every"><summary>Every entry ('+rec.length+')</summary><ol class="bi-entries">'+rec.map(function(e){ return '<li>
1320'+esc(e.who)+', '+esc(new Date(e.at).toLocaleString("en-US",{timeZone:"America/Chicago",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}))+' Central: '+esc(e.line)+(e.counted?"":" (not counted)")+'. Proof code '+esc(e.proof)+'</li>'; }).join("")+'</ol></details>'; 1321 $("biItemBody").innerHTML=h; 1322 var resultForm=$("biItemBody").querySelector('[data-decision-meeting="staff"]'); 1323 if(resultForm) resultForm.addEventListener("submit",function(e){ 1324 e.preventDefault(); 1325 var fields; 1326 try{ fields=window.RRCC_BOARD_DECISIONS.meetingFields(resultForm); } 1327 catch(err){ msg("biItemMsg","bad",err.message); return; } 1328 var button=resultForm.querySelector('button[type="submit"]'); button.disabled=true; 1329 itemPost(Object.assign({action:"draftMeetingResult",id:it.id},fields)).then(function(d){ if(d.stale) return; 1330 button.disabled=false; 1331 if(CUR!==it.id) return; 1332 done(d,"The draft is saved for the Secretary or Chair to check and record."); 1333 }); 1334 }); 1335 $("biItemBody").querySelectorAll('[data-attendance]').forEach(function(select){ select.addEventListener('change',function(){ var vote=select.closest('[data-decision-member]').querySelector('[data-vote]'); vote.disabled=select.value!=='present'; if(vote.disabled) vote.value=''; }); }); 1336 var ratify=$("biItemBody").querySelector('[data-bi-act="ratify"]'); 1337 if(ratify) ratify.addEventListener('click',function(){ 1338 closeItemDlg(); openPrep(); PREP.ratifyItem=it.id; 1339 document.querySelector('[name="biNeed"][value="vote"]').checked=true; 1340 document.querySelector('[name="biAbout"][value="'+(["budget","policy","minutes","bylaws"].indexOf(it.type)>=0?it.type:"other")+'"]').checked=true; 1341 $("biTitle").value="Ratify: "+it.title; $("biWords").value=it.motion; ask(); 1342 }); 1343 ADDFILE=null; 1344 var f=$("biAddFile"); 1345 if(f) f.addEventListener("change",function(){ 1346 var file=f.files&&f.files[0]; ADDFILE=null; 1347 var epoch=ITEM_EPOCH, fileEpoch=++FILE_EPOCH; 1348 if(!file) return; 1349 var bad=fileProblem(file); 1350 if(bad){ msg("biItemMsg","bad",bad); f.value=""; return; } 1351 readFile(file).then(function(d){ if(d.stale) return; if(epoch===ITEM_EPOCH&&fileEpoch===FILE_EPOCH&&f.isConnected) ADDFILE=d; }).catch(function(){ if(epoch===ITEM_EPOCH&&fileEpoch===FILE_EPOCH&&f.isConnected) msg("biItemMsg","bad","That file could not be read. Choose it again."); }); 1352 }); 1353 } 1354 function done(d,note){ 1355 if(!d.ok){ msg("biItemMsg","bad",d.error||"That did not go through."); return; } 1356 if(d.item) renderItem(d.item,note); else refreshItem(note); 1357 load(); 1358 } 1359 $("biItemBody").addEventListener("click",function(e){ 1360 var b=e.target.closest("button[data-bi-act]"); if(!b||!CUR) return; 1361 var a=b.getAttribute("data-bi-act"), id=CUR; 1362 if(a==="early") return withdrawEarly(id,b); 1363 if(a==="comment"){ 1364 var text=$("biComment").value.trim(); 1365 if(!text){ msg("biItemMsg","bad","Write the comment first."); return; } 1366 b.disabled=true; 1367 return itemPost({action:"comment",id:id,text:text}).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"Your comment is posted as RRCC staff."); }); 1368 } 1369 if(a==="addDoc"){ 1370 if(!ADDFILE){ msg("biItemMsg","bad","Choose the newer file first."); return; } 1371 var body={action:"addDoc",id:id,doc:ADDFILE}; 1372 var sup=$("biSupersedes"); if(sup&&sup.value) body.supersedes=sup.value; 1373 var wc=$("biWhatChanged").value.trim(); if(wc) body.whatChanged=wc; 1374 b.disabled=true; 1375 return itemPost(body).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"The newer document is added."); }); 1376 } 1377 if(a==="extend"){ 1378 var to=$("biExtendTo").value, why=$("biExtendWhy").value.trim(); 1379 if(!to){ msg("biItemMsg","bad","Choose the new deadline."); return; } 1380 if(!why){ msg("biItemMsg","bad","Say why the deadline moves."); return; } 1381 b.disabled=true; 1382 return itemPost({action:"extend",id:id,deadlineLocal:to,reason:why}).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"The deadline is extended."); }); 1383 } 1384 if(a==="withdraw"){ 1385 var r=$("biWithdrawWhy").value.trim(); 1386 if(!r){ msg("biItemMsg","bad","Say why it is withdrawn."); return; } 1387 if(!confirm("Withdraw "+id+"? Directors can no longer act on it. The item and everything in it stay in the record.")) return; 1388 b.disabled=true; 1389 return itemPost({action:"withdraw",id:id,reason:r}).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"The item is withdrawn."); }); 1390 } 1391 if(a==="record"){ 1392 b.disabled=true; 1393 return itemPost({action:"record",id:id}).then(function(d){ if(d.stale) return; 1394 b.disabled=false; 1395 if(!d.ok||!d.b64){ msg("biCheckMsg","bad",d.error||"The record could not be made."); return; } 1396 var bin=atob(d.b64), u8=new Uint8Array(bin.length); 1397 for(var i=0;i<bin.length;i++) u8[i]=bin.charCodeAt(i); 1398 var a2=document.createElement("a"); 1399 a2.href=URL.createObjectURL(new Blob([u8],{type:"application/pdf"})); 1400 a2.download=d.fileName||("rrcc-board-record-"+id+".pdf"); 1401 document.body.appendChild(a2); a2.click(); 1402 setTimeout(function(){ try{ URL.revokeObjectURL(a2.href); }catch(_){ } a2.remove(); },1000); 1403 msg("biCheckMsg","ok","The record downloaded. Every page carries check code "+d.checkCode+"."); 1404 }); 1405 } 1406 if(a==="verify"){ 1407 b.disabled=true; 1408 return itemPost({action:"verify",id:id}).then(function(d){ if(d.stale) return; 1409 b.disabled=false; 1410 if(!d.ok||!d.check){ msg("biCheckMsg","bad",d.error||"The check could not be done just now."); return; } 1411 msg("biCheckMsg",d.check.ok?"ok":"bad",d.check.words); 1412 }); 1413 } 1414 }); 1415 $("biItemClose").addEventListener("click",closeItemDlg); 1416 ["biPrepOv","biItemOv"].forEach(function(id){ 1417 $(id).addEventListener("click",function(e){ if(e.target===$(id)){ if(id==="biPrepOv") closePrep(); else closeItemDlg(); } }); 1418 }); 1419 document.addEventListener("keydown",function(e){ 1420 if(e.key!=="Escape") return; 1421 if(!$("biItemOv").hidden) closeItemDlg(); 1422 else if(!$("biPrepOv").hidden) closePrep(); 1423 }); 1424 load(); 1425 })(); 1426</script>
1426 1427</body> 1428</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.