/* 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'+esc(list[i])+''; }
return '
'+
''+
''+
(opts.hint?''+esc(opts.hint)+'':'')+
'
This is required.
'+
'
';
}
function pills(id,label,list,kind,opts){
opts = opts||{};
var h = '
' +
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."}) +
'
' +
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);
});
})();