/* Divine Mercy Family of Parishes: request form behavior */ (function(){ "use strict"; /* ================================================================ EDIT ME. Everything you may ever need to change lives here. ================================================================ */ var CONFIG = { // Paste your Google Apps Script Web App URL between the quotes. // Setup steps are in SETUP.md. endpoint: "https://script.google.com/macros/s/AKfycby_d8VM44-j_CjIkgQdyspxUlbf7Qv2Jjydghc4h2IRubZyHNbEaiOV3QAOUbDaVTyIZQ/exec", // Who receives what. Change an address here and the form follows. routing: { calendar: { "St Joseph": { name: "Tracy", email: "twagner@divinemercyfam.org" }, "Visitation": { name: "Tracy", email: "twagner@divinemercyfam.org" }, "St Jude": { name: "Tracy", email: "twagner@divinemercyfam.org" } }, event: { name: "Deb Wells, Director of Communications", email: "dwells@divinemercyfam.org" } }, // Bookable spaces by campus. spaces: { "St Joseph": [ "Church","Gathering Space","St. Ignatius","Seton","St. Cecilia", "St. Agatha","St. Francis","Hospitality","Outdoor Garden Shelter" ], "Visitation": [ "Welcome Center","Church","Church Hall","Library","Canteen","Pavilion" ], "St Jude": [ "Church","Church Undercroft","Meeting Room 1","Meeting Room 2", "Meeting Room 3","School Cafeteria" ] }, // Appended to every parish list above. otherSpaceLabel: "Other" }; /* ================= END EDIT ME ================= */ var $ = function(id){ return document.getElementById(id); }; var esc = function(s){ return String(s == null ? "" : s).replace(/[&<>"']/g, function(c){ return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]; }); }; var mode = null; var openedAt = Date.now(); /* ---------------- liturgical calendar helpers ---------------- */ function easter(y){ // anonymous Gregorian computus var a=y%19,b=Math.floor(y/100),c=y%100,d=Math.floor(b/4),e=b%4, f=Math.floor((b+8)/25),g=Math.floor((b-f+1)/3),h=(19*a+b-d-g+15)%30, i=Math.floor(c/4),k=c%4,l=(32+2*e+2*i-h-k)%7,m=Math.floor((a+11*h+22*l)/451), mo=Math.floor((h+l-7*m+114)/31),da=((h+l-7*m+114)%31)+1; return new Date(y,mo-1,da); } function shift(d,n){ var x=new Date(d.getTime()); x.setDate(x.getDate()+n); return x; } function adventStart(y){ var xmas=new Date(y,11,25), dow=xmas.getDay(); return shift(xmas, -(21 + (dow || 7))); } function seasonFlags(dateStr){ if(!dateStr) return []; var p = dateStr.split("-"); var d = new Date(+p[0], +p[1]-1, +p[2]); if(isNaN(d)) return []; var y = d.getFullYear(), out = [], E = easter(y); var ash = shift(E,-46), holyMon = shift(E,-6), holySat = shift(E,-1), palm = shift(E,-7), advent = adventStart(y), xmasEve = new Date(y,11,24); if(d >= palm && d <= holySat){ out.push("This falls in Holy Week. Parish activities are canceled during Holy Week, and sports practices are not moved off-site. Expect this date to be declined."); } else if(d >= ash && d < holyMon){ out.push("This falls during Lent. Events in Lent are discouraged unless they fit the essence of the season. Say in your notes how this event fits Lent."); } if(d >= advent && d <= xmasEve){ out.push("This falls during Advent. Events in Advent are discouraged unless they fit the essence of the season. Say in your notes how this event fits Advent."); } var day = d.getDay(); if(day === 0){ out.push("Sunday. Nothing may take place on church grounds during Mass. Sunday activities begin 30 minutes after the last Mass ends, based on that campus schedule."); } else if(day === 6){ out.push("Saturday. Sports must finish one hour before Mass begins so parking clears."); } return out; } /* ---------------- field builders ---------------- */ function text(id,label,opts){ opts = opts||{}; return '
'+ ''+ ''+ (opts.hint?''+esc(opts.hint)+'':'')+ '
This is required.
'+ '
'; } function area(id,label,opts){ opts = opts||{}; return '
'+ ''+ ''+ (opts.hint?''+esc(opts.hint)+'':'')+ '
This is required.
'+ '
'; } function select(id,label,list,opts){ opts = opts||{}; var o = ''; for(var i=0;i'; } return '
'+ ''+ ''+ (opts.hint?''+esc(opts.hint)+'':'')+ '
This is required.
'+ '
'; } function pills(id,label,list,kind,opts){ opts = opts||{}; var h = '
'+esc(label)+(opts.req?'*':'')+''+ (opts.hint?''+esc(opts.hint)+'':'')+ '
'; for(var i=0;i'+esc(list[i])+''; } return h + '
Choose at least one.
'; } /* ---------------- the two forms ---------------- */ function calendarForm(){ return '' + '
Where and when
' + '
' + select('parish','Parish',["St Joseph","Visitation","St Jude"],{req:true}) + select('space','Space',[],{req:true,blank:"Choose a parish first"}) + '
' + text('spaceOther','Describe the space',{req:true,cls:"dmf-hide"}) + '
' + text('date','First date',{req:true,type:'date'}) + text('start','Start time',{req:true,type:'time'}) + text('end','End time',{req:true,type:'time'}) + '
' + '
' + '
' + select('repeat','Does this repeat?',["No, one meeting","Weekly","Every other week","Monthly","Other pattern"],{req:true}) + text('through','Repeat through',{type:'date',hint:"Leave blank if it does not repeat."}) + '
' + '
Your group
' + '
' + text('group','Ministry, committee, or group',{req:true,ph:"Knights of Columbus, PTO, Choir"}) + text('count','Expected attendance',{type:'number',ph:"25",min:"1"}) + '
' + text('purpose','What is the meeting for?',{req:true,ph:"Monthly planning meeting"}) + area('notes','Anything else the coordinator should know') + '
Contact
' + contactBlock(); } function eventForm(){ return '' + '
The event
' + text('event','Event or theme',{req:true,ph:"Fall Festival, Sacred Music Concert"}) + pills('epurpose','Purpose',["Appreciation","Fellowship","Education or study","Outreach","Fundraiser"],'radio',{req:true}) + text('recipient','Fund recipient',{req:true,hint:"Who receives the money?",cls:"dmf-hide"}) + pills('audience','Audience',["All parish","Families","Women","Men","Young adult","Teen","Youth","Adult only"],'checkbox',{req:true,hint:"Choose all that apply."}) + area('activities','Activities planned',{req:true,ph:"Walk through what happens: food, music, games, speakers, liturgy."}) + '
Where and when
' + '
' + text('date','Date',{req:true,type:'date'}) + text('start','Event start time',{req:true,type:'time'}) + text('end','Event end time',{req:true,type:'time'}) + '
' + '
' + '
' + select('parish','Location',["St Joseph","St Jude","Visitation"],{req:true}) + text('room','Specific room or space',{req:true,ph:"Undercroft, Pavilion, Gym, Parish grounds"}) + '
' + text('count','Expected attendance',{type:'number',ph:"150",min:"1"}) + '
Contact
' + contactBlock() + '
' + '' + '
Please confirm before sending.
' + '
'; } function contactBlock(){ return '' + '
' + text('name','Your name',{req:true,autocomplete:'name'}) + text('email','Your email',{req:true,type:'email',autocomplete:'email',hint:"We send your copy here."}) + '
' + text('phone','Phone',{req:true,type:'tel',autocomplete:'tel'}); } /* ---------------- routing display ---------------- */ function updateRoute(){ var box = $('dmfRoute'); if(mode === 'event'){ var r = CONFIG.routing.event; box.innerHTML = ''+esc(r.name)+''+esc(r.email)+' · reviewed with Father Don and the Leadership Team'; return; } var p = $('parish') ? $('parish').value : ""; var t = p && CONFIG.routing.calendar[p]; box.innerHTML = t ? ''+esc(t.name)+''+esc(t.email)+' · coordinator for '+esc(p)+'' : 'Choose a parish belowYour request is sent to the coordinator for that campus.'; } function updateFlags(){ var d = $('date') ? $('date').value : ""; var list = seasonFlags(d), box = $('dmfFlags'); if(!box) return; if(!list.length){ box.className = 'dmf-flags'; box.innerHTML = ''; return; } box.className = 'dmf-flags dmf-on'; box.innerHTML = list.map(function(t){ return '
'+t+'
'; }).join(''); } function refreshSpaces(){ var p = $('parish').value, sel = $('space'); if(!sel) return; var list = (CONFIG.spaces[p] || []).concat(p ? [CONFIG.otherSpaceLabel] : []); sel.innerHTML = '' + list.map(function(s){ return ''; }).join(''); toggleOtherSpace(); } function toggleOtherSpace(){ var wrap = $('spaceOther-wrap'); if(!wrap) return; var isOther = $('space').value === CONFIG.otherSpaceLabel; wrap.classList.toggle('dmf-hide', !isOther); if(!isOther){ $('spaceOther').value = ''; clearErr('spaceOther'); } } /* ---------------- render ---------------- */ function render(m){ mode = m; $('dmfFields').innerHTML = (m === 'calendar') ? calendarForm() : eventForm(); $('dmfFormTitle').textContent = (m === 'calendar') ? 'Reserve a Space' : 'Special Event Request'; $('dmfFormLede').textContent = (m === 'calendar') ? 'For meetings of an existing ministry, committee, or group.' : 'For a new event or program. Deb reviews these with Father Don and the Leadership Team for date conflicts across all three parishes, purpose and theme, and adherence to Catholic teaching.'; var today = new Date().toISOString().slice(0,10); ['date','through'].forEach(function(id){ if($(id)) $(id).min = today; }); if($('parish')) $('parish').addEventListener('change', function(){ refreshSpaces(); updateRoute(); }); if($('space')) $('space').addEventListener('change', toggleOtherSpace); if($('date')) $('date').addEventListener('change', updateFlags); Array.prototype.forEach.call(document.getElementsByName('epurpose'), function(r){ r.addEventListener('change', function(){ var isFund = picked('epurpose')[0] === 'Fundraiser'; $('recipient-wrap').classList.toggle('dmf-hide', !isFund); if(!isFund){ $('recipient').value = ''; clearErr('recipient'); } }); }); // pill visual state Array.prototype.forEach.call(document.querySelectorAll('.dmf-wrap .dmf-opt input'), function(inp){ inp.addEventListener('change', function(){ if(inp.type === 'radio'){ Array.prototype.forEach.call(document.getElementsByName(inp.name), function(o){ o.closest('.dmf-opt').classList.toggle('dmf-checked', o.checked); }); } else { inp.closest('.dmf-opt').classList.toggle('dmf-checked', inp.checked); } }); }); // clear errors as people type Array.prototype.forEach.call(document.querySelectorAll('.dmf-wrap #dmfFields input, .dmf-wrap #dmfFields select, .dmf-wrap #dmfFields textarea'), function(el){ el.addEventListener('input', function(){ clearErr(el.id); }); el.addEventListener('change', function(){ clearErr(el.id); }); }); updateRoute(); updateFlags(); show(2); } function show(n){ $('dmfStep1').hidden = (n !== 1); $('dmfStep2').hidden = (n !== 2); $('dmfStep3').hidden = (n !== 3); try{ $('dmfStep'+n).scrollIntoView({behavior:'smooth', block:'start'}); }catch(e){} } function clearErr(id){ var f = $(id), e = $(id+'-err'); if(f && f.setAttribute) f.setAttribute('data-invalid','false'); if(e) e.className = 'dmf-err'; } function setErr(id,msg){ var f = $(id), e = $(id+'-err'); if(f && f.setAttribute) f.setAttribute('data-invalid','true'); if(e){ if(msg) e.textContent = msg; e.className = 'dmf-err dmf-on'; } } function val(id){ var f = $(id); return f ? String(f.value||"").trim() : ""; } function picked(name){ return Array.prototype.filter.call(document.getElementsByName(name), function(i){ return i.checked; }) .map(function(i){ return i.value; }); } /* ---------------- validate ---------------- */ function validate(){ var bad = []; var required = (mode === 'calendar') ? ['parish','space','date','start','end','repeat','group','purpose','name','email','phone'] : ['event','date','start','end','parish','room','activities','name','email','phone']; required.forEach(function(id){ if(!val(id)){ setErr(id); bad.push(id); } }); if(mode === 'calendar' && val('space') === CONFIG.otherSpaceLabel && !val('spaceOther')){ setErr('spaceOther','Tell the coordinator which space you need.'); bad.push('spaceOther'); } if(mode === 'event'){ if(!picked('epurpose').length){ setErr('epurpose'); bad.push('epurpose'); } if(!picked('audience').length){ setErr('audience'); bad.push('audience'); } if(picked('epurpose')[0] === 'Fundraiser' && !val('recipient')){ setErr('recipient','Name the fund recipient for a fundraiser.'); bad.push('recipient'); } if(!$('ack').checked){ setErr('ack'); bad.push('ack'); } } var em = val('email'); if(em && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(em)){ setErr('email','Check this email address.'); bad.push('email'); } if(val('start') && val('end') && val('end') <= val('start')){ setErr('end','End time must be after the start time.'); bad.push('end'); } return bad; } /* ---------------- payload ---------------- */ function buildPayload(){ var to = (mode === 'event') ? CONFIG.routing.event : (CONFIG.routing.calendar[val('parish')] || {name:"", email:""}); var fields; if(mode === 'calendar'){ fields = [ ["Parish", val('parish')], ["Space", val('spaceOther') ? val('space') + ": " + val('spaceOther') : val('space')], ["First date", val('date')], ["Time", val('start') + " to " + val('end')], ["Repeats", val('repeat')], ["Repeat through", val('through')], ["Group", val('group')], ["Purpose", val('purpose')], ["Expected attendance", val('count')], ["Notes", val('notes')] ]; } else { fields = [ ["Event or theme", val('event')], ["Purpose", picked('epurpose').join(", ") + (val('recipient') ? "; fund recipient: " + val('recipient') : "")], ["Audience", picked('audience').join(", ")], ["Activities planned", val('activities')], ["Date", val('date')], ["Time", val('start') + " to " + val('end')], ["Location", val('parish')], ["Room or space", val('room')], ["Expected attendance", val('count')], ["Policy acknowledged", $('ack').checked ? "Yes" : "No"] ]; } return { formType: mode, formLabel: (mode === 'calendar') ? "Space Reservation" : "Special Event Request", routeName: to.name, routeEmail: to.email, parish: val('parish'), subjectLine: (mode === 'calendar') ? "Space request: " + val('group') + " at " + val('parish') + " on " + val('date') : "Special event request: " + val('event') + " on " + val('date'), contactName: val('name'), contactEmail: val('email'), contactPhone: val('phone'), advisories: seasonFlags(val('date')).map(function(s){ return s.replace(/<[^>]+>/g,""); }), fields: fields.filter(function(r){ return r[1]; }), pageUrl: window.location.href, hp: val('dmfHp'), elapsed: Math.round((Date.now() - openedAt) / 1000) }; } /* ---------------- send ---------------- */ function finish(ok, p, unconfirmed){ var box = $('dmfDone'); box.className = ok ? 'dmf-done' : 'dmf-done dmf-fail'; if(ok){ $('dmfDoneTitle').textContent = 'Request sent'; $('dmfDoneBody').innerHTML = 'Your ' + esc(p.formLabel.toLowerCase()) + ' went to ' + esc(p.routeName) + ' at ' + esc(p.routeEmail) + '.'; var items = ['A copy is on its way to ' + esc(p.contactEmail) + '.']; if(p.formType === 'event'){ items.push('Deb reviews the request with Father Don and the Leadership Team.'); items.push('You get a reply as soon as it has been reviewed. Approved events are then placed on the calendar.'); } else { items.push('The coordinator confirms the room and holds the date.'); } if(p.advisories.length){ items.push('Note the scheduling advisories shown on the form. Expect follow-up questions.'); } if(unconfirmed){ items.push('If you do not receive your copy within an hour, email ' + esc(p.routeEmail) + ' directly.'); } $('dmfDoneList').innerHTML = items.map(function(t){ return '
  • '+t+'
  • '; }).join(''); } else { $('dmfDoneTitle').textContent = 'That did not send'; $('dmfDoneBody').innerHTML = 'Nothing was lost. Email your request straight to ' + esc(p.routeEmail) + ', or try again in a moment.'; $('dmfDoneList').innerHTML = ''; } show(3); } function send(){ var bad = validate(); if(bad.length){ $('dmfStatus').textContent = bad.length + (bad.length === 1 ? ' field needs attention.' : ' fields need attention.'); var first = $(bad[0]); if(first && first.focus) first.focus(); if(first && first.scrollIntoView) first.scrollIntoView({behavior:'smooth', block:'center'}); return; } $('dmfStatus').textContent = ''; var p = buildPayload(); if(p.hp){ finish(true, p); return; } // bot: silently accept, send nothing var btn = $('dmfSend'); if(btn.getAttribute('data-busy') === 'true') return; btn.setAttribute('data-busy','true'); btn.textContent = 'Sending'; fetch(CONFIG.endpoint, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body: JSON.stringify(p) }) .then(function(r){ return r.json(); }) .then(function(d){ if(d && d.ok){ finish(true, p); } else { throw new Error('rejected'); } }) .catch(function(){ // Some hosts block the readable response. Fire once more blind, then tell the truth. fetch(CONFIG.endpoint, { method:'POST', mode:'no-cors', headers:{'Content-Type':'text/plain;charset=utf-8'}, body: JSON.stringify(p) }) .then(function(){ finish(true, p, true); }) .catch(function(){ finish(false, p); }); }) .then(function(){ btn.removeAttribute('data-busy'); btn.textContent = 'Send request'; }); } /* ---------------- wire up ---------------- */ Array.prototype.forEach.call(document.querySelectorAll('.dmf-wrap .dmf-choice'), function(b){ var go = function(){ render(b.getAttribute('data-mode')); }; b.addEventListener('click', go); b.addEventListener('keydown', function(ev){ if(ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar'){ ev.preventDefault(); go(); } }); }); function tap(el, fn){ el.addEventListener('click', fn); el.addEventListener('keydown', function(ev){ if(ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar'){ ev.preventDefault(); fn(); } }); } tap($('dmfSend'), send); tap($('dmfBack'), function(){ show(1); }); tap($('dmfAgain'), function(){ $('dmfFields').innerHTML = ''; $('dmfStatus').textContent = ''; openedAt = Date.now(); show(1); }); })();