Compare commits
No commits in common. "master" and "fa25" have entirely different histories.
|
|
@ -1,7 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
$MY_TITLE = "All Sessions";
|
$MY_TITLE = "Events, Training, & Workshops";
|
||||||
$MY_CRUMB = "All Sessions";
|
$MY_CRUMB = "Activities";
|
||||||
$timeslot_config = file_exists('schedule_timeslots.json') ? json_decode(file_get_contents('schedule_timeslots.json'), true) : [];
|
$CONTENT = '<activitylist :static="0" :itineraryview="false" zoom_on="0" survey_on="0"></activitylist>';
|
||||||
$config_js = '<script>window.TIMESLOT_CONFIG = ' . json_encode($timeslot_config) . ';</script>';
|
|
||||||
$CONTENT = '<div id="timeslot-config" class="hidden"></div><activitylist :itineraryview="0" :static="1" :show_all_sessions="true"></activitylist>';
|
|
||||||
include 'layout.php';
|
include 'layout.php';
|
||||||
|
|
|
||||||
118
allusers.php
118
allusers.php
|
|
@ -1,118 +0,0 @@
|
||||||
<?php
|
|
||||||
// allusers.php - inline editing table for all users
|
|
||||||
include_once("peter_db.php");
|
|
||||||
$peter_db = new peter_db();
|
|
||||||
$c = $peter_db->getConnection();
|
|
||||||
mysqli_set_charset($c, 'utf8');
|
|
||||||
|
|
||||||
// Fetch departments
|
|
||||||
$dept_opts = [];
|
|
||||||
$dept_q = "SELECT id, name FROM gavi_departments ORDER BY name";
|
|
||||||
$dept_r = mysqli_query($c, $dept_q);
|
|
||||||
while ($row = mysqli_fetch_assoc($dept_r)) { $dept_opts[] = $row; }
|
|
||||||
|
|
||||||
$users = [];
|
|
||||||
$q = "SELECT cu.id, cu.name, cu.email, cu.goo, cud.department_id
|
|
||||||
FROM conf_users cu
|
|
||||||
LEFT JOIN conf_user_departments cud ON cud.user_id = cu.id
|
|
||||||
ORDER BY cu.name";
|
|
||||||
$r = mysqli_query($c, $q);
|
|
||||||
while ($row = mysqli_fetch_assoc($r)) { $users[] = $row; }
|
|
||||||
|
|
||||||
// Handle AJAX updates
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$uid = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
|
||||||
$field = isset($_POST['field']) ? $_POST['field'] : '';
|
|
||||||
$value = isset($_POST['value']) ? trim($_POST['value']) : '';
|
|
||||||
|
|
||||||
if ($uid > 0 && in_array($field, ['name','email','goo'])) {
|
|
||||||
$stmt = mysqli_prepare($c, "UPDATE conf_users SET $field = ? WHERE id = ?");
|
|
||||||
mysqli_stmt_bind_param($stmt, "si", $value, $uid);
|
|
||||||
mysqli_stmt_execute($stmt);
|
|
||||||
echo json_encode(['ok' => true]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
if ($uid > 0 && $field === 'department_id') {
|
|
||||||
$dept = intval($value);
|
|
||||||
if ($dept > 0) {
|
|
||||||
$stmt = mysqli_prepare($c, "INSERT INTO conf_user_departments (user_id, department_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE department_id = VALUES(department_id)");
|
|
||||||
mysqli_stmt_bind_param($stmt, "ii", $uid, $dept);
|
|
||||||
mysqli_stmt_execute($stmt);
|
|
||||||
} else {
|
|
||||||
$stmt = mysqli_prepare($c, "DELETE FROM conf_user_departments WHERE user_id = ?");
|
|
||||||
mysqli_stmt_bind_param($stmt, "i", $uid);
|
|
||||||
mysqli_stmt_execute($stmt);
|
|
||||||
}
|
|
||||||
echo json_encode(['ok' => true]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
echo json_encode(['ok' => false, 'err' => 'invalid']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$MY_TITLE = "All Users";
|
|
||||||
$MY_CRUMB = "All Users";
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
?>
|
|
||||||
<div class="bg-white p-4 rounded shadow">
|
|
||||||
<table class="min-w-full text-sm border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr class="border-b">
|
|
||||||
<th class="text-left py-2 pr-3">Name</th>
|
|
||||||
<th class="text-left py-2 pr-3">Email</th>
|
|
||||||
<th class="text-left py-2 pr-3">GOO</th>
|
|
||||||
<th class="text-left py-2 pr-3">Department</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($users as $u) { ?>
|
|
||||||
<tr class="border-b" data-id="<?= $u['id'] ?>">
|
|
||||||
<td contenteditable="true" data-field="name" class="py-1 pr-3"><?= htmlspecialchars($u['name']) ?></td>
|
|
||||||
<td contenteditable="true" data-field="email" class="py-1 pr-3"><?= htmlspecialchars($u['email']) ?></td>
|
|
||||||
<td contenteditable="true" data-field="goo" class="py-1 pr-3"><?= htmlspecialchars($u['goo']) ?></td>
|
|
||||||
<td class="py-1 pr-3">
|
|
||||||
<select data-field="department_id" class="border rounded px-2 py-1">
|
|
||||||
<option value="0">-- None --</option>
|
|
||||||
<?php foreach ($dept_opts as $d) { ?>
|
|
||||||
<option value="<?= $d['id'] ?>" <?= ($u['department_id'] == $d['id']) ? 'selected' : '' ?>><?= htmlspecialchars($d['name']) ?></option>
|
|
||||||
<?php } ?>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php } ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
const rows = document.querySelectorAll('tbody tr');
|
|
||||||
function postUpdate(id, field, value) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('id', id);
|
|
||||||
form.append('field', field);
|
|
||||||
form.append('value', value);
|
|
||||||
fetch(window.location.href, { method: 'POST', body: form })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(j => { if(!j.ok){ alert('Save failed'); } })
|
|
||||||
.catch(() => alert('Save failed'));
|
|
||||||
}
|
|
||||||
rows.forEach(row => {
|
|
||||||
const id = row.getAttribute('data-id');
|
|
||||||
row.querySelectorAll('[contenteditable]').forEach(cell => {
|
|
||||||
cell.addEventListener('blur', () => {
|
|
||||||
postUpdate(id, cell.dataset.field, cell.innerText.trim());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
row.querySelectorAll('select[data-field]').forEach(sel => {
|
|
||||||
sel.addEventListener('change', () => {
|
|
||||||
postUpdate(id, sel.dataset.field, sel.value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
$CONTENT = ob_get_clean();
|
|
||||||
include 'layout.php';
|
|
||||||
|
|
@ -649,9 +649,6 @@ 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); }
|
||||||
|
|
||||||
|
|
|
||||||
1
edit.php
1
edit.php
|
|
@ -3,4 +3,5 @@
|
||||||
$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';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
$MY_TITLE = "Schedule & Sessions";
|
$MY_TITLE = "Itinerary";
|
||||||
$MY_CRUMB = "My Schedule";
|
$MY_CRUMB = "My Schedule";
|
||||||
$timeslot_config = file_exists('schedule_timeslots.json') ? json_decode(file_get_contents('schedule_timeslots.json'), true) : [];
|
$CONTENT = "<activitylist :itineraryview='1'></activitylist>";
|
||||||
$config_js = '<script>window.TIMESLOT_CONFIG = ' . json_encode($timeslot_config) . ';</script>';
|
|
||||||
$CONTENT = '<div id="timeslot-config" class="hidden"></div><activitylist :itineraryview="1" :show_all_sessions="true"></activitylist>';
|
|
||||||
include 'layout.php';
|
include 'layout.php';
|
||||||
|
|
|
||||||
603
js/dir_app.js
603
js/dir_app.js
|
|
@ -28,18 +28,10 @@ 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 }
|
||||||
if (mysqldate instanceof Date) { return mysqldate }
|
var field = mysqldate.match(/^(\d\d\d\d)\-(\d+)\-(\d+)[T|\s](\d+)\:(\d+)\:(\d+)$/)
|
||||||
var s = String(mysqldate).trim()
|
var mydate = new Date(field[1], field[2] - 1 , field[3], field[4], field[5], field[6])
|
||||||
var field = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})[T\s](\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
return mydate }
|
||||||
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)) }
|
||||||
|
|
||||||
|
|
@ -863,34 +855,16 @@ const ActivityEditor = Vue.component('activityedit', {
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// Empty time slot card
|
|
||||||
const EmptySlotCard = Vue.component('emptyslotcard', {
|
|
||||||
props: ['label'],
|
|
||||||
template: `<button
|
|
||||||
class="w-full border-4 border-dashed border-blue-300 bg-blue-50 text-blue-800 rounded-xl py-6 px-4 flex flex-col items-center justify-center gap-2 hover:bg-blue-100 hover:border-blue-400 transition"
|
|
||||||
@click="$emit('select')">
|
|
||||||
<span class="text-4xl font-black leading-none">+</span>
|
|
||||||
<span class="text-lg font-semibold uppercase tracking-wide">{{ label || 'Signup' }}</span>
|
|
||||||
</button>`
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const ActivityList = Vue.component('activitylist', {
|
const ActivityList = Vue.component('activitylist', {
|
||||||
props: [ 'itineraryview','static','show_all_sessions' ],
|
props: [ 'itineraryview','static' ],
|
||||||
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:{},
|
||||||
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:{},
|
|
||||||
surveyAnswersBySession: {}, } },
|
|
||||||
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;
|
||||||
|
|
@ -910,61 +884,13 @@ 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) {
|
addTime: function(time, x) {
|
||||||
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) {
|
|
||||||
if (!starttime) { return null; }
|
|
||||||
return this.$root.$dj(starttime).format('YYYY-MM-DDTHH:mm');
|
|
||||||
},
|
|
||||||
slotSelection: function(slot) {
|
|
||||||
var self = this;
|
|
||||||
var list = this.filtered(this.mysessions, { applySearch: false, applySlot: false });
|
|
||||||
return _.filter(list, function(s) { return self.slotKeyFromStart(s.starttime) === slot.key; });
|
|
||||||
},
|
|
||||||
selectSlot: function(slot) {
|
|
||||||
if (this.selectedSlotKey === slot.key) { this.selectedSlotKey = null; return; }
|
|
||||||
this.selectedSlotKey = slot.key;
|
|
||||||
this.search = '';
|
|
||||||
},
|
|
||||||
clearSlot: function() { this.selectedSlotKey = null; },
|
|
||||||
addTime: function(time, x) {
|
|
||||||
x = parseInt(x)
|
x = parseInt(x)
|
||||||
let [y, m, d, h, i, s] = time.split(/[- :]/); // split time into parts
|
let [y, m, d, h, i, s] = time.split(/[- :]/); // split time into parts
|
||||||
let dt = new Date(y, m-1, d, h, i, s); // create Date object
|
let dt = new Date(y, m-1, d, h, i, s); // create Date object
|
||||||
|
|
@ -978,10 +904,8 @@ const ActivityList = Vue.component('activitylist', {
|
||||||
mode_string: function(a) { if (this.$root.active) { return _.findWhere(this.$root.modes_menu, { 'id': a.mode })['string'] } return a.mode },
|
mode_string: function(a) { if (this.$root.active) { return _.findWhere(this.$root.modes_menu, { 'id': a.mode })['string'] } return a.mode },
|
||||||
get_day_title: function(day) {
|
get_day_title: function(day) {
|
||||||
var d = dayjs(day, 'MMM DD YYYY')
|
var d = dayjs(day, 'MMM DD YYYY')
|
||||||
var convertedDateString = d.format('YYYY-MM-DD')
|
convertedDateString = d.format('YYYY-MM-DD')
|
||||||
var found = _.findWhere( this.conference, {date1:convertedDateString} )
|
return _.findWhere( this.conference, {date1:convertedDateString} ).title
|
||||||
if (found && found.title) { return found.title }
|
|
||||||
return ''
|
|
||||||
},
|
},
|
||||||
month_year: function(d) { var b = this.$root.$dj(d).format('MMM D YYYY'); return b },
|
month_year: function(d) { var b = this.$root.$dj(d).format('MMM D YYYY'); return b },
|
||||||
setsort: function(ss) {
|
setsort: function(ss) {
|
||||||
|
|
@ -992,144 +916,49 @@ 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;
|
||||||
var hostUrl = 'dir_api.php?a=get/hosts';
|
basic_get('dir_api.php?a=get/hosts', function(r2) {
|
||||||
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
|
||||||
self.my_ses_ids = _.map(_.pluck(r2.mysessions, 'id'), function(val) {
|
self.my_ses_ids = _.pluck(r2.mysessions, 'id')
|
||||||
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 = [] }
|
||||||
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 )
|
||||||
self.active = 1
|
self.active = 1
|
||||||
self.loadTimeslots()
|
|
||||||
self.$forceUpdate();
|
self.$forceUpdate();
|
||||||
|
|
||||||
} )
|
} )
|
||||||
},
|
},
|
||||||
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) {
|
||||||
if (!self.my_ses_ids.includes(id)) { self.my_ses_ids.push(id) }
|
self.mysessions.push(_.findWhere(self.activities, {'id':id}))
|
||||||
var existing = _.find(self.mysessions, function(s) {
|
self.my_ses_ids.push(id)
|
||||||
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) }
|
|
||||||
basic_get('dir_api.php?a=signdown/' + id,
|
basic_get('dir_api.php?a=signdown/' + id,
|
||||||
function(r2) {
|
function(r2) {
|
||||||
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.$forceUpdate()
|
||||||
});
|
alert_message("Removed activity") })
|
||||||
self.my_ses_ids = _.filter(self.my_ses_ids, function(sid) {
|
|
||||||
return self.normalizeId(sid) !== id;
|
|
||||||
});
|
|
||||||
self.cancelingIds = _.without(self.cancelingIds, id)
|
|
||||||
self.$forceUpdate()
|
|
||||||
alert_message("Removed activity")
|
|
||||||
}, 250)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
sessionsForSlot: function(slot) {
|
filtered: function(ff) {
|
||||||
var self=this;
|
|
||||||
if (slot.presetSessions && slot.presetSessions.length) { return []; }
|
|
||||||
return _.chain(this.activities_for_slots)
|
|
||||||
.filter(function(item) { return self.slotKeyFromStart(item.starttime) === slot.key; })
|
|
||||||
.sortBy('starttime')
|
|
||||||
.value();
|
|
||||||
},
|
|
||||||
chooseSession: function(activity) {
|
|
||||||
var self=this;
|
|
||||||
if (!self.my_ses_ids.includes(activity.id)) {
|
|
||||||
self.joinme(activity.id);
|
|
||||||
}
|
|
||||||
self.clearSlot();
|
|
||||||
},
|
|
||||||
slotPresets: function(slot) { return slot.presetSessions || []; },
|
|
||||||
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) {
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
hasSurveyAnswer: function(activity) {
|
|
||||||
if (!activity) { return false; }
|
|
||||||
var sid = this.normalizeId(activity.id);
|
|
||||||
return !!this.surveyAnswersBySession[sid];
|
|
||||||
},
|
|
||||||
hasJoinLink: function(activity) {
|
|
||||||
if (!activity || !activity.location) { return false; }
|
|
||||||
return String(activity.location).trim().length > 0;
|
|
||||||
},
|
|
||||||
loadTimeslots: function() {
|
|
||||||
var self=this;
|
|
||||||
if (window.TIMESLOT_CONFIG) {
|
|
||||||
self.timeslotConfig = window.TIMESLOT_CONFIG;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetch('schedule_timeslots.json').then(function(resp) {
|
|
||||||
if (!resp.ok) { throw new Error('timeslot fetch'); }
|
|
||||||
return resp.json();
|
|
||||||
}).then(function(json) {
|
|
||||||
self.timeslotConfig = json;
|
|
||||||
}).catch(function(e) {
|
|
||||||
console.warn('No schedule_timeslots.json found or unreadable', e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isCanceling: function(id) { return this.cancelingIds.includes(id) },
|
|
||||||
filtered: function(ff, opts = { applySearch: true, applySlot: false }) {
|
|
||||||
var self = this
|
var self = this
|
||||||
var applySearch = (opts.applySearch !== false)
|
if (this.search) {
|
||||||
var applySlot = (opts.applySlot !== false)
|
|
||||||
if (applySearch && this.search) {
|
|
||||||
var ss = self.search.toLowerCase()
|
var ss = self.search.toLowerCase()
|
||||||
ff = ff.filter(function(x) { return ('searchable' in x ? x.searchable.includes(ss) : 0) })
|
ff = ff.filter(function(x) { return ('searchable' in x ? x.searchable.includes(ss) : 0) })
|
||||||
}
|
}
|
||||||
|
|
@ -1142,9 +971,6 @@ const ActivityList = Vue.component('activitylist', {
|
||||||
ff = ff.filter( function(item,index) {
|
ff = ff.filter( function(item,index) {
|
||||||
this_time = dayjs(item.starttime)
|
this_time = dayjs(item.starttime)
|
||||||
return this_time.isBefore(end) && start.isBefore(this_time) } )
|
return this_time.isBefore(end) && start.isBefore(this_time) } )
|
||||||
if (this.selectedSlotKey && applySlot) {
|
|
||||||
ff = ff.filter(function(item) { return self.slotKeyFromStart(item.starttime) === self.selectedSlotKey })
|
|
||||||
}
|
|
||||||
ff = _.sortBy(ff, function(x) {
|
ff = _.sortBy(ff, function(x) {
|
||||||
if (x[self.sortby]) { var s = x[self.sortby]; return s.trim().toLowerCase() }
|
if (x[self.sortby]) { var s = x[self.sortby]; return s.trim().toLowerCase() }
|
||||||
return '' })
|
return '' })
|
||||||
|
|
@ -1158,58 +984,7 @@ const ActivityList = Vue.component('activitylist', {
|
||||||
current_time: function() { return dayjs().format('MMM D, h:mma') },
|
current_time: function() { return dayjs().format('MMM D, h:mma') },
|
||||||
activities_filtered: function() { var a = this.filtered(this.activities); return a; },
|
activities_filtered: function() { var a = this.filtered(this.activities); return a; },
|
||||||
mysessions_filtered: function() { return this.filtered(this.mysessions) },
|
mysessions_filtered: function() { return this.filtered(this.mysessions) },
|
||||||
activities_for_slots: function() { return this.filtered(this.activities, { applySearch: false, applySlot: false }) },
|
|
||||||
timeSlotsByDay: function() {
|
|
||||||
if (this.active < 1) { return {}; }
|
|
||||||
var self=this;
|
|
||||||
if (this.timeslotConfig && this.timeslotConfig.versions && this.timeslotConfig.days) {
|
|
||||||
var res = {};
|
|
||||||
_.each(this.timeslotConfig.days, function(versionKey, dayStr) {
|
|
||||||
var version = self.timeslotConfig.versions[versionKey];
|
|
||||||
if (!version || !version.slots) { return; }
|
|
||||||
var dayLabel = dayjs(dayStr).format('MMM D YYYY');
|
|
||||||
res[dayLabel] = _.map(version.slots, function(slot) {
|
|
||||||
var start = dayjs(dayStr + ' ' + slot.start);
|
|
||||||
var end = dayjs(dayStr + ' ' + slot.end);
|
|
||||||
return {
|
|
||||||
key: dayStr + 'T' + slot.start,
|
|
||||||
startLabel: start.isValid() ? start.format('h:mma') : slot.start,
|
|
||||||
endLabel: end.isValid() ? end.format('h:mma') : slot.end,
|
|
||||||
dayLabel: dayLabel,
|
|
||||||
presetSessions: slot.sessions || [],
|
|
||||||
version: versionKey,
|
|
||||||
versionLabel: version.label || versionKey
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
var grouped = _.groupBy(this.activities_for_slots, function(x) { return self.month_year(x.starttime) });
|
|
||||||
return _.mapObject(grouped, function(list) {
|
|
||||||
return _.chain(list)
|
|
||||||
.sortBy('starttime')
|
|
||||||
.map(function(item) {
|
|
||||||
var start = self.$root.$dj(item.starttime);
|
|
||||||
return {
|
|
||||||
key: self.slotKeyFromStart(item.starttime),
|
|
||||||
startLabel: start.format('h:mma'),
|
|
||||||
endLabel: self.addTime(item.starttime, item.length),
|
|
||||||
dayLabel: self.month_year(item.starttime),
|
|
||||||
presetSessions: [],
|
|
||||||
version: null,
|
|
||||||
versionLabel: null,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.uniq(false, function(slot) { return slot.key; })
|
|
||||||
.value();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selectedSlotLabel: function() {
|
|
||||||
if (!this.selectedSlotKey) { return '' }
|
|
||||||
var m = dayjs(this.selectedSlotKey);
|
|
||||||
if (!m.isValid()) { return '' }
|
|
||||||
return m.format('MMM D, h:mma');
|
|
||||||
},
|
|
||||||
activities_g_filtered: function() { if (this.active<1) { return {} }
|
activities_g_filtered: function() { if (this.active<1) { return {} }
|
||||||
var self=this;
|
var self=this;
|
||||||
return _.groupBy(self.activities_filtered, function(x) { return self.month_year(x.starttime) } ); },
|
return _.groupBy(self.activities_filtered, function(x) { return self.month_year(x.starttime) } ); },
|
||||||
|
|
@ -1228,170 +1003,136 @@ const ActivityList = Vue.component('activitylist', {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Time slots view -->
|
|
||||||
<div v-if="itineraryview && active > 0" class="mb-8">
|
|
||||||
<div v-if="Object.keys(timeSlotsByDay).length === 0" class="bg-white border rounded p-4 text-sm text-gray-700">
|
|
||||||
No session times are available yet.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-for="(slots, mmyy) in timeSlotsByDay" :key="mmyy" class="mb-6">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<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 class="w-32 text-right">
|
|
||||||
<div class="text-2xl font-extrabold text-blue-800 leading-tight">{{ slot.startLabel }}</div>
|
|
||||||
<div class="text-sm text-gray-500">to {{ slot.endLabel }}</div>
|
|
||||||
<span class="block text-xs text-gray-500 mt-1" v-if="selectedSlotKey === slot.key">Selected</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<div v-if="slotHasPreset(slot)" class="space-y-2">
|
|
||||||
<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="text-sm text-gray-600">
|
|
||||||
<span v-if="sel.audience" class="capitalize">{{ sel.audience }} meeting</span>
|
|
||||||
<span v-if="sel.location">at {{ sel.location }}</span>
|
|
||||||
<span v-if="sel.mode">· {{ sel.mode }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="sel.notes" class="text-xs text-gray-500 mt-1" v-html="nl2br(sel.notes)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="slotSelection(slot).length" class="space-y-2">
|
|
||||||
<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="text-sm text-gray-600">
|
|
||||||
<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 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 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)"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="mt-1 text-sm text-blue-600 hover:underline" @click="selectSlot(slot)">Change session</button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="mt-1">
|
|
||||||
<emptyslotcard label="Signup" @select="selectSlot(slot)"></emptyslotcard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="slotpanel">
|
|
||||||
<div v-if="selectedSlotKey === slot.key && !slotHasPreset(slot)" class="mt-4 border rounded-lg bg-blue-50 border-blue-200 p-4 shadow-inner">
|
|
||||||
<div class="flex items-start justify-between gap-3 mb-3">
|
|
||||||
<div>
|
|
||||||
<div class="text-sm text-gray-600">Pick for {{ slot.startLabel }} - {{ slot.endLabel }}</div>
|
|
||||||
<div class="text-base font-semibold text-gray-800">Available sessions</div>
|
|
||||||
</div>
|
|
||||||
<button class="text-sm text-gray-600 hover:text-gray-800" @click="clearSlot">X</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="sessionsForSlot(slot).length" class="space-y-3 max-h-72 overflow-y-auto pr-1">
|
|
||||||
<div v-for="a in sessionsForSlot(slot)" :key="a.id" class="bg-white rounded border border-gray-200 p-3 shadow-sm">
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
|
||||||
<div class="flex-1">
|
|
||||||
<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-sm text-gray-600 mt-1">
|
|
||||||
<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<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>
|
|
||||||
</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 class="flex items-center gap-2">
|
|
||||||
<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"
|
|
||||||
@click.prevent="chooseSession(a)">
|
|
||||||
Sign Up
|
|
||||||
</button>
|
|
||||||
<span v-else class="text-sm text-green-700 font-semibold">In itinerary</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-sm text-gray-700">No sessions are available in this time slot.</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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>
|
<p class="font-semibold text-gray-800">It looks like you haven't signed up for any sessions yet!</p>
|
||||||
<template v-else>
|
<p class="mt-2 text-sm text-blue-600">
|
||||||
<p class="font-semibold text-gray-800">It looks like you haven't signed up for any sessions yet!</p>
|
Go to the <a class="underline hover:text-blue-800" href="allsessions.php">Sessions List</a> to sign up.
|
||||||
<p class="mt-2 text-sm text-blue-600">
|
</p>
|
||||||
Go to the <a class="underline hover:text-blue-800" href="allsessions.php">Sessions List</a> to sign up.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Simple full list for All Sessions -->
|
<!-- Grouped sessions by date -->
|
||||||
<div v-if="!itineraryview && active > 0" class="space-y-6">
|
<div v-if="itineraryview && active > 0" v-for="(items, mmyy) in mysessions_g_filtered" :key="mmyy" class="mb-8">
|
||||||
<div v-for="(items, mmyy) in activities_g_filtered" :key="mmyy">
|
<h3 class="text-xl font-bold text-gray-800 mb-2">{{ mmyy }} {{ get_day_title(mmyy) }}</h3>
|
||||||
<h3 class="text-xl font-semibold text-gray-800 mb-2">{{ mmyy }} {{ get_day_title(mmyy) }}</h3>
|
|
||||||
<div class="space-y-3">
|
<div v-for="a in items" :key="a.id" class="bg-white rounded-lg shadow p-4 mb-6 border-l-4 border-blue-200">
|
||||||
<div v-for="a in items" :key="a.id" class="bg-white border rounded-lg p-4 shadow-sm">
|
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
|
||||||
<div class="flex justify-between items-start gap-3">
|
|
||||||
<div>
|
<!-- Left side controls -->
|
||||||
<div class="text-sm text-gray-500">{{ $root.$dj(a.starttime).format('h:mma') }} - {{ addTime(a.starttime, a.length) }}</div>
|
<div class="flex flex-wrap gap-2 md:flex-col md:min-w-[100px]">
|
||||||
<h4 class="text-lg font-semibold text-gray-900">{{ a.title }}</h4>
|
<a v-if="my_host_ids.includes(a.id)"
|
||||||
<div class="text-sm text-gray-600">
|
:href="'ed_act.php?w=' + a.id"
|
||||||
<span v-if="hoststr(a.id)">{{ hoststr(a.id) }}</span>
|
class="text-sm text-blue-600 hover:underline">Edit</a>
|
||||||
<i v-else>no host registered</i>
|
|
||||||
</div>
|
<a v-if="my_host_ids.includes(a.id)"
|
||||||
<p class="text-sm text-gray-700 mt-1" v-html="a.desc"></p>
|
:href="'report.php?s=' + a.id"
|
||||||
<p class="text-xs uppercase text-gray-500 mt-2">{{ mode_string(a) }}</p>
|
class="text-sm text-blue-600 hover:underline">Info</a>
|
||||||
<p class="text-sm text-gray-600" v-if="a.location_irl">Location: {{ a.location_irl }}</p>
|
</div>
|
||||||
<p class="text-sm text-blue-600" v-if="a.location">Link: <a :href="a.location" class="underline">{{ a.location }}</a></p>
|
|
||||||
</div>
|
<!-- Session info -->
|
||||||
|
<div class="flex-grow relative">
|
||||||
|
<h2 class="text-base font-semibold text-gray-800">{{ a.title }}</h2>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
{{ $root.$dj(a.starttime).format('h:mma') }} - {{ addTime(a.starttime, a.length) }} <!--· {{ mode_string(a) }} -->
|
||||||
|
<span v-if="a.mode === 'hybrid'"> · In person at {{ a.location_irl }} or
|
||||||
|
<a v-if="zoom_on && a.location" :href="a.location" class="underline text-blue-600">online</a>
|
||||||
|
<span v-else>online</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="a.mode === 'inperson'"> · In person at {{ a.location_irl }}</span>
|
||||||
|
<span v-if="zoom_on && a.mode === 'online'"> · <a :href="a.location" class="underline text-blue-600">Online</a></span>
|
||||||
|
<span v-if="!zoom_on && a.mode === 'online'"> · Online</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Bottom action buttons -->
|
||||||
|
<div class="mt-4 flex gap-3 flex-wrap">
|
||||||
|
<a v-if="zoom_on && a.location"
|
||||||
|
:href="a.location"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded hover:bg-blue-700">
|
||||||
|
Join Zoom
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a v-if="survey_on"
|
||||||
|
:href="'survey.php?s=' + a.id"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700">
|
||||||
|
Survey
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="!itineraryview && active > 0" v-for="(items, mmyy) in activities_g_filtered" :key="mmyy" class="mb-6">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-baseline text-xl font-semibold text-gray-800 border-b pb-1 mb-2 clickme"
|
||||||
|
@click="toggleDay(get_day_index(mmyy))">
|
||||||
|
{{ mmyy }} {{ get_day_title(mmyy) }}
|
||||||
|
<button
|
||||||
|
:id="get_day_index(mmyy) + '-toggle'"
|
||||||
|
class="text-sm text-blue-600 hover:underline font-normal"
|
||||||
|
>
|
||||||
|
Collapse Day
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- all sessions in a day -->
|
||||||
|
<div :id="get_day_index(mmyy)">
|
||||||
|
<div v-for="a in items" :key="a.id" class="relative bg-white rounded-lg shadow p-4 mb-4">
|
||||||
|
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
|
||||||
|
<!-- Action buttons (left on desktop) -->
|
||||||
|
<div v-if="!static" class="flex-shrink-0 flex gap-2">
|
||||||
|
<a v-if="my_ses_ids.includes(a.id) && !my_host_ids.includes(a.id)"
|
||||||
|
@click.prevent="dumpme(a.id)"
|
||||||
|
href="#"
|
||||||
|
class="px-3 py-1 text-sm font-medium rounded text-sm text-white bg-red-600 hover:underline">Cancel</a>
|
||||||
|
|
||||||
|
<a v-if="!special_signup(a.id) && !my_ses_ids.includes(a.id) && !my_host_ids.includes(a.id)"
|
||||||
|
@click.prevent="joinme(a.id)"
|
||||||
|
href="#"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700">Sign Up</a>
|
||||||
|
|
||||||
|
<!--<a v-if="special_signup(a.id)"
|
||||||
|
target="_blank"
|
||||||
|
href="https://forms.office.com/r/GGz56DdSEG"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-white bg-amber-500 rounded hover:bg-amber-700">Sign Up</a>
|
||||||
|
-->
|
||||||
|
<span v-if="special_signup(a.id)"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-white">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a v-if="my_host_ids.includes(a.id)"
|
||||||
|
:href="'ed_act.php?w=' + a.id"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700">Edit</a>
|
||||||
|
<b v-if="my_host_ids.includes(a.id)">You are Host.</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main session info -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">{{ a.title }}</h2>
|
||||||
|
<ModePill :mode="a.mode" class="absolute top-2 right-2" />
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
{{ $root.$dj(a.starttime).format('h:mma') }} - {{ addTime(a.starttime, a.length) }} ·
|
||||||
|
{{ mode_string(a) }}
|
||||||
|
<span v-if="a.mode == 'hybrid' || a.mode == 'inperson'"> · {{ a.location_irl }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="hoststr(a.id)" class="text-sm text-gray-600">Presented by: {{ hoststr(a.id) }}</p>
|
||||||
|
<p v-if="static && a.location_irl" class="text-sm text-gray-600">Location: {{ a.location_irl }}</p>
|
||||||
|
<p v-if="static && a.location" class="text-sm text-blue-600">
|
||||||
|
Zoom Link:
|
||||||
|
<a :href="a.location" class="underline hover:text-blue-800">{{ a.location }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="expanded" class="mt-2 text-sm text-gray-700" v-html="a.desc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>` })
|
</div>` })
|
||||||
|
|
||||||
|
|
@ -1595,7 +1336,6 @@ 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: {
|
||||||
|
|
@ -1619,27 +1359,9 @@ 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() {
|
||||||
|
|
@ -1738,13 +1460,6 @@ 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]) {
|
||||||
|
|
@ -1775,7 +1490,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") {
|
} else if (e.key === "Enter" || e.key.toLowerCase() === "e") {
|
||||||
const a = this.activitiesFiltered[this.selectedIndex];
|
const a = this.activitiesFiltered[this.selectedIndex];
|
||||||
this.startEdit(a.id);
|
this.startEdit(a.id);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -1812,20 +1527,6 @@ 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));
|
||||||
},
|
},
|
||||||
|
|
@ -1841,10 +1542,6 @@ 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>
|
||||||
|
|
||||||
|
|
@ -2214,30 +1911,14 @@ 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 class="survey-page bg-white p-4 rounded shadow">
|
template: `<div>
|
||||||
<div v-if="questions.length" class="mb-4">
|
<h3 v-if="questions.length">{{ questions[0].title }}</h3>
|
||||||
<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>`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ require_once('semester.php');
|
||||||
if (!isset($MY_TITLE)) $MY_TITLE = "Untitled Page";
|
if (!isset($MY_TITLE)) $MY_TITLE = "Untitled Page";
|
||||||
if (!isset($MY_CRUMB)) $MY_CRUMB = $MY_TITLE;
|
if (!isset($MY_CRUMB)) $MY_CRUMB = $MY_TITLE;
|
||||||
if (!isset($CONTENT)) $CONTENT = "<p>No content provided.</p>";
|
if (!isset($CONTENT)) $CONTENT = "<p>No content provided.</p>";
|
||||||
$DIR_APP_VER = file_exists('js/dir_app.js') ? filemtime('js/dir_app.js') : time();
|
|
||||||
|
|
||||||
$MY_PATH = $_SERVER['PHP_SELF'];
|
$MY_PATH = $_SERVER['PHP_SELF'];
|
||||||
$MOD_DATE = file_exists(__FILE__) ? date("F d Y H:i.", filemtime(__FILE__)) : "(unknown)";
|
$MOD_DATE = file_exists(__FILE__) ? date("F d Y H:i.", filemtime(__FILE__)) : "(unknown)";
|
||||||
|
|
@ -25,8 +24,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; } ?>
|
|
||||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 min-h-screen">
|
<body class="bg-gray-100 min-h-screen">
|
||||||
|
|
@ -74,8 +72,8 @@ $MOD_DATE = file_exists(__FILE__) ? date("F d Y H:i.", filemtime(__FILE__)) : "(
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<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"></script>
|
||||||
<script src="<?= $XTRAJS ?>"></script>
|
<script src="<?= $XTRAJS ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
251
ops.py
251
ops.py
|
|
@ -1,251 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import datetime as dt
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import html
|
|
||||||
import unicodedata
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pymysql
|
|
||||||
except ImportError: # pragma: no cover - depends on local env
|
|
||||||
pymysql = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
except ImportError: # pragma: no cover - depends on local env
|
|
||||||
BeautifulSoup = None
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_FILE = "peter_db.php"
|
|
||||||
|
|
||||||
|
|
||||||
def load_db_config():
|
|
||||||
if not os.path.exists(CONFIG_FILE):
|
|
||||||
raise FileNotFoundError(f"Missing {CONFIG_FILE}")
|
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
|
|
||||||
text = fh.read()
|
|
||||||
def pick(key):
|
|
||||||
match = re.search(rf"\\$this->{key}\\s*=\\s*'([^']*)'", text)
|
|
||||||
return match.group(1) if match else None
|
|
||||||
cfg = {
|
|
||||||
"host": pick("DBServer"),
|
|
||||||
"user": pick("DBUser"),
|
|
||||||
"password": pick("DBPass"),
|
|
||||||
"database": pick("DBName"),
|
|
||||||
}
|
|
||||||
missing = [k for k, v in cfg.items() if not v]
|
|
||||||
if missing:
|
|
||||||
raise ValueError(f"Missing DB config values: {', '.join(missing)}")
|
|
||||||
return cfg
|
|
||||||
|
|
||||||
|
|
||||||
def get_conn():
|
|
||||||
from secretdir import dbhost, dbuser,dbpassword
|
|
||||||
if pymysql is None:
|
|
||||||
raise RuntimeError("pymysql is not installed. Install it with: pip install pymysql")
|
|
||||||
#cfg = load_db_config()
|
|
||||||
return pymysql.connect(
|
|
||||||
host=dbhost,
|
|
||||||
user=dbuser,
|
|
||||||
password=dbpassword,
|
|
||||||
database="db",
|
|
||||||
charset="utf8mb4",
|
|
||||||
cursorclass=pymysql.cursors.DictCursor,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_day(input_str):
|
|
||||||
input_str = input_str.strip()
|
|
||||||
for fmt in ("%m/%d/%y", "%m/%d/%Y", "%Y-%m-%d"):
|
|
||||||
try:
|
|
||||||
d = dt.datetime.strptime(input_str, fmt).date()
|
|
||||||
if d.year < 100:
|
|
||||||
d = d.replace(year=2000 + d.year)
|
|
||||||
return d
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
raise ValueError("Expected a date like 1/22/26 or 2026-01-22")
|
|
||||||
|
|
||||||
|
|
||||||
def json_filename(day):
|
|
||||||
return f"sessions_{day.strftime('%Y-%m-%d')}.json"
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_value(val):
|
|
||||||
if isinstance(val, (dt.datetime, dt.date)):
|
|
||||||
if isinstance(val, dt.date) and not isinstance(val, dt.datetime):
|
|
||||||
return val.strftime("%Y-%m-%d")
|
|
||||||
return val.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
def sql_literal(val):
|
|
||||||
if val is None:
|
|
||||||
return "NULL"
|
|
||||||
if isinstance(val, (dt.datetime, dt.date)):
|
|
||||||
if isinstance(val, dt.date) and not isinstance(val, dt.datetime):
|
|
||||||
return f"'{val.strftime('%Y-%m-%d')}'"
|
|
||||||
return f"'{val.strftime('%Y-%m-%d %H:%M:%S')}'"
|
|
||||||
if isinstance(val, (int, float)) and not isinstance(val, bool):
|
|
||||||
return str(val)
|
|
||||||
text = str(val)
|
|
||||||
if pymysql is not None:
|
|
||||||
text = pymysql.converters.escape_string(text)
|
|
||||||
return "'" + text + "'"
|
|
||||||
|
|
||||||
|
|
||||||
def clean_desc_for_export(text):
|
|
||||||
if text is None:
|
|
||||||
return None
|
|
||||||
if BeautifulSoup is None:
|
|
||||||
raise RuntimeError("beautifulsoup4 is not installed. Install it with: pip install beautifulsoup4")
|
|
||||||
allowed = ["b", "i", "em", "strong", "ul", "ol", "li", "p", "br"]
|
|
||||||
s = unicodedata.normalize("NFKD", str(text))
|
|
||||||
# Remove script/style blocks entirely.
|
|
||||||
s = re.sub(r"(?is)<(script|style)[^>]*>.*?</\\1>", "", s)
|
|
||||||
soup = BeautifulSoup(s, "html.parser")
|
|
||||||
# Drop disallowed tags, keep allowed tags without attributes.
|
|
||||||
for tag in soup.find_all(True):
|
|
||||||
if tag.name not in allowed:
|
|
||||||
tag.unwrap()
|
|
||||||
else:
|
|
||||||
tag.attrs = {}
|
|
||||||
s = "".join(str(x) for x in soup.contents)
|
|
||||||
s = html.unescape(s)
|
|
||||||
s = s.replace("\u00a0", " ")
|
|
||||||
s = s.replace("\u200b", "")
|
|
||||||
'''s = re.sub(r"[ \\t\\f\\v]+", " ", s)
|
|
||||||
s = re.sub(r"\\s*<br>\\s*", "<br>", s)
|
|
||||||
s = re.sub(r"(?:<br>\\s*){3,}", "<br><br>", s)
|
|
||||||
s = re.sub(r"\\s*</p>\\s*", "</p>", s)
|
|
||||||
s = re.sub(r"\\s*<p>\\s*", "<p>", s)'''
|
|
||||||
return s.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def db_dump_day():
|
|
||||||
day = parse_day(input("Enter day (e.g., 1/22/26): "))
|
|
||||||
fname = json_filename(day)
|
|
||||||
conn = get_conn()
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT * FROM conf_sessions WHERE DATE(starttime) = %s ORDER BY starttime, id",
|
|
||||||
(day.strftime("%Y-%m-%d"),),
|
|
||||||
)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
with open(fname, "w", encoding="utf-8") as fh:
|
|
||||||
json.dump(rows, fh, indent=2, sort_keys=True, default=str)
|
|
||||||
print(f"Wrote {len(rows)} session(s) to {fname}")
|
|
||||||
|
|
||||||
|
|
||||||
def db_reload_day_json():
|
|
||||||
day = parse_day(input("Enter day (e.g., 1/22/26): "))
|
|
||||||
fname = json_filename(day)
|
|
||||||
if not os.path.exists(fname):
|
|
||||||
raise FileNotFoundError(f"Missing {fname}")
|
|
||||||
with open(fname, "r", encoding="utf-8") as fh:
|
|
||||||
rows = json.load(fh)
|
|
||||||
if not isinstance(rows, list):
|
|
||||||
raise ValueError("JSON file must be a list of session rows")
|
|
||||||
conn = get_conn()
|
|
||||||
updated = 0
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
for row in rows:
|
|
||||||
if "id" not in row:
|
|
||||||
continue
|
|
||||||
ses_id = row["id"]
|
|
||||||
cur.execute("SELECT * FROM conf_sessions WHERE id = %s", (ses_id,))
|
|
||||||
current = cur.fetchone()
|
|
||||||
if not current:
|
|
||||||
print(f"Skipping missing id {ses_id}")
|
|
||||||
continue
|
|
||||||
changes = {}
|
|
||||||
for key, new_val in row.items():
|
|
||||||
if key == "id" or key not in current:
|
|
||||||
continue
|
|
||||||
cur_val = current[key]
|
|
||||||
norm_new = normalize_value(new_val)
|
|
||||||
norm_cur = normalize_value(cur_val)
|
|
||||||
if norm_new != norm_cur:
|
|
||||||
changes[key] = new_val
|
|
||||||
if changes:
|
|
||||||
set_sql = ", ".join([f"`{k}`=%s" for k in changes.keys()])
|
|
||||||
params = list(changes.values()) + [ses_id]
|
|
||||||
cur.execute(f"UPDATE conf_sessions SET {set_sql} WHERE id=%s", params)
|
|
||||||
updated += 1
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
print(f"Updated {updated} session(s) from {fname}")
|
|
||||||
|
|
||||||
|
|
||||||
def db_export_sessions_sql():
|
|
||||||
if pymysql is None:
|
|
||||||
raise RuntimeError("pymysql is not installed. Install it with: pip install pymysql")
|
|
||||||
conn = get_conn()
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT * FROM conf_sessions WHERE id > 1462 ORDER BY id")
|
|
||||||
rows = cur.fetchall()
|
|
||||||
columns = [col[0] for col in cur.description]
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
if not rows:
|
|
||||||
print("No sessions found with id > 1462")
|
|
||||||
return
|
|
||||||
if "id" in columns:
|
|
||||||
columns = [c for c in columns if c != "id"]
|
|
||||||
os.makedirs("data", exist_ok=True)
|
|
||||||
out_path = os.path.join("data", "sessions.sql")
|
|
||||||
with open(out_path, "w", encoding="utf-8") as fh:
|
|
||||||
for row in rows:
|
|
||||||
values = []
|
|
||||||
for col in columns:
|
|
||||||
val = row.get(col)
|
|
||||||
if col == "desc":
|
|
||||||
val = clean_desc_for_export(val)
|
|
||||||
values.append(sql_literal(val))
|
|
||||||
col_sql = ", ".join([f"`{c}`" for c in columns])
|
|
||||||
val_sql = ", ".join(values)
|
|
||||||
fh.write(f"INSERT INTO conf_sessions ({col_sql}) VALUES ({val_sql});\n")
|
|
||||||
print(f"Wrote {len(rows)} INSERT statements to {out_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
actions = [
|
|
||||||
("Dump day's sessions from db", db_dump_day),
|
|
||||||
("Reload day's session from json", db_reload_day_json),
|
|
||||||
("Export sessions id>1462 to SQL inserts", db_export_sessions_sql),
|
|
||||||
("Reload ops.py", "__RELOAD__"),
|
|
||||||
("Quit", None),
|
|
||||||
]
|
|
||||||
while True:
|
|
||||||
print("\nOps Menu")
|
|
||||||
for i, (label, _) in enumerate(actions, 1):
|
|
||||||
print(f"{i}. {label}")
|
|
||||||
choice = input("Choose an option: ").strip()
|
|
||||||
if not choice.isdigit():
|
|
||||||
print("Please enter a number.")
|
|
||||||
continue
|
|
||||||
idx = int(choice) - 1
|
|
||||||
if idx < 0 or idx >= len(actions):
|
|
||||||
print("Invalid choice.")
|
|
||||||
continue
|
|
||||||
label, action = actions[idx]
|
|
||||||
if action is None:
|
|
||||||
return
|
|
||||||
if action == "__RELOAD__":
|
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
||||||
try:
|
|
||||||
action()
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"Error: {exc}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
36
style.css
36
style.css
|
|
@ -48,39 +48,3 @@ ol {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-all; /* only affects long unbroken tokens like URLs */
|
word-break: break-all; /* only affects long unbroken tokens like URLs */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slot picker slide/fade */
|
|
||||||
.slotpanel-enter-active,
|
|
||||||
.slotpanel-leave-active {
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
}
|
|
||||||
.slotpanel-enter,
|
|
||||||
.slotpanel-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-6px);
|
|
||||||
max-height: 0;
|
|
||||||
}
|
|
||||||
.slotpanel-enter-to,
|
|
||||||
.slotpanel-leave {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
115
user.php
115
user.php
|
|
@ -1,115 +0,0 @@
|
||||||
<?php
|
|
||||||
// user.php - edit a single user (conf_users) and their department mapping (conf_user_departments)
|
|
||||||
|
|
||||||
include_once("peter_db.php");
|
|
||||||
$peter_db = new peter_db();
|
|
||||||
$c = $peter_db->getConnection();
|
|
||||||
mysqli_set_charset($c, 'utf8');
|
|
||||||
|
|
||||||
$user_id = isset($_REQUEST['id']) ? intval($_REQUEST['id']) : 0;
|
|
||||||
$message = '';
|
|
||||||
|
|
||||||
// Fetch department options
|
|
||||||
$dept_opts = [];
|
|
||||||
$dept_q = "SELECT id, parent, name FROM gavi_departments ORDER BY name";
|
|
||||||
$dept_r = mysqli_query($c, $dept_q);
|
|
||||||
while ($row = mysqli_fetch_assoc($dept_r)) { $dept_opts[] = $row; }
|
|
||||||
|
|
||||||
// Helper: fetch user record
|
|
||||||
function fetch_user($c, $uid) {
|
|
||||||
$sql = "SELECT id, goo, email, name FROM conf_users WHERE id = ?";
|
|
||||||
$stmt = mysqli_prepare($c, $sql);
|
|
||||||
mysqli_stmt_bind_param($stmt, "i", $uid);
|
|
||||||
mysqli_stmt_execute($stmt);
|
|
||||||
$res = mysqli_stmt_get_result($stmt);
|
|
||||||
return mysqli_fetch_assoc($res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: fetch mapping
|
|
||||||
function fetch_dept($c, $uid) {
|
|
||||||
$sql = "SELECT department_id FROM conf_user_departments WHERE user_id = ?";
|
|
||||||
$stmt = mysqli_prepare($c, $sql);
|
|
||||||
mysqli_stmt_bind_param($stmt, "i", $uid);
|
|
||||||
mysqli_stmt_execute($stmt);
|
|
||||||
$res = mysqli_stmt_get_result($stmt);
|
|
||||||
$row = mysqli_fetch_assoc($res);
|
|
||||||
return $row ? intval($row['department_id']) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process save
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user_id > 0) {
|
|
||||||
$name = isset($_POST['name']) ? trim($_POST['name']) : '';
|
|
||||||
$email = isset($_POST['email']) ? trim($_POST['email']) : '';
|
|
||||||
$goo = isset($_POST['goo']) ? trim($_POST['goo']) : '';
|
|
||||||
$dept = isset($_POST['department_id']) ? intval($_POST['department_id']) : 0;
|
|
||||||
|
|
||||||
// Update conf_users
|
|
||||||
$upd = mysqli_prepare($c, "UPDATE conf_users SET name = ?, email = ?, goo = ? WHERE id = ?");
|
|
||||||
mysqli_stmt_bind_param($upd, "sssi", $name, $email, $goo, $user_id);
|
|
||||||
mysqli_stmt_execute($upd);
|
|
||||||
|
|
||||||
// Upsert department mapping
|
|
||||||
if ($dept > 0) {
|
|
||||||
$ins = mysqli_prepare($c, "INSERT INTO conf_user_departments (user_id, department_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE department_id = VALUES(department_id)");
|
|
||||||
mysqli_stmt_bind_param($ins, "ii", $user_id, $dept);
|
|
||||||
mysqli_stmt_execute($ins);
|
|
||||||
} else {
|
|
||||||
$del = mysqli_prepare($c, "DELETE FROM conf_user_departments WHERE user_id = ?");
|
|
||||||
mysqli_stmt_bind_param($del, "i", $user_id);
|
|
||||||
mysqli_stmt_execute($del);
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = "Saved changes.";
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $user_id ? fetch_user($c, $user_id) : null;
|
|
||||||
$user_dept = $user_id ? fetch_dept($c, $user_id) : null;
|
|
||||||
|
|
||||||
$MY_TITLE = "Edit User";
|
|
||||||
$MY_CRUMB = "Edit User";
|
|
||||||
|
|
||||||
if (!$user) {
|
|
||||||
$CONTENT = "<p>No user found. Provide ?id=USER_ID in the query string.</p>";
|
|
||||||
include 'layout.php';
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
?>
|
|
||||||
<?php if ($message) { ?>
|
|
||||||
<div class="mb-4 p-3 bg-green-100 text-green-800 rounded border border-green-200"><?= htmlspecialchars($message) ?></div>
|
|
||||||
<?php } ?>
|
|
||||||
<form method="post" class="space-y-4 bg-white p-4 rounded shadow">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Name</label>
|
|
||||||
<input name="name" value="<?= htmlspecialchars($user['name']) ?>" class="w-full border rounded px-3 py-2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Email</label>
|
|
||||||
<input name="email" value="<?= htmlspecialchars($user['email']) ?>" class="w-full border rounded px-3 py-2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">GOO</label>
|
|
||||||
<input name="goo" value="<?= htmlspecialchars($user['goo'] ?? '') ?>" class="w-full border rounded px-3 py-2" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Department</label>
|
|
||||||
<select name="department_id" class="w-full border rounded px-3 py-2">
|
|
||||||
<option value="0">-- None --</option>
|
|
||||||
<?php foreach ($dept_opts as $d) {
|
|
||||||
$dept_name = trim($d['name'] ?? '');
|
|
||||||
if ($dept_name === '') { $dept_name = '(Unnamed Department)'; }
|
|
||||||
?>
|
|
||||||
<option value="<?= intval($d['id']) ?>" <?= ($user_dept === intval($d['id'])) ? 'selected' : '' ?>>
|
|
||||||
<?= htmlspecialchars($dept_name) ?> (<?= intval($d['id']) ?>)
|
|
||||||
</option>
|
|
||||||
<?php } ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="pt-2">
|
|
||||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<?php
|
|
||||||
$CONTENT = ob_get_clean();
|
|
||||||
include 'layout.php';
|
|
||||||
Loading…
Reference in New Issue