This commit is contained in:
Peter Howell 2026-01-15 20:02:46 +00:00
parent 4c705703b5
commit 4ceb28e7ef
3 changed files with 128 additions and 24 deletions

View File

@ -3,5 +3,5 @@ $MY_TITLE = "Schedule & Sessions";
$MY_CRUMB = "My Schedule"; $MY_CRUMB = "My Schedule";
$timeslot_config = file_exists('schedule_timeslots.json') ? json_decode(file_get_contents('schedule_timeslots.json'), true) : []; $timeslot_config = file_exists('schedule_timeslots.json') ? json_decode(file_get_contents('schedule_timeslots.json'), true) : [];
$config_js = '<script>window.TIMESLOT_CONFIG = ' . json_encode($timeslot_config) . ';</script>'; $config_js = '<script>window.TIMESLOT_CONFIG = ' . json_encode($timeslot_config) . ';</script>';
$CONTENT = $config_js . '<div id="timeslot-config" class="hidden"></div><activitylist :itineraryview="1" :show_all_sessions="true"></activitylist>'; $CONTENT = '<div id="timeslot-config" class="hidden"></div><activitylist :itineraryview="1" :show_all_sessions="true"></activitylist>';
include 'layout.php'; include 'layout.php';

View File

@ -872,11 +872,16 @@ const ActivityList = Vue.component('activitylist', {
data: function () { data: function () {
return { activities:[], mysessions:[], search:'', sortby:'starttime', reversed:false, my_ses_ids:[], my_host_ids:[], return { activities:[], mysessions:[], search:'', sortby:'starttime', reversed:false, my_ses_ids:[], my_host_ids:[],
show_filters: 'all', expanded: 1, editing: -1, active:-1, hosts:{}, selectedSlotKey: null, show_filters: 'all', expanded: 1, editing: -1, active:-1, hosts:{}, selectedSlotKey: null,
hosts_by_sesid: {}, options:{}, conference:{}, ay:{}, conf:-1, survey_on:0, zoom_on:1, cancelingIds:[], timeslotConfig:null, } }, hosts_by_sesid: {}, options:{}, conference:{}, ay:{}, conf:-1, survey_on:0, zoom_on:1,
cancelingIds:[], timeslotConfig:null, expandedDesc:{}, collapsedDays:{}, } },
mounted: function() { mounted: function() {
this.fetch_myevents() this.fetch_myevents()
}, },
methods: { methods: {
normalizeId: function(id) {
var parsed = parseInt(id, 10);
return Number.isNaN(parsed) ? id : parsed;
},
special_signup: function(activity_id) { special_signup: function(activity_id) {
if (activity_id==1462 || activity_id==1455) { if (activity_id==1462 || activity_id==1455) {
return true; return true;
@ -896,12 +901,45 @@ const ActivityList = Vue.component('activitylist', {
group.classList.toggle('hidden'); group.classList.toggle('hidden');
btn.textContent = group.classList.contains('hidden') ? 'Expand Day' : 'Collapse Day'; btn.textContent = group.classList.contains('hidden') ? 'Expand Day' : 'Collapse Day';
}, },
toggleDaySlots: function(dayKey) {
var isCollapsed = !!this.collapsedDays[dayKey];
this.$set(this.collapsedDays, dayKey, !isCollapsed);
},
isDayCollapsed: function(dayKey) {
return !!this.collapsedDays[dayKey];
},
toggleDescription: function(id) { toggleDescription: function(id) {
const para = document.getElementById(id); const para = document.getElementById(id);
const btn = document.getElementById(id + '-btn'); const btn = document.getElementById(id + '-btn');
para.classList.toggle('line-clamp-2'); para.classList.toggle('line-clamp-2');
btn.textContent = para.classList.contains('line-clamp-2') ? 'Show More' : 'Show Less'; btn.textContent = para.classList.contains('line-clamp-2') ? 'Show More' : 'Show Less';
}, },
descPlain: function(desc) {
if (!desc) { return ''; }
return String(desc)
.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
},
descPreview: function(desc) {
var text = this.descPlain(desc);
var limit = 200;
if (text.length <= limit) { return text; }
var slice = text.slice(0, limit + 1);
var cutoff = slice.lastIndexOf(' ');
if (cutoff < 0) { cutoff = limit; }
return text.slice(0, cutoff).trim() + '...';
},
descIsTruncated: function(desc) {
return this.descPlain(desc).length > 200;
},
isDescExpanded: function(id) {
return this.expandedDesc && this.expandedDesc[id];
},
toggleDesc: function(id) {
if (!this.expandedDesc) { this.expandedDesc = {}; }
this.$set(this.expandedDesc, id, !this.expandedDesc[id]);
},
slotKeyFromStart: function(starttime) { slotKeyFromStart: function(starttime) {
if (!starttime) { return null; } if (!starttime) { return null; }
return this.$root.$dj(starttime).format('YYYY-MM-DDTHH:mm'); return this.$root.$dj(starttime).format('YYYY-MM-DDTHH:mm');
@ -952,7 +990,9 @@ const ActivityList = Vue.component('activitylist', {
basic_get('api2.php?query=app', basic_get('api2.php?query=app',
function(r2) { function(r2) {
self.mysessions = r2.mysessions self.mysessions = r2.mysessions
self.my_ses_ids = _.pluck(r2.mysessions, 'id') self.my_ses_ids = _.map(_.pluck(r2.mysessions, 'id'), function(val) {
return self.normalizeId(val);
})
self.activities = r2.sessions self.activities = r2.sessions
if (r2.host != null) { self.my_host_ids = r2.host } if (r2.host != null) { self.my_host_ids = r2.host }
else { self.my_host_ids = [] } else { self.my_host_ids = [] }
@ -971,20 +1011,35 @@ const ActivityList = Vue.component('activitylist', {
}, },
joinme: function(id) { joinme: function(id) {
var self = this var self = this
id = self.normalizeId(id)
basic_get('dir_api.php?a=signup/' + id, basic_get('dir_api.php?a=signup/' + id,
function(r2) { function(r2) {
self.mysessions.push(_.findWhere(self.activities, {'id':id})) if (!self.my_ses_ids.includes(id)) { self.my_ses_ids.push(id) }
self.my_ses_ids.push(id) var existing = _.find(self.mysessions, function(s) {
return self.normalizeId(s.id) === id;
});
if (!existing) {
var activity = _.find(self.activities, function(a) {
return self.normalizeId(a.id) === id;
});
if (activity) { self.mysessions.push(activity) }
}
self.$forceUpdate()
alert_message("Added activity") }) alert_message("Added activity") })
}, },
dumpme: function(id) { dumpme: function(id) {
var self = this var self = this
id = self.normalizeId(id)
if (!self.cancelingIds.includes(id)) { self.cancelingIds.push(id) } if (!self.cancelingIds.includes(id)) { self.cancelingIds.push(id) }
basic_get('dir_api.php?a=signdown/' + id, basic_get('dir_api.php?a=signdown/' + id,
function(r2) { function(r2) {
setTimeout(function() { setTimeout(function() {
self.mysessions = _.without( self.mysessions, _.findWhere(self.activities, {'id':id})) self.mysessions = _.filter(self.mysessions, function(s) {
self.my_ses_ids = _.without( self.my_ses_ids, id) return self.normalizeId(s.id) !== id;
});
self.my_ses_ids = _.filter(self.my_ses_ids, function(sid) {
return self.normalizeId(sid) !== id;
});
self.cancelingIds = _.without(self.cancelingIds, id) self.cancelingIds = _.without(self.cancelingIds, id)
self.$forceUpdate() self.$forceUpdate()
alert_message("Removed activity") alert_message("Removed activity")
@ -1008,6 +1063,18 @@ const ActivityList = Vue.component('activitylist', {
}, },
slotPresets: function(slot) { return slot.presetSessions || []; }, slotPresets: function(slot) { return slot.presetSessions || []; },
slotHasPreset: function(slot) { return (slot.presetSessions && slot.presetSessions.length>0); }, slotHasPreset: function(slot) { return (slot.presetSessions && slot.presetSessions.length>0); },
canJoin: function(activity) {
if (this.zoom_on !== 1) { return false; }
return activity && (activity.mode === 'online' || activity.mode === 'hybrid');
},
canSurvey: function(activity) {
if (this.survey_on !== 1) { return false; }
return !!activity;
},
hasJoinLink: function(activity) {
if (!activity || !activity.location) { return false; }
return String(activity.location).trim().length > 0;
},
loadTimeslots: function() { loadTimeslots: function() {
var self=this; var self=this;
if (window.TIMESLOT_CONFIG) { if (window.TIMESLOT_CONFIG) {
@ -1134,8 +1201,14 @@ const ActivityList = Vue.component('activitylist', {
</div> </div>
<div v-for="(slots, mmyy) in timeSlotsByDay" :key="mmyy" class="mb-6"> <div v-for="(slots, mmyy) in timeSlotsByDay" :key="mmyy" class="mb-6">
<h4 class="text-lg font-semibold text-gray-700 mb-2">{{ mmyy }} {{ get_day_title(mmyy) }}</h4> <div class="flex items-center gap-2 mb-2">
<div class="space-y-3"> <button class="text-lg font-semibold text-gray-500 hover:text-gray-700 w-6 text-center"
@click.prevent="toggleDaySlots(mmyy)">
{{ isDayCollapsed(mmyy) ? '+' : '-' }}
</button>
<h4 class="text-lg font-semibold text-gray-700">{{ mmyy }} {{ get_day_title(mmyy) }}</h4>
</div>
<div v-if="!isDayCollapsed(mmyy)" class="space-y-3">
<div v-for="slot in slots" :key="slot.key" :class="['border rounded-lg p-4 bg-white shadow-sm flex items-start gap-4', selectedSlotKey === slot.key ? 'ring-2 ring-blue-400' : '']"> <div v-for="slot in slots" :key="slot.key" :class="['border rounded-lg p-4 bg-white shadow-sm flex items-start gap-4', selectedSlotKey === slot.key ? 'ring-2 ring-blue-400' : '']">
<div class="w-32 text-right"> <div class="w-32 text-right">
<div class="text-2xl font-extrabold text-blue-800 leading-tight">{{ slot.startLabel }}</div> <div class="text-2xl font-extrabold text-blue-800 leading-tight">{{ slot.startLabel }}</div>
@ -1160,13 +1233,28 @@ const ActivityList = Vue.component('activitylist', {
<div v-for="sel in slotSelection(slot)" :key="sel.id" :class="['border-b last:border-none pb-2 last:pb-0 transition-opacity duration-300', isCanceling(sel.id) ? 'opacity-40' : '']"> <div v-for="sel in slotSelection(slot)" :key="sel.id" :class="['border-b last:border-none pb-2 last:pb-0 transition-opacity duration-300', isCanceling(sel.id) ? 'opacity-40' : '']">
<div class="font-semibold text-gray-900">{{ sel.title }}</div> <div class="font-semibold text-gray-900">{{ sel.title }}</div>
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
{{ $root.$dj(sel.starttime).format('h:mma') }} · {{ mode_string(sel) }} <div class="flex flex-wrap items-center gap-2">
<span>{{ $root.$dj(sel.starttime).format('h:mma') }} - {{ addTime(sel.starttime, sel.length) }} · {{ mode_string(sel) }}</span>
<span v-if="sel.mode === 'hybrid'">·
<span v-if="sel.location_irl">{{ sel.location_irl }}</span>
<span v-else>(location TBD)</span>
</span>
</div> </div>
<div class="mt-1 flex flex-wrap items-center gap-2">
<a v-if="canJoin(sel) && hasJoinLink(sel)"
:href="sel.location"
class="inline-flex items-center px-2 py-1 text-xs font-semibold uppercase tracking-wide text-white bg-blue-600 rounded hover:bg-blue-700">JOIN Online</a>
<span v-else-if="canJoin(sel)" class="text-xs text-red-600">[missing zoom link]</span>
<a v-if="canSurvey(sel)"
:href="'survey.php?s=' + sel.id"
class="inline-flex items-center px-2 py-1 text-xs font-semibold uppercase tracking-wide text-white bg-emerald-600 rounded hover:bg-emerald-700">TAKE Survey</a>
<button v-if="!my_host_ids.includes(sel.id)" <button v-if="!my_host_ids.includes(sel.id)"
class="mt-2 text-sm text-red-600 hover:underline" class="inline-flex items-center px-2 py-1 text-xs font-semibold uppercase tracking-wide text-white bg-red-600 rounded hover:bg-red-700"
@click.prevent="dumpme(sel.id)">Cancel</button> @click.prevent="dumpme(sel.id)">Cancel</button>
</div> </div>
<button class="mt-1 text-sm text-blue-600 hover:underline" @click="selectSlot(slot)">Change selection</button> </div>
</div>
<button class="mt-1 text-sm text-blue-600 hover:underline" @click="selectSlot(slot)">Change session</button>
</div> </div>
<div v-else class="mt-1"> <div v-else class="mt-1">
<emptyslotcard label="Signup" @select="selectSlot(slot)"></emptyslotcard> <emptyslotcard label="Signup" @select="selectSlot(slot)"></emptyslotcard>
@ -1187,15 +1275,28 @@ const ActivityList = Vue.component('activitylist', {
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="flex-1"> <div class="flex-1">
<div class="font-semibold text-gray-900">{{ a.title }}</div> <div class="font-semibold text-gray-900">{{ a.title }}</div>
<div class="text-xs uppercase text-gray-500">{{ mode_string(a) }}</div> <!--<div class="text-xs uppercase text-gray-500">{{ mode_string(a) }}</div>-->
<div class="text-sm text-gray-600 mt-1"> <div class="text-sm text-gray-600 mt-1">
<span v-if="a.mode === 'hybrid'">In person at {{ a.location_irl }} or online</span> <span v-if="a.mode === 'hybrid'">In person<span v-if="a.location_irl"> at {{ a.location_irl }}</span> or online</span>
<span v-if="a.mode === 'inperson'">In person at {{ a.location_irl }}</span> <span v-if="a.mode === 'inperson'">In person<span v-if="a.location_irl"> at {{ a.location_irl }}</span><span v-else> location TBD</span></span>
<span v-if="a.mode === 'online'">Online</span> <span v-if="a.mode === 'online'">Online</span>
</div> </div>
<div v-if="a.desc" class="text-sm text-gray-700 mt-2">
<span v-if="!isDescExpanded(a.id)">
{{ descPreview(a.desc) }}
<button v-if="descIsTruncated(a.desc)"
class="ml-1 text-blue-600 hover:underline"
@click.prevent="toggleDesc(a.id)">[+ read more]</button>
</span>
<span v-else>
<span v-html="a.desc"></span>
<button class="ml-1 text-blue-600 hover:underline"
@click.prevent="toggleDesc(a.id)">[- read less]</button>
</span>
</div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button v-if="!my_ses_ids.includes(a.id)" <button v-if="!my_ses_ids.includes(normalizeId(a.id))"
class="px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded hover:bg-blue-700" class="px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded hover:bg-blue-700"
@click.prevent="chooseSession(a)"> @click.prevent="chooseSession(a)">
Sign Up Sign Up
@ -1216,10 +1317,13 @@ const ActivityList = Vue.component('activitylist', {
<!-- No sessions signed up --> <!-- No sessions signed up -->
<div v-if="itineraryview && mysessions_filtered.length === 0" class="mb-6"> <div v-if="itineraryview && mysessions_filtered.length === 0" class="mb-6">
<p v-if="active < 1" class="font-semibold text-gray-800">Loading your itinerary...</p>
<template v-else>
<p class="font-semibold text-gray-800">It looks like you haven't signed up for any sessions yet!</p> <p class="font-semibold text-gray-800">It looks like you haven't signed up for any sessions yet!</p>
<p class="mt-2 text-sm text-blue-600"> <p class="mt-2 text-sm text-blue-600">
Go to the <a class="underline hover:text-blue-800" href="allsessions.php">Sessions List</a> to sign up. Go to the <a class="underline hover:text-blue-800" href="allsessions.php">Sessions List</a> to sign up.
</p> </p>
</template>
</div> </div>
<!-- Simple full list for All Sessions --> <!-- Simple full list for All Sessions -->

View File

@ -25,7 +25,7 @@ $MOD_DATE = file_exists(__FILE__) ? date("F d Y H:i.", filemtime(__FILE__)) : "(
<title><?= htmlspecialchars($MY_TITLE) ?> | Gavilan Intranet</title> <title><?= htmlspecialchars($MY_TITLE) ?> | Gavilan Intranet</title>
<script src="js/tailwind.js"></script> <script src="js/tailwind.js"></script>
<script src="js/vue27max.js"></script> <script src="js/vue27max.js"></script>
<script src="js/intranet_libs.js"/></script> <script src="js/intranet_libs.js"></script>
<?php if (isset($config_js)) { echo $config_js; } ?> <?php if (isset($config_js)) { echo $config_js; } ?>
<link rel="stylesheet" type="text/css" href="style.css" /> <link rel="stylesheet" type="text/css" href="style.css" />
</head> </head>
@ -74,7 +74,7 @@ $MOD_DATE = file_exists(__FILE__) ? date("F d Y H:i.", filemtime(__FILE__)) : "(
<br /> &nbsp; <br /> &nbsp;
<br /> &nbsp; <br /> &nbsp;
<br /> &nbsp; <br /> &nbsp;
<script src="js/intranet_libs_bottom.js"/></script> <script src="js/intranet_libs_bottom.js"></script>
<script src="js/dir_app.js?v=<?= $DIR_APP_VER ?>"></script> <script src="js/dir_app.js?v=<?= $DIR_APP_VER ?>"></script>
<script src="<?= $XTRAJS ?>"></script> <script src="<?= $XTRAJS ?>"></script>
</body> </body>