eventlineup enhancments (availability notes, reminders, flags popup)

This commit is contained in:
2024-03-18 20:20:00 -05:00
parent e459a0688a
commit a1cb6fcf0a
10 changed files with 226 additions and 54 deletions

View File

@@ -67,3 +67,22 @@ exports.getEvent = async (req, res, next) => {
};
res.render("event/show", context);
};
exports.sendAvailabilityReminders = async (req,res,next) => {
await Promise.all(req.promises)
if (req.params.event_id != req.body.eventId) {
// Load actual event. Do I want this to be an error? probably
res.status(500).send()
}
const {event} = req
const {eventId, memberIds} = req.body
const sendingMember = req.members.find(m=>m.userId==req.user.id)
try {
await teamsnap.sendAvailabilityReminders(event, sendingMember, memberIds)
res.status(200).send('OK')
} catch (err) {
res.status(500).send()
}
return
}

View File

@@ -14,6 +14,22 @@ const statusCodeIcons = {
undefined: embeddedSvgFromPath("/bootstrap-icons/question-lg.svg")
}
const statusCodeClasses = {
1: "u-colorPositive",
0: "u-colorNegative",
2: "u-colorPrimary",
null: "u-colorGrey",
undefined: "u-colorGrey"
}
const statusCodeButtonClasses = {
1: "Button--yes",
0: "Button--no",
2: "Button--maybe",
null: "",
undefined: ""
}
exports.helpers = {
flagsString: (flags) => {
return flags != null ? Array.from(flags).join(",") : ''
@@ -21,25 +37,8 @@ exports.helpers = {
plus1: (i) => Number(i)+1,
positions: () => ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "EH", "DH", "DR"],
defense_positions: () => ["C", "1B", "2B", "3B", "SS", "LF", "CF", "RF", "P"],
avail_status_code_icon: (status_code) => {
const icon_classes = {
1: "u-colorPositive",
0: "u-colorNegative",
2: "u-colorPrimary",
null: "u-colorGrey",
undefined: "u-colorGrey"
}
const button_classes = {
1: "Button--yes",
0: "Button--no",
2: "Button--maybe",
null: "",
undefined: ""
}
return `<button class="Button Button--smallSquare ${button_classes[status_code]}" type="button"><span class="">${statusCodeIcons[status_code]}</span></button>`
},
avail_status_code_class: (status_code) => statusCodeButtonClasses[status_code],
avail_status_code_icon: (status_code) => statusCodeIcons[status_code],
positionLabelWithoutFlags: (label) => {
const {positionLabelWithoutFlags} = parsePositionLabel(label);
return positionLabelWithoutFlags
@@ -92,6 +91,12 @@ exports.helpers = {
statusShortLookup[NONE] = "UNK"
statusShortLookup[undefined] = "UNK"
return (statusShortLookup[availability?.statusCode])
},
filterNonPlayers: (members) => {
return members.filter(m=>!m.isNonPlayer)
},
joinMemberEmailAddresses: (members) => {
return members.map(m=>m.emailAddresses.join(',')).join(',')
}
}
@@ -121,9 +126,13 @@ exports.getAdjacentEventLineup = async (req, res) => {
} else {
throw new Error('Index must be positive or negative number')
}
if (!event) {
res.status(500).send()
return
}
const availabilitySummary = event.availabilitySummary
const event_lineup = req.timeline.event_lineups.find(i=>i.eventId==event.id)
const event_lineup_entries = req.timeline.event_lineup_entries.filter(i=>i.eventId==event.id)
const event_lineup_entries = req.timeline.event_lineup_entries?.filter(i=>i.eventId==event.id)
const availabilities = req.timeline.availabilities.filter(i=>i.eventId==event.id)
attachBenchcoachPropertiesToMember(members, event_lineup_entries, availabilities)
members.sort(tsUtils.teamsnapMembersSortLineupAvailabilityLastName)
@@ -142,7 +151,7 @@ attachBenchcoachPropertiesToMember = (members, event_lineup_entries, availabilit
// as far as I can tell, member_name should consistently be formulated from first and last name
// perhaps could have some edge cases if first or last names change, but this *should be* exceedingly rare.
const member_name = `${member.firstName} ${member.lastName}`
const event_lineup_entry = event_lineup_entries.find(e=> e.memberId == member.id || e.memberName == member_name)
const event_lineup_entry = event_lineup_entries?.find(e=> e.memberId == member.id || e.memberName == member_name)
const availability = availabilities.find(e=>e.memberId == member.id)
member.benchcoach.availability = availability
if (event_lineup_entry != null) {

View File

@@ -1004,21 +1004,21 @@ h6 {
color: #ffffff;
}
.Button--blue {
.Button--blue, button:has(+ .position-label-flags :checked) {
background-color: #1A6BAF;
border-color: #15568c;
color: #ffffff;
}
.Button--blue:hover, .Button--blue:active, .Button--blue:focus {
.Button--blue:hover, button:hover:has(+ .position-label-flags :checked), .Button--blue:active, button:active:has(+ .position-label-flags :checked), .Button--blue:focus, button:focus:has(+ .position-label-flags :checked) {
background-color: #17609e;
border-color: #134d7e;
color: #ffffff;
}
.Button--blue.is-active {
.Button--blue.is-active, button.is-active:has(+ .position-label-flags :checked) {
background-color: #17609e;
color: #ffffff;
}
.Button--blue.is-disabled, .Button--blue.is-disabled:hover, .Button--blue.is-disabled:active, .Button--blue:disabled, .Button--blue:disabled:hover, .Button--blue:disabled:active {
.Button--blue.is-disabled, button.is-disabled:has(+ .position-label-flags :checked), .Button--blue.is-disabled:hover, button.is-disabled:hover:has(+ .position-label-flags :checked), .Button--blue.is-disabled:active, button.is-disabled:active:has(+ .position-label-flags :checked), .Button--blue:disabled, button:disabled:has(+ .position-label-flags :checked), .Button--blue:disabled:hover, button:disabled:hover:has(+ .position-label-flags :checked), .Button--blue:disabled:active, button:disabled:active:has(+ .position-label-flags :checked) {
background-color: #1A6BAF;
border-color: #15568c;
color: #ffffff;
@@ -7137,8 +7137,7 @@ div[id^=event-lineup] .Panel.position-only .Panel-cell:has(.sequence), div[id^=e
}
.Panel .Panel {
border: none;
margin: 0;
margin: 8px;
}
.scroll-horizontal {

File diff suppressed because one or more lines are too long

View File

@@ -50,6 +50,7 @@ function initFlagsCheckboxes(){
).forEach((slot, i) => {
const flags = new Set(slot.querySelector("input[name*=flags]")?.value?.split(',')?.map(s=>s.trim())) || new Set()
if (flags.has('DHd')) {
console.log('dhd')
slot.querySelector('[name=flag-dhd]').checked = true;
}
@@ -509,14 +510,14 @@ function toggleChildSlots (element) {
console.log(element.closest(".slot-set"))
for (lineup_slot of document.querySelectorAll("[id^=lineup-out] .lineup-slot")) {
console.log(lineup_slot)
const cells = lineup_slot.querySelectorAll('.Panel-cell:has(.sequence), .Panel-cell:has(.drag-handle), .Panel-cell:has(.position-select-box), div.position-label-flags ')
const cells = lineup_slot.querySelectorAll('.Panel-cell:has(.sequence), .Panel-cell:has(.drag-handle), .Panel-cell:has(.position-select-box), button:has(+.position-label-flags)')
Array.from(cells).forEach(cell=>{
cell.classList.toggle('u-hidden')
})
}
}
async function submitEmail () {
async function copyEmailTable (element) {
// range=document.createRange();
// window.getSelection().removeAllRanges();
// // range.selectNode(document.querySelector('.Modal').querySelector('.Modal-body'));
@@ -558,16 +559,25 @@ async function submitEmail () {
margin: 0;
}</style>
`
html_content = emailStyle+tinymce.activeEditor.getContent()
console.log(html_content)
// html_content = emailStyle+tinymce.activeEditor.getContent()
// console.log(html_content)
const table = element.closest('form').querySelector('.lineup-table table')
navigator.clipboard.write(
[new ClipboardItem(
{
'text/plain': new Blob([tinymce.activeEditor.getContent({format: "text"})], {type: 'text/plain'}),
'text/html': new Blob([html_content], {type: 'text/html'})
})
])
// navigator.clipboard.write(
// [new ClipboardItem(
// {
// // 'text/plain': new Blob([tinymce.activeEditor.getContent({format: "text"})], {type: 'text/plain'}),
// 'text/plain': new Blob([table.innerText], {type: 'text/plain'}),
// 'text/html': new Blob([emailStyle+table.outerHTML], {type: 'text/html'})
// })
// ])
window.getSelection().removeAllRanges();
var range = document.createRange();
range.selectNode(table);
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
}
function insertLineup(direction, teamId, eventId, element) {
@@ -657,11 +667,77 @@ function initPage (){
}
for (lineup_slot of document.querySelectorAll("[id^=lineup-out] .lineup-slot")) {
const cells = lineup_slot.querySelectorAll('.Panel-cell:has(.sequence), .Panel-cell:has(.drag-handle), .Panel-cell:has(.position-select-box), div.position-label-flags')
const cells = lineup_slot.querySelectorAll('.Panel-cell:has(.sequence), .Panel-cell:has(.drag-handle), .Panel-cell:has(.position-select-box), button:has(+.position-label-flags)')
Array.from(cells).forEach(cell=>{
cell.classList.add('u-hidden')
})
}
}
function mailToLink(el, protocol='mailto') {
console.log(el)
console.log(el.dataset)
const {to, bcc, subject} = el.dataset
const params = new URLSearchParams({
bcc, subject: encodeURIComponent(subject),
})
const url = `${protocol}:${to}?${params}`
console.log(url)
// location.href=`mailto:${to}${params}`
const windowRef = window.open(url, '_blank');
windowRef.focus();
}
function sparkMailToLink(el) {
const protocol = 'readdle-spark'
const {to, bcc, subject} = el.dataset
const url = `${protocol}://compose?recipient=${to}&bcc=${bcc}&subject=${encodeURIComponent(subject)}`
console.log(url)
// location.href=`mailto:${to}${params}`
const windowRef = window.open(url, '_blank');
windowRef.focus();
}
function sendAvailabilityReminder(element, eventId, memberIds, csrf_token) {
const icon = element.querySelector('svg')
const button_text = element.querySelector('span')
icon.classList.toggle('u-hidden')
button_text.classList.toggle('u-hidden')
const loader = '<span class="PulseAnimation"><span class="PulseAnimation-dot"></span><span class="PulseAnimation-dot"></span><span class="PulseAnimation-dot"></span></span>'
const loader_node = new DOMParser().parseFromString(loader, "text/html").firstChild.querySelector('span');
element.appendChild(loader_node)
element.blur();
const data = new FormData();
const url = "../availability_reminders"
data.append('eventId', eventId)
for (var i = 0; i < memberIds.length; i++) {
data.append('memberIds[]', memberIds[i]);
}
console.log(data)
fetch(url, {
method: "POST",
body: data,
headers: {
'CSRF-Token': csrf_token
}
})
.then((response) => {
if (response.ok) {
console.log(response)
return response.text();
} else {
return Promise.reject(response.text());
}
})
.finally(()=>{
loader_node.remove()
icon.classList.toggle('u-hidden')
button_text.classList.toggle('u-hidden')
})
console.log(element, eventId, memberIds)
}
document.addEventListener('DOMContentLoaded', initPage)

View File

@@ -3,6 +3,8 @@ const eventsController = require("../controllers/event");
const router = express.Router();
const tsUtils = require("../lib/utils")
const {teamsnapCallback} = require("../lib/utils")
const multer = require("multer");
const upload = multer()
// Middleware
const loadEvent = (req,res,next) => {
@@ -68,6 +70,7 @@ router.use("/:team_id([0-9]+)/event/:event_id([0-9]+)", loadEvent)
// Routes
router.get("/:team_id([0-9]+)/schedule", eventsController.getEvents);
router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)", eventsController.getEvent);
router.post("/:team_id([0-9]+)/event/:event_id([0-9]+)/availability_reminders", upload.none(), eventsController.sendAvailabilityReminders)
// router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup", eventsController.getLineup);
// router.get("/:team_id([0-9]+)/event/:event_id([0-9]+)/lineup_card", eventsController.getLineupCard);

View File

@@ -363,3 +363,7 @@ div[id^="event-lineup"] .Panel {
.scroll-horizontal {
overflow-x: scroll;
}
button:has(+.position-label-flags :checked) {
@extend .Button--blue
}

View File

@@ -39,6 +39,25 @@
{{{embeddedSvgFromPath "/bootstrap-icons/caret-left.svg"}}}
<span>Insert previous lineup</span>
</a>
<div class="u-hidden">
<hr class="Divider u-spaceEndsNone">
<span class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="console.log('not implemented yet')">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/send.svg"}}}
<span>Availability Reminders</span>
</span>
<hr class="Divider u-spaceEndsNone">
<span class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="console.log('not implemented yet')">
<span>Reset All Availabilities</span>
</span>
<hr class="Divider u-spaceEndsNone">
<span class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="console.log('not implemented yet')">
<span>Clear Lineup</span>
</span>
<hr class="Divider u-spaceEndsNone">
<span class="u-padEndsSm u-padSidesMd u-textDecorationNone" href="javascript:void(0)" onclick="console.log('not implemented yet')">
<span>Publish</span>
</span>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,4 @@
<div id="modal" class="Modal Modal--clickableBg">
<div class="Modal-bgDismiss" onclick="javascript:this.closest('.Modal').remove();tinymce.remove();"></div>
<div class="Modal-content">
<div onclick="javascript:this.closest('.Modal').remove();tinymce.remove();">{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/dismiss.svg" "Modal-iconDismiss"}}}</div>
<div class="Modal-header">
@@ -17,10 +16,21 @@
</div>
<div class="FieldGroup">
<label class="FieldGroup-label">Lineup</label>
<div class="lineup-email">{{>email_table}}</div>
<div class="lineup-email lineup-table">{{>email_table}}</div>
</div>
<div class="FieldGroup">
<button class="Button" role="button" onclick="submitEmail();">Submit</button>
<button class="Button" role="button" type="button" onclick="copyEmailTable(this)">
{{{embeddedSvgFromPath "/bootstrap-icons/clipboard-fill.svg"}}}
{{{embeddedSvgFromPath "/bootstrap-icons/table.svg"}}}
Copy Table
</button>
<button class="Button" role="button" type="button" onclick="sparkMailToLink(this);",
data-to="{{user.email}}"
data-bcc="{{joinMemberEmailAddresses (filterNonPlayers members)}}"
data-subject="{{dateFormat event.startDate "ddd, MMM D, YYYY h:mm A" }}, {{ event.locationName }}, ({{#if (isAway event) }}@{{/if}}{{ event.opponentName }})">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/mail.svg"}}}
Spark Mail
</button>
</div>
</form>
</div>

View File

@@ -17,13 +17,39 @@
</div>
<div class="Panel-cell u-padXs u-sizeFill u-flex">
<div
class="availability-status-code-{{
class="Popup availability-status-code-{{
member.benchcoach.availability?.statusCode
}}"
>
{{#if member.benchcoach.availability}}{{{avail_status_code_icon member.benchcoach.availability.statusCode}}}{{/if}}
{{#if member.benchcoach.availability}}
{{#with member.benchcoach.availability}}
<button class="Popup-toggle Button Button--smallSquare {{avail_status_code_class statusCode}}"
type="button"
data-control="popup"
data-open="availablility-popup-{{memberId}}-{{eventId}}"
onclick="this.closest('div').querySelector('.Popup-container').classList.toggle('is-open')"
>
{{#if notes}}{{{embeddedSvgFromPath "/bootstrap-icons/asterisk.svg"}}}{{else}}{{{avail_status_code_icon statusCode}}}{{/if}}
</button>
<div class="Popup-container Popup-container--left" data-popup="availablility-popup-{{memberId}}-{{eventId}}">
<div class="Popup-content u-padSm u-textCenter">
<h3 class="u-spaceBottomSm">Availability</h3>
{{#if notes}}
<p class="u-textLeft">“ <i>{{notes}}</i> ”</p>
{{else}}
<p class="u-textLeft">No notes.</p>
{{/if}}
<button type="button" class="Button u-spaceTopSm" onclick="sendAvailabilityReminder(this, {{eventId}}, ['{{memberId}}'], {{csrfToken}})">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/send.svg"}}}
<span>Send Reminder</span>
</button>
</div>
</div>
{{/with}}
{{/if}}
</div>
<div class="u-fontSizeLg u-textNoWrap">
<div class="u-fontSizeLg u-textNoWrap">
<span class="lastname">
{{member.lastName}}
</span>
@@ -35,14 +61,21 @@
</span>
</div>
<div class="u-flexGrow1"></div>
<div class="position-label-flags u-textNoWrap">
<div class="Checkbox Checkbox--inline">
<input class="Checkbox-input" type="checkbox" name="flag-drd" id="flag-drd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}" onclick="refreshLineup()">
<label class="Checkbox-label" for="flag-drd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}">DR<small>d</small></label>
</div>
<div class="Checkbox Checkbox--inline">
<input class="Checkbox-input" type="checkbox" name="flag-dhd" id="flag-dhd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}" onclick="refreshLineup()">
<label class="Checkbox-label" for="flag-dhd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}">DH<small>d</small></label>
<div class="Popup">
<button type="button" class="Popup-toggle Button Button--smallSquare" onclick="this.closest('div').querySelector('.Popup-container').classList.toggle('is-open');this.blur();" href="javascript:void(0)">
{{{embeddedSvgFromPath "/teamsnap-ui/assets/icons/flag.svg"}}}
</button>
<div class="Popup-container Popup-container--rightHang position-label-flags">
<div class="Popup-content u-padSm u-textCenter">
<div class="Checkbox Checkbox--inline">
<input class="Checkbox-input" type="checkbox" name="flag-drd" id="flag-drd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}" onclick="refreshLineup()">
<label class="Checkbox-label" for="flag-drd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}">DR<small>d</small></label>
</div>
<div class="Checkbox Checkbox--inline">
<input class="Checkbox-input" type="checkbox" name="flag-dhd" id="flag-dhd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}" onclick="refreshLineup()">
<label class="Checkbox-label" for="flag-dhd-{{member.id}}-{{member.benchcoach.eventLineupEntry.id}}">DH<small>d</small></label>
</div>
</div>
</div>
</div>
</div>