jan26 prepped

This commit is contained in:
Peter Howell 2026-01-15 21:46:58 +00:00
parent 4ceb28e7ef
commit 411a2a96d0
4 changed files with 137 additions and 14 deletions

View File

@ -649,6 +649,9 @@ function get_ses_hosts() { global $c, $AY;
$ID = ok($_REQUEST['id']); $ID = ok($_REQUEST['id']);
return multi_row_select("select s.id, s.title, s.starttime, u.name, u.email, u.id AS hostid from conf_sessions as s LEFT OUTER JOIN conf_hosts as h ON h.session=s.id LEFT OUTER JOIN conf_users AS u ON h.host=u.id WHERE s.id='{$ID}' ORDER BY u.name;",1); return multi_row_select("select s.id, s.title, s.starttime, u.name, u.email, u.id AS hostid from conf_sessions as s LEFT OUTER JOIN conf_hosts as h ON h.session=s.id LEFT OUTER JOIN conf_users AS u ON h.host=u.id WHERE s.id='{$ID}' ORDER BY u.name;",1);
} }
if (isset($_REQUEST['all']) && $_REQUEST['all'] == '1') {
return multi_row_select("select s.id, s.title, s.starttime, u.name, u.email, u.id AS hostid from conf_sessions as s LEFT OUTER JOIN conf_hosts as h ON h.session=s.id LEFT OUTER JOIN conf_users AS u ON h.host=u.id ORDER BY u.name;",1);
}
$date_clause = api_date_clause('s.starttime'); $date_clause = api_date_clause('s.starttime');
return multi_row_select("select s.id, s.title, s.starttime, u.name, u.email, u.id AS hostid from conf_sessions as s LEFT OUTER JOIN conf_hosts as h ON h.session=s.id LEFT OUTER JOIN conf_users AS u ON h.host=u.id WHERE $date_clause ORDER BY u.name;",1); } return multi_row_select("select s.id, s.title, s.starttime, u.name, u.email, u.id AS hostid from conf_sessions as s LEFT OUTER JOIN conf_hosts as h ON h.session=s.id LEFT OUTER JOIN conf_users AS u ON h.host=u.id WHERE $date_clause ORDER BY u.name;",1); }

View File

@ -3,5 +3,4 @@
$MY_TITLE = "Edit Sessions"; $MY_TITLE = "Edit Sessions";
$MY_CRUMB = "Edit"; $MY_CRUMB = "Edit";
$CONTENT = '<activityeditorlist></activityeditorlist>'; $CONTENT = '<activityeditorlist></activityeditorlist>';
$XTRAJS = 'js/editor.js';
include 'layout.php'; include 'layout.php';

View File

