Compare commits

..

No commits in common. "master" and "fa25" have entirely different histories.
master ... fa25

10 changed files with 151 additions and 998 deletions

View File

@ -1,7 +1,5 @@
<?php
$MY_TITLE = "All Sessions";
$MY_CRUMB = "All Sessions";
$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>';
$CONTENT = '<div id="timeslot-config" class="hidden"></div><activitylist :itineraryview="0" :static="1" :show_all_sessions="true"></activitylist>';
$MY_TITLE = "Events, Training, & Workshops";
$MY_CRUMB = "Activities";
$CONTENT = '<activitylist :static="0" :itineraryview="false" zoom_on="0" survey_on="0"></activitylist>';
include 'layout.php';

View File

@ -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';

View File

@ -649,9 +649,6 @@ function get_ses_hosts() { global $c, $AY;
$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);
}
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');
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,4 +3,5 @@
$MY_TITLE = "Edit Sessions";
$MY_CRUMB = "Edit";
$CONTENT = '<activityeditorlist></activityeditorlist>';
$XTRAJS = 'js/editor.js';
include 'layout.php';

View File

@ -1,7 +1,5 @@
<?php
$MY_TITLE = "Schedule & Sessions";
$MY_TITLE = "Itinerary";
$MY_CRUMB = "My Schedule";
$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>';
$CONTENT = '<div id="timeslot-config" class="hidden"></div><activitylist :itineraryview="1" :show_all_sessions="true"></activitylist>';
$CONTENT = "<activitylist :itineraryview='1'></activitylist>";
include 'layout.php';

View File

@ -29,17 +29,9 @@ function init_file_dropzone(parameter_name) {
function parsesqltime(mysqldate) { // 2021-01-29 09:00:00
if (! mysqldate) { return 0 }
if (mysqldate instanceof Date) { return mysqldate }
var s = String(mysqldate).trim()
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
}
var field = mysqldate.match(/^(\d\d\d\d)\-(\d+)\-(\d+)[T|\s](\d+)\:(\d+)\:(\d+)$/)
var mydate = new Date(field[1], field[2] - 1 , field[3], field[4], field[5], field[6])
return mydate }
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', {
props: [ 'itineraryview','static','show_all_sessions' ],
props: [ 'itineraryview','static' ],
data: function () {
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,
hosts_by_sesid: {}, options:{}, conference:{}, ay:{}, conf:-1, survey_on:0, zoom_on:1,
cancelingIds:[], timeslotConfig:null, expandedDesc:{}, collapsedDays:{},
surveyAnswersBySession: {}, } },
show_filters: 'all', expanded: 1, editing: -1, active:-1, hosts:{},
hosts_by_sesid: {}, options:{}, conference:{}, ay:{}, conf:-1, survey_on:0, zoom_on:1, } },
mounted: function() {
this.fetch_myevents()
},
methods: {
normalizeId: function(id) {
var parsed = parseInt(id, 10);
return Number.isNaN(parsed) ? id : parsed;
},
special_signup: function(activity_id) {
if (activity_id==1462 || activity_id==1455) {
return true;
@ -910,60 +884,12 @@ const ActivityList = Vue.component('activitylist', {
group.classList.toggle('hidden');
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) {
const para = document.getElementById(id);
const btn = document.getElementById(id + '-btn');
para.classList.toggle('line-clamp-2');
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) {
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)
let [y, m, d, h, i, s] = time.split(/[- :]/); // split time into parts
@ -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 },
get_day_title: function(day) {
var d = dayjs(day, 'MMM DD YYYY')
var convertedDateString = d.format('YYYY-MM-DD')
var found = _.findWhere( this.conference, {date1:convertedDateString} )
if (found && found.title) { return found.title }
return ''
convertedDateString = d.format('YYYY-MM-DD')
return _.findWhere( this.conference, {date1:convertedDateString} ).title
},
month_year: function(d) { var b = this.$root.$dj(d).format('MMM D YYYY'); return b },
setsort: function(ss) {
@ -992,144 +916,49 @@ const ActivityList = Vue.component('activitylist', {
am_editing: function(id) { return 0 },
fetch_myevents: function() {
var self = this;
var hostUrl = 'dir_api.php?a=get/hosts';
if (self.show_all_sessions) { hostUrl += '&all=1'; }
basic_get(hostUrl, function(r2) {
basic_get('dir_api.php?a=get/hosts', function(r2) {
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',
function(r2) {
self.mysessions = r2.mysessions
self.my_ses_ids = _.map(_.pluck(r2.mysessions, 'id'), function(val) {
return self.normalizeId(val);
})
self.my_ses_ids = _.pluck(r2.mysessions, 'id')
self.activities = r2.sessions
if (r2.host != null) { self.my_host_ids = r2.host }
else { self.my_host_ids = [] }
self.options = r2.options
self.conference = r2.conference
self.ay = r2.ay
if (r2.hostbysession) {
self.hosts_by_sesid = _.groupBy(r2.hostbysession,function(x) { return x.id } )
}
self.survey_on = parseInt( _.findWhere(self.options, { label:'survey_on' }).value )
self.zoom_on = parseInt( _.findWhere(self.options, { label:'zoom_on' }).value )
self.active = 1
self.loadTimeslots()
self.$forceUpdate();
} )
},
joinme: function(id) {
var self = this
id = self.normalizeId(id)
basic_get('dir_api.php?a=signup/' + id,
function(r2) {
if (!self.my_ses_ids.includes(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()
self.mysessions.push(_.findWhere(self.activities, {'id':id}))
self.my_ses_ids.push(id)
alert_message("Added activity") })
},
dumpme: function(id) {
var self = this
id = self.normalizeId(id)
if (!self.cancelingIds.includes(id)) { self.cancelingIds.push(id) }
basic_get('dir_api.php?a=signdown/' + id,
function(r2) {
setTimeout(function() {
self.mysessions = _.filter(self.mysessions, function(s) {
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.mysessions = _.without( self.mysessions, _.findWhere(self.activities, {'id':id}))
self.my_ses_ids = _.without( self.my_ses_ids, id)
self.$forceUpdate()
alert_message("Removed activity")
}, 250)
})
alert_message("Removed activity") })
},
sessionsForSlot: function(slot) {
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 }) {
filtered: function(ff) {
var self = this
var applySearch = (opts.applySearch !== false)
var applySlot = (opts.applySlot !== false)
if (applySearch && this.search) {
if (this.search) {
var ss = self.search.toLowerCase()
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) {
this_time = dayjs(item.starttime)
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) {
if (x[self.sortby]) { var s = x[self.sortby]; return s.trim().toLowerCase() }
return '' })
@ -1158,58 +984,7 @@ const ActivityList = Vue.component('activitylist', {
current_time: function() { return dayjs().format('MMM D, h:mma') },
activities_filtered: function() { var a = this.filtered(this.activities); return a; },
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 {} }
var self=this;
return _.groupBy(self.activities_filtered, function(x) { return self.month_year(x.starttime) } ); },
@ -1228,169 +1003,135 @@ const ActivityList = Vue.component('activitylist', {
</p>
</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 -->
<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="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.
</p>
</template>
</div>
<!-- Simple full list for All Sessions -->
<div v-if="!itineraryview && active > 0" class="space-y-6">
<div v-for="(items, mmyy) in activities_g_filtered" :key="mmyy">
<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 border rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-start gap-3">
<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>
<div class="text-sm text-gray-600">
<span v-if="hoststr(a.id)">{{ hoststr(a.id) }}</span>
<i v-else>no host registered</i>
<!-- Grouped sessions by date -->
<div v-if="itineraryview && active > 0" v-for="(items, mmyy) in mysessions_g_filtered" :key="mmyy" class="mb-8">
<h3 class="text-xl font-bold text-gray-800 mb-2">{{ mmyy }} {{ get_day_title(mmyy) }}</h3>
<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 class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<!-- Left side controls -->
<div class="flex flex-wrap gap-2 md:flex-col md:min-w-[100px]">
<a v-if="my_host_ids.includes(a.id)"
:href="'ed_act.php?w=' + a.id"
class="text-sm text-blue-600 hover:underline">Edit</a>
<a v-if="my_host_ids.includes(a.id)"
:href="'report.php?s=' + a.id"
class="text-sm text-blue-600 hover:underline">Info</a>
</div>
<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-sm text-gray-600" v-if="a.location_irl">Location: {{ a.location_irl }}</p>
<p class="text-sm text-blue-600" v-if="a.location">Link: <a :href="a.location" class="underline">{{ a.location }}</a></p>
<!-- 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 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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</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>` })
@ -1595,7 +1336,6 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
reversed: false,
selectedIndex: 0,
editingId: null,
dayFilter: "all",
// filters
show_filters: "all",
filters: {
@ -1619,27 +1359,9 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
this.$root.fetch_menus();
this.fetchAll();
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() {
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: {
async fetchAll() {
@ -1738,13 +1460,6 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
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
const ay = this.$root.settings["default_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") {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
e.preventDefault();
} else if (e.key === "Enter") {
} else if (e.key === "Enter" || e.key.toLowerCase() === "e") {
const a = this.activitiesFiltered[this.selectedIndex];
this.startEdit(a.id);
e.preventDefault();
@ -1812,20 +1527,6 @@ const ActivityEditorList = Vue.component('activityeditorlist', {
activitiesFiltered() {
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() {
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('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>
</div>
@ -2214,30 +1911,14 @@ const AskSurvey = Vue.component('asksurvey', {
function(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() },
template: `<div class="survey-page bg-white p-4 rounded shadow">
<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>
template: `<div>
<h3 v-if="questions.length">{{ questions[0].title }}</h3>
<div class="session-survey" v-for="q in questions">
<number-question v-if="q['type']=='2'" :qq="q"></number-question>
<essay-question v-if="q['type']=='1'" :qq="q"></essay-question>
</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>`
});

View File

@ -13,7 +13,6 @@ require_once('semester.php');
if (!isset($MY_TITLE)) $MY_TITLE = "Untitled Page";
if (!isset($MY_CRUMB)) $MY_CRUMB = $MY_TITLE;
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'];
$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>
<script src="js/tailwind.js"></script>
<script src="js/vue27max.js"></script>
<script src="js/intranet_libs.js"></script>
<?php if (isset($config_js)) { echo $config_js; } ?>
<script src="js/intranet_libs.js"/></script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<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 /> &nbsp;
<br /> &nbsp;
<br /> &nbsp;
<script src="js/intranet_libs_bottom.js"></script>
<script src="js/dir_app.js?v=<?= $DIR_APP_VER ?>"></script>
<script src="js/intranet_libs_bottom.js"/></script>
<script src="js/dir_app.js"></script>
<script src="<?= $XTRAJS ?>"></script>
</body>
</html>

251
ops.py
View File

@ -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()

View File

@ -48,39 +48,3 @@ ol {
overflow-wrap: anywhere;
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
View File

@ -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';