@ -28,10 +28,18 @@ function init_file_dropzone(parameter_name) {
function parsesqltime(mysqldate) { // 2021-01-29 09:00:00 function parsesqltime(mysqldate) { // 2021-01-29 09:00:00
if (! mysqldate) { return 0 } if (!mysqldate) { return 0 }
var field = mysqldate.match(/^(\d\d\d\d)\-(\d+)\-(\d+)[T|\s](\d+)\:(\d+)\:(\d+)$/) if (mysqldate instanceof Date) { return mysqldate }
var mydate = new Date(field[1], field[2] - 1 , field[3], field[4], field[5], field[6]) var s = String(mysqldate).trim()
return mydate } var field = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})[T\s](\d{1,2}):(\d{2})(?::(\d{2}))?$/)
if (field) {
var sec = field[6] ? field[6] : 0
return new Date(field[1], field[2] - 1, field[3], field[4], field[5], sec)
}
var parsed = new Date(s)
if (!isNaN(parsed.getTime())) { return parsed }
return 0
}
function dj(mysqldate) { return dayjs(parsesqltime(mysqldate)) } function dj(mysqldate) { return dayjs(parsesqltime(mysqldate)) }
@ -873,7 +881,8 @@ const ActivityList = Vue.component('activitylist', {
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, hosts_by_sesid: {}, options:{}, conference:{}, ay:{}, conf:-1, survey_on:0, zoom_on:1,
cancelingIds:[], timeslotConfig:null, expandedDesc:{}, collapsedDays:{}, } }, cancelingIds:[], timeslotConfig:null, expandedDesc:{}, collapsedDays:{},
surveyAnswersBySession: {}, } },
mounted: function() { mounted: function() {
this.fetch_myevents() this.fetch_myevents()
}, },
@ -983,10 +992,24 @@ const ActivityList = Vue.component('activitylist', {
am_editing: function(id) { return 0 }, am_editing: function(id) { return 0 },
fetch_myevents: function() { fetch_myevents: function() {
var self = this; var self = this;
basic_get('dir_api.php?a=get/hosts', function(r2) { var hostUrl = 'dir_api.php?a=get/hosts';
if (self.show_all_sessions) { hostUrl += '&all=1'; }
basic_get(hostUrl, function(r2) {
self.hosts_by_sesid = _.groupBy(r2,function(x) { return x.id } ) self.hosts_by_sesid = _.groupBy(r2,function(x) { return x.id } )
} ) } )
basic_get('dir_api.php?a=get/questions', function(r2) {
var answered = {};
_.each(r2, function(row) {
var sesId = self.normalizeId(row.ses_id || row.session || row.ses);
if (!sesId) { return; }
if (row.answer !== null && String(row.answer).trim() !== '') {
answered[sesId] = true;
}
});
self.surveyAnswersBySession = answered;
})
basic_get('api2.php?query=app', basic_get('api2.php?query=app',
function(r2) { function(r2) {
self.mysessions = r2.mysessions self.mysessions = r2.mysessions
@ -999,7 +1022,9 @@ const ActivityList = Vue.component('activitylist', {
self.options = r2.options self.options = r2.options
self.conference = r2.conference self.conference = r2.conference
self.ay = r2.ay self.ay = r2.ay
if (r2.hostbysession) {
self.hosts_by_sesid = _.groupBy(r2.hostbysession,function(x) { return x.id } ) self.hosts_by_sesid = _.groupBy(r2.hostbysession,function(x) { return x.id } )
}
self.survey_on = parseInt( _.findWhere(self.options, { label:'survey_on' }).value ) self.survey_on = parseInt( _.findWhere(self.options, { label:'survey_on' }).value )
self.zoom_on = parseInt( _.findWhere(self.options, { label:'zoom_on' }).value ) self.zoom_on = parseInt( _.findWhere(self.options, { label:'zoom_on' }).value )
@ -1063,6 +1088,10 @@ 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); },
nl2br: function(text) {
if (!text) { return ''; }
return String(text).replace(/\r?\n/g, '<br>');
},
canJoin: function(activity) { canJoin: function(activity) {
if (this.zoom_on !== 1) { return false; } if (this.zoom_on !== 1) { return false; }
return activity && (activity.mode === 'online' || activity.mode === 'hybrid'); return activity && (activity.mode === 'online' || activity.mode === 'hybrid');
@ -1071,6 +1100,11 @@ const ActivityList = Vue.component('activitylist', {
if (this.survey_on !== 1) { return false; } if (this.survey_on !== 1) { return false; }
return !!activity; return !!activity;
}, },
hasSurveyAnswer: function(activity) {
if (!activity) { return false; }
var sid = this.normalizeId(activity.id);
return !!this.surveyAnswersBySession[sid];
},
hasJoinLink: function(activity) { hasJoinLink: function(activity) {
if (!activity || !activity.location) { return false; } if (!activity || !activity.location) { return false; }
return String(activity.location).trim().length > 0; return String(activity.location).trim().length > 0;
@ -1221,11 +1255,11 @@ const ActivityList = Vue.component('activitylist', {
<div v-for="sel in slotPresets(slot)" :key="sel.title" class="border-b last:border-none pb-2 last:pb-0"> <div v-for="sel in slotPresets(slot)" :key="sel.title" class="border-b last:border-none pb-2 last:pb-0">
<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">
<span v-if="sel.audience" class="mr-2 capitalize">{{ sel.audience }} meeting</span> <span v-if="sel.audience" class="capitalize">{{ sel.audience }} meeting</span>
<span v-if="sel.location">at {{ sel.location }}</span> <span v-if="sel.location">at {{ sel.location }}</span>
<span v-if="sel.mode">· {{ sel.mode }}</span> <span v-if="sel.mode">· {{ sel.mode }}</span>
</div> </div>
<div v-if="sel.notes" class="text-xs text-gray-500 mt-1">{{ sel.notes }}</div> <div v-if="sel.notes" class="text-xs text-gray-500 mt-1" v-html="nl2br(sel.notes)"></div>
</div> </div>
</div> </div>
@ -1247,7 +1281,10 @@ const ActivityList = Vue.component('activitylist', {
<span v-else-if="canJoin(sel)" class="text-xs text-red-600">[missing zoom link]</span> <span v-else-if="canJoin(sel)" class="text-xs text-red-600">[missing zoom link]</span>
<a v-if="canSurvey(sel)" <a v-if="canSurvey(sel)"
:href="'survey.php?s=' + sel.id" :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> :class="['inline-flex items-center px-2 py-1 text-xs font-semibold uppercase tracking-wide text-white rounded',
hasSurveyAnswer(sel) ? 'bg-red-600 hover:bg-red-700' : 'bg-emerald-600 hover:bg-emerald-700']">
{{ hasSurveyAnswer(sel) ? 'EDIT Survey' : 'TAKE Survey' }}
</a>
<button v-if="!my_host_ids.includes(sel.id)" <button v-if="!my_host_ids.includes(sel.id)"
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" 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>
@ -1336,6 +1373,10 @@ const ActivityList = Vue.component('activitylist', {
<div> <div>
<div class="text-sm text-gray-500">{{ $root.$dj(a.starttime).format('h:mma') }} - {{ addTime(a.starttime, a.length) }}</div> <div class="text-sm text-gray-500">{{ $root.$dj(a.starttime).format('h:mma') }} - {{ addTime(a.starttime, a.length) }}</div>
<h4 class="text-lg font-semibold text-gray-900">{{ a.title }}</h4> <h4 class="text-lg font-semibold text-gray-900">{{ a.title }}</h4>
<div class="text-sm text-gray-600">
<span v-if="hoststr(a.id)">{{ hoststr(a.id) }}</span>
<i v-else>no host registered</i>
</div>
<p class="text-sm text-gray-700 mt-1" v-html="a.desc"></p> <p class="text-sm text-gray-700 mt-1" v-html="a.desc"></p>
<p class="text-xs uppercase text-gray-500 mt-2">{{ mode_string(a) }}</p> <p class="text-xs uppercase text-gray-500 mt-2">{{ mode_string(a) }}</p>
<p class="text-sm text-gray-600" v-if="a.location_irl">Location: {{ a.location_irl }}</p> <p class="text-sm text-gray-600" v-if="a.location_irl">Location: {{ a.location_irl }}</p>
@ -1554,6 +1595,7 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
reversed: false, reversed: false,
selectedIndex: 0, selectedIndex: 0,
editingId: null, editingId: null,
dayFilter: "all",
// filters // filters
show_filters: "all", show_filters: "all",
filters: { filters: {
@ -1577,9 +1619,27 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
this.$root.fetch_menus(); this.$root.fetch_menus();
this.fetchAll(); this.fetchAll();
window.addEventListener("keydown", this.onKey); window.addEventListener("keydown", this.onKey);
this.changedHandler = (dat) => {
const column = dat[0];
const table = dat[1];
const value = dat[2];
const target = dat[3];
if (table !== "conf_sessions" || !target) return;
const a = this.byId[target];
if (!a) return;
a[column] = value;
if (column === "title" || column === "desc") {
a.searchable = ((a.title || "") + " " + (a.desc || "")).toLowerCase();
}
a.missing = this.computeMissing(a);
};
this.$root.events.bind("changed", this.changedHandler);
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener("keydown", this.onKey); window.removeEventListener("keydown", this.onKey);
if (this.changedHandler && this.$root && this.$root.events && this.$root.events.unbind) {
this.$root.events.unbind("changed", this.changedHandler);
}
}, },
methods: { methods: {
async fetchAll() { async fetchAll() {
@ -1678,6 +1738,13 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
ff = ff.filter((x) => x.searchable.includes(q)); ff = ff.filter((x) => x.searchable.includes(q));
} }
if (this.dayFilter && this.dayFilter !== "all") {
ff = ff.filter((item) => {
const t = dayjs(item.starttime);
return t.isValid() && t.format("YYYY-MM-DD") === this.dayFilter;
});
}
// AY filter example // AY filter example
const ay = this.$root.settings["default_ay"]; const ay = this.$root.settings["default_ay"];
if (ay && this.$root.ay_menu && this.$root.ay_menu[ay]) { if (ay && this.$root.ay_menu && this.$root.ay_menu[ay]) {
@ -1708,7 +1775,7 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
} else if (e.key === "ArrowUp") { } else if (e.key === "ArrowUp") {
this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.selectedIndex = Math.max(0, this.selectedIndex - 1);
e.preventDefault(); e.preventDefault();
} else if (e.key === "Enter" || e.key.toLowerCase() === "e") { } else if (e.key === "Enter") {
const a = this.activitiesFiltered[this.selectedIndex]; const a = this.activitiesFiltered[this.selectedIndex];
this.startEdit(a.id); this.startEdit(a.id);
e.preventDefault(); e.preventDefault();
@ -1745,6 +1812,20 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
activitiesFiltered() { activitiesFiltered() {
return this.filtered(this.activities); return this.filtered(this.activities);
}, },
availableDays() {
const seen = {};
this.activities.forEach((a) => {
const t = dayjs(a.starttime);
if (!t.isValid()) return;
const key = t.format("YYYY-MM-DD");
if (!seen[key]) {
seen[key] = t.format("MMM D YYYY");
}
});
return Object.keys(seen)
.sort()
.map((key) => ({ key, label: seen[key] }));
},
groupedByDay() { groupedByDay() {
return _.groupBy(this.activitiesFiltered, (x) => this.month_year(x.starttime)); return _.groupBy(this.activitiesFiltered, (x) => this.month_year(x.starttime));
}, },
@ -1760,6 +1841,10 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
/> />
<button @click="setsort('starttime')" class="border px-2 py-1 rounded">Sort: Start</button> <button @click="setsort('starttime')" class="border px-2 py-1 rounded">Sort: Start</button>
<button @click="setsort('title')" class="border px-2 py-1 rounded">Sort: Title</button> <button @click="setsort('title')" class="border px-2 py-1 rounded">Sort: Title</button>
<select v-model="dayFilter" class="border px-2 py-1 rounded">
<option value="all">All days</option>
<option v-for="d in availableDays" :key="d.key" :value="d.key">{{ d.label }}</option>
</select>
<span class="text-sm text-gray-600">/ select Enter/E edit Esc close</span> <span class="text-sm text-gray-600">/ select Enter/E edit Esc close</span>
</div> </div>
@ -2129,14 +2214,30 @@ const AskSurvey = Vue.component('asksurvey', {
function(r2) { function(r2) {
self.questions = r2 }) self.questions = r2 })
}, },
saveAll: function() {
var self = this
_.each(self.questions, function(q) {
self.$root.events.trigger('update_survey', [q.user, q.session, q.qid, q.answer])
})
if (typeof alert_message === 'function') {
alert_message('Saved.', 'lightgreen')
}
},
}, },
mounted: function() { this.start() }, mounted: function() { this.start() },
template: `<div> template: `<div class="survey-page bg-white p-4 rounded shadow">
<h3 v-if="questions.length">{{ questions[0].title }}</h3> <div v-if="questions.length" class="mb-4">
<span class="text-gray-600 font-medium">Session name:</span>
<span class="text-gray-900 font-semibold">[{{ questions[0].title }}]</span>
</div>
<div class="session-survey" v-for="q in questions"> <div class="session-survey" v-for="q in questions">
<number-question v-if="q['type']=='2'" :qq="q"></number-question> <number-question v-if="q['type']=='2'" :qq="q"></number-question>
<essay-question v-if="q['type']=='1'" :qq="q"></essay-question> <essay-question v-if="q['type']=='1'" :qq="q"></essay-question>
</div> </div>
<div class="mt-4">
<button class="inline-flex items-center px-3 py-2 text-sm font-semibold uppercase tracking-wide text-white bg-blue-600 rounded hover:bg-blue-700"
@click.prevent="saveAll">Save</button>
</div>
</div>` </div>`
}); });

View File

@ -64,3 +64,23 @@ ol {
.slotpanel-leave { .slotpanel-leave {
max-height: 600px; max-height: 600px;
} }
/* Survey page label tone */
.survey-page .question,
.survey-page label,
.survey-page .form-check-label {
color: #4b5563;
}
/* Survey textarea should span full width */
.survey-page textarea {
width: 100%;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 8px;
box-sizing: border-box;
}
.survey-page .session-survey {
margin-bottom: 12px;
}