flexday/js/dir_app.js

3117 lines
126 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var PROD = 1
if (PROD && location.protocol !== 'https:') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}
// _ _
// | | | |
// | |__ ___| |_ __ ___ _ __ ___
// | '_ \ / _ \ | '_ \ / _ \ '__/ __|
// | | | | __/ | |_) | __/ | \__ \
// |_| |_|\___|_| .__/ \___|_| |___/
// | |
// |_|
function init_file_dropzone(parameter_name) {
Dropzone.options.myGreatDropzone = { // camelized version of the `id`
paramName: parameter_name, // The name that will be used to transfer the file
maxFilesize: 6 }; // MB
}
// init_file_dropzone("staffpicupload")
function parsesqltime(mysqldate) { // 2021-01-29 09:00:00
if (! mysqldate) { 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)) }
Object.defineProperty(Vue.prototype, '$dj', { value: dj });
Object.defineProperty(Vue.prototype, '$parsesqltime', { value: parsesqltime });
// Insert zero-width spaces into long unbroken tokens so they wrap nicely.
function wrapLongTokens(str, n=30) {
if (str === null || str === undefined) return ''
const s = String(str)
return s.split(/(\s+)/).map(tok => {
if (!tok || /\s/.test(tok) || tok.length < n) return tok
return tok.replace(new RegExp('(.{'+n+'})', 'g'), '$1\u200B')
}).join('')
}
Object.defineProperty(Vue.prototype, '$wrap', { value: wrapLongTokens });
// ________ __
// | ____\ \ / /
// | |__ \ V /
// | __| > <
// | | / . \
// |_| /_/ \_\
//
//
//
// VISUAL EFFECTS
//
function fade_message(theselector='#alert') {
var a_dom = document.querySelector(theselector);
TinyAnimate.animateCSS(a_dom, 'opacity', '', 1.0, 0.0, 750, 'easeInOutQuart', function() { }); }
function alert_message(msg,color='yellow') {
var a = $('#alert')
a.text(msg)
a.removeClass('hidden');
a.css('background-color',color)
var a_dom = document.querySelector('#alert');
TinyAnimate.animateCSS(a_dom, 'opacity', '', 0.0, 1.0, 500, 'easeInOutQuart', function() {
setTimeout( fade_message, 2500 ) }); }
function fadein_message(theclass='success') {
//var a = $('#'+theclass)
//a.css('visibility','visible')
var a_dom = document.querySelector('.'+theclass);
TinyAnimate.animateCSS(a_dom, 'opacity', '', 0.0, 1.0, 500, 'easeInOutQuart', function() {
setTimeout( function() { fade_message('.'+theclass) }, 2500 ) }); }
// _
// | |
// ___ ___ _ __ ___ _ __ ___ _ __ ___ _ __ | |_ ___
// / __/ _ \| '_ ` _ \| '_ \ / _ \| '_ \ / _ \ '_ \| __/ __|
// | (_| (_) | | | | | | |_) | (_) | | | | __/ | | | |_\__ \
// \___\___/|_| |_| |_| .__/ \___/|_| |_|\___|_| |_|\__|___/
// | |
// |_|
//
// FORM COMPONENTS
//
// TODO these should know if they're modifying the current user or someone else.
// A single text style question
const TQuestion = Vue.component('field', {
props: [ 'table', 'qid', 'question', 'answer', 'placeholder', 'targetid', ],
data: function () {
return { "a":this.answer } },
watch: {
"answer": function(val,Oldval) { this.a = val },
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) }, },
mounted() {
},
template: `<div class="mb-4">
<label v-if="question" :for="qid" class="block text-sm font-medium text-gray-700 mb-1">
{{ question }}
</label>
<input
:id="qid"
type="text"
v-model="a"
:placeholder="placeholder"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2 text-sm"
/>
</div>`,
})
// A single INLINE text style question
const TIQuestion = Vue.component('ifield', {
props: [ 'table', 'qid', 'question', 'answer', 'placeholder', 'myclass', 'targetid', ],
data: function () {
return { "a":this.answer } },
watch: {
"answer": function(val,Oldval) { this.a = val },
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) }, },
template: `<input :id="this.qid" type="text" v-model="a" :placeholder="placeholder" :class="this.myclass" />`,
})
// A single checkbox
const Checkbox = Vue.component('checkbox', {
props: [ 'table', 'qid', 'question', 'answer', 'targetid', ],
data: function () {
return { "a":this.answer } },
watch: {
"answer": function(val,Oldval) { this.a = val },
"a": function (val, oldVal) {
var newVal = 0
if (val==true) { newVal = 1 }
this.$root.events.trigger('changed',[this.qid,this.table, newVal, this.targetid]) }, },
template: `<div class="pure-controls">
<label :for="this.qid" class="question pure-checkbox">
<input :id="this.qid" type="checkbox" class="form-control" v-model="a" />
{{ question }}</label></div>`
})
// A single long format text question
const TAQuestion = Vue.component('tfield', {
props: [ 'table', 'qid', 'question', 'answer', 'targetid', 'myclass' ],
data: function () {
return { "a": this.answer } },
watch: {
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) },
"answer": function (val, oldVal) { this.a = val },
},
template: `<div class="mb-4">
<label :for="qid" class="block text-sm font-medium text-gray-700 mb-1">
{{ question }}
</label>
<textarea
:id="qid"
v-model="a"
:class="myclass"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2 text-sm"
></textarea>
</div>` })
// long format text WYSIWYG HTML
const HTAQuestion = Vue.component('htfield', {
props: [ 'table', 'qid', 'question', 'answer', 'targetid', 'myclass' ],
data: function () { return { "a": this.answer, "p":0, } }, // the pell editor object
mounted: function() {
var self = this
var element = document.getElementById(self.qid)
this.p = pell.init( { element: element, onChange: function(h) {
self.$root.events.trigger('changed',[self.qid, self.table, h, self.targetid]) } } )
self.p.content.innerHTML = self.answer },
watch: {
"answer": function (val, oldVal) { this.p.content.innerHTML = val }, },
template: `<div><label :for="this.qid" class="question">{{ question }} </label>
<div :id="this.qid" class="pell"></div></div>` })
// long format text WYSIWYG HTML FORM ALIGNED STYLE
const HTAQuestionFA = Vue.component('htfield_fa', {
props: [ 'table', 'qid', 'question', 'answer', 'targetid', 'myclass' ],
data: function () { return { "a": this.answer, "p":0, } }, // the pell editor object
mounted: function() {
var self = this
var element = document.getElementById(self.qid)
this.p = pell.init( { element: element, onChange: function(h) {
self.$root.events.trigger('changed',[self.qid, self.table, h, self.targetid]) } } )
self.p.content.innerHTML = self.answer },
watch: {
"answer": function (val, oldVal) { this.p.content.innerHTML = val }, },
template: `<div class=""><label :for="this.qid" class="question">{{ question }} </label>
<div :id="this.qid" class="form-control pell"></div></div>` })
// A single numeric question
const NQuestioon = Vue.component('n-question', {
props: [ 'qq' ],
data: function () { return { "answer": this.qq.answer } },
watch: { "answer": function (val, oldVal) {
this.$root.events.trigger('update_survey', [this.qq.user, this.qq.session, this.qq.qid, val]) }, },
template: `<div class=""><span class="question">{{ qq.question }}</span><br />
<div class="makeup_indent "><input class="form-check-input" type="radio" id="one" value="1" v-model="answer">
<label class="form-check-label" for="one">One</label> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<input class="form-check-input" type="radio" id="two" value="2" v-model="answer">
<label class="form-check-label" for="two">Two</label> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<input class="form-check-input" type="radio" id="three" value="3" v-model="answer">
<label class="form-check-label" for="three">Three</label> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<input class="form-check-input" type="radio" id="four" value="4" v-model="answer">
<label class="form-check-label" for="four">Four</label> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<input class="form-check-input" type="radio" id="five" value="5" v-model="answer">
<label class="form-check-label" for="five">Five</label> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</div></div>`
})
// A single survey text question
const STQuestion = Vue.component('st-question', {
props: [ 'qq' ],
data: function () { return { "answer": this.qq.answer } },
watch: { "answer": function (val, oldVal) {
this.$emit('dirty')
this.$root.events.trigger('update_survey', [this.qq.user, this.qq.session, this.qq.qid, val]); }, },
template: `<div class="pure-form-aligned"><label class="question">{{ qq.question }}</label>
<textarea :id="qq.id" class="form-control pure-form-aligned" v-model="answer"></textarea></div>`,
})
// The "I Certify" question
const ICertify = Vue.component('icertify', {
props: [ 'c' ],
data: function () { return { "checked": this.c } },
watch: { checked() {
this.$emit('dirty')
this.$parent.docert(this.checked); },
},
template: `<div class="makeup_indent"><input class="form-check-input" type="checkbox" id="checkbox" v-model="checked">
<label class="icert form-check-label" for="checkbox">I certify that I have attended this event.</label><br />
</div>` });
// Select menu
/*
table: name of database table
qid: what column of the table to edit
question: label displayed to user
answer: data item used for v-model
menu: key of root vue object with menu items
labelfield: key of 'menu' object that should be displayed as choices
targetid: when editing existing row: the id of the row
*/
const SelMenu = Vue.component('selectmenu', {
props: [ 'table', 'qid', 'question', 'answer', 'menu', 'labelfield', 'targetid', ],
data: function () { return { "a": this.answer } },
watch: {
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) },
"answer": function (val, oldVal) { this.a = val }, },
template: `<div class="mb-4">
<label :for="qid" class="block text-sm font-medium text-gray-700 mb-1">
{{ question }}
</label>
<select
:id="qid"
v-model="a"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2 text-sm bg-white"
>
<option v-for="o in $root[menu]" :value="o.id">
{{ o[labelfield] }}
</option>
</select>
</div>` })
// Select menu FORM ALIGNED STYLE
const SelMenuFA = Vue.component('selectmenu_fa', {
props: [ 'table', 'qid', 'question', 'answer', 'menu', 'labelfield', 'targetid', ],
data: function () { return { "a": this.answer } },
watch: {
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) },
"answer": function (val, oldVal) { this.a = val }, },
template: `<div class="mb-4">
<label :for="qid" class="block text-sm font-medium text-gray-700 mb-1">
{{ question }}
</label>
<select
:id="qid"
v-model="a"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2 text-sm bg-white"
>
<option v-for="o in $root[menu]" :value="o.id">
{{ o[labelfield] }}
</option>
</select>
</div>` })
// A date time picker
const DTPicker = Vue.component('dtpicker', {
props: [ 'table', 'qid', 'question', 'answer', 'targetid', ],
methods: { },
mounted: function() { },
data: function () { return { "a":this.answer } },
watch: {
"answer": function(val,Oldval) { this.a = val },
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) }, },
template: `<div class="mb-4">
<label v-if="question" :for="qid" class="block text-sm font-medium text-gray-700 mb-1">
{{ question }}
</label>
<input
type="datetime-local"
:id="qid"
v-model="a"
step="300"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2 text-sm"
/>
</div>`,
})
// Accordion style box
const ExpandyBox = Vue.component('expandybox', {
props: [ 'header', 'body', ],
methods: {
toggle: function() {
if (this.state=='close') { this.state='open'; this.symbol='-' }
else { this.state='close'; this.symbol='+' }
}
},
mounted: function() { },
data: function () { return { "state":'close', "symbol":"+", } },
watch: {
"answer": function(val,Oldval) { this.a = val },
"a": function (val, oldVal) {
this.$root.events.trigger('changed',[this.qid,this.table, val, this.targetid]) }, },
template: `<div><div class="pure-g">
<div class="pure-u-1-1">
<h4 class="exp_header">
<a href="#" v-on:click.prevent="toggle()">
<span class="decorate">{{symbol}}</span>
{{header}}</a>
</h4>
</div>
</div>
<div v-if="state=='open'" class="pure-g">
<div class="pure-u-1-1" v-html="body"><slot></slot></div>
</div>
</div>`,
})
// mode pill
Vue.component('ModePill', {
props: ['mode'],
computed: {
label() {
switch (this.mode) {
case 'inperson': return 'In Person';
case 'online': return 'Online';
case 'hybrid': return 'Hyflex';
default: return this.mode;
}
},
bgClass() {
switch (this.mode) {
case 'inperson': return 'bg-green-500';
case 'online': return 'bg-blue-500';
case 'hybrid': return 'bg-yellow-500 text-black';
default: return 'bg-gray-400';
}
}
},
template: `
<span class="text-xs font-semibold text-white px-2 py-1 rounded"
:class="bgClass">
{{ label }}
</span>
`
});
// _ __ __ _____ _____ _____
// | | / _|/ _| | __ \_ _| __ \
// ___| |_ __ _| |_| |_ | | | || | | |__) |
// / __| __/ _` | _| _| | | | || | | _ /
// \__ \ || (_| | | | | | |__| || |_| | \ \
// |___/\__\__,_|_| |_| |_____/_____|_| \_\
//
//
//
// ONE LINE OF THE STAFF DIR EDITOR listing
//
const StaffLine = Vue.component('staff_line', {
props: [ 's', 'i', 'dup_class', ],
//data: function() { return { } },
methods: {
nope: function() { return 1; },
odd: function(i) { if (i % 2 ==0) { return "even" } return "odd" },
bio: function() { return "bio.php?p=" + this.s.id },
gtitle: function() { if (this.s.gtitle) {return _.findWhere(this.$root.titles_menu, {id:this.s.gtitle}).name}; return "" },
gdept1: function() { if (this.s.dept1) {return _.findWhere(this.$root.depts_menu, {id:this.s.dept1}).name}; return "" },
gdept2: function() { if (this.s.dept2) {return _.findWhere(this.$root.depts_menu, {id:this.s.dept2}).name}; return "-" },
grole: function() { if (this.s.role) {return _.findWhere(this.$root.roles_menu, {id:this.s.role}).descr}; return "" },
swapedit: function() { this.$emit('swapout', this.s.id) }
},
template: `<div>
<div :class="odd(i) + ' pure-g'">
<div class="pure-u-1-4"><pre>
status: {{ s.status }}
permissions: {{ grole() }}
pers id: {{ s.id }}
ext id: {{ s.ext_id }}
bio page: {{ s.web_on }}
Photo - use: {{s.use_dir_photo}} release: {{s.general_photo_release}}
path: {{s.dir_photo_path}}
# Sections: {{s.num_taught}}
{{ s.sections }} </pre>
</div>
<div class="pure-u-1-4"><pre>
staff: {{ s.staff_type }}
conf_id: {{ s.conf_id }}
G00{{s.conf_goo}}
espanol: {{s.espanol}}
</pre>
</div>
<div class="pure-u-1-4"><pre>
{{s.first_name}} {{s.last_name}}
{{gtitle()}}
{{gdept1()}}
{{gdept2()}}
(old dept: {{s.department}} ) </pre>
</div>
<div class="pure-u-1-4"><pre>
email: {{s.email}}
room: {{s.room}}
phone: {{s.phone_number}}
Zoom: {{s.zoom}}
Preferred contact: {{s.preferred_contact}}
</pre><div class="xxpure-u-1-24 clicky" v-on:click="swapedit"><span class="pure-button button-inlist">edit</span></div>
</div>
</div>
</div>`
});
//
//
// STAFF DIR LISTING
//
// ONE LINE - BUT EDITING!
//
const StaffLineEdit = Vue.component('staff_line_edit', {
props: [ 's', 'i' ],
//data: function() { return { } },
methods: {
nope: function() { return 1; },
odd: function(i) { if (i % 2 ==0) { return "even" } return "odd" },
bio: function() { return "bio.php?p=" + this.s.id },
done: function() { this.$emit('done_edit') },
gtitle: function() { if (this.s.gtitle) {return _.findWhere(this.$root.titles_menu, {id:this.s.gtitle}).name}; return "" },
gdept1: function() { if (this.s.dept1) {return _.findWhere(this.$root.depts_menu, {id:this.s.dept1}).name}; return "" },
gdept2: function() { if (this.s.dept2) {return _.findWhere(this.$root.depts_menu, {id:this.s.dept2}).name}; return "-" },
grole: function() { if (this.s.role) {return _.findWhere(this.$root.roles_menu, {id:this.s.role}).descr}; return "" },
}, //v-lazy-container="{ selector: 'img' }"
template: `<form class="pure-form">
<div :class="odd(i) + ' list-editor pure-g'">
<div class="pure-u-1-4">
<checkbox table="personnel" qid="status" question="Published" :answer="s.status" :targetid="s.id"></checkbox>
permissions: <selectmenu table="personnel_ext" qid="role" :answer="s.role" menu="roles_menu" :targetid="s.ext_id" labelfield="descr"></selectmenu><br />
pers id: {{ s.id }}<br />
ext id: {{ s.ext_id }}<br />
<checkbox table="personnel" qid="web_on" question="Bio Page On" :answer="s.web_on" :targetid="s.id"></checkbox>
<checkbox table="personnel_ext" qid="use_dir_photo" question="Use Photo" :answer="s.use_dir_photo" :targetid="s.ext_id"></checkbox>
<checkbox table="personnel_ext" qid="general_photo_release" question="Release Photo" :answer="s.general_photo_release" :targetid="s.ext_id"></checkbox>
Dir photo <ifield myclass="double" table="personnel_ext" qid="dir_photo_path" :answer="s.dir_photo_path" :targetid="s.ext_id" placeholder="Dir Photo Path"></ifield><br />
# Sections: {{s.num_taught}}<br />
{{ s.sections }}
</div>
<div class="pure-u-1-4">
Staff Type <ifield myclass="double" table="personnel" qid="staff_type" :answer="s.staff_type" :targetid="s.id" placeholder="Staff Type"></ifield><br />
conf_id: {{ s.conf_id }}<br />
<!-- GOO <ifield myclass="double" table="conf_users" qid="goo" :answer="s.conf_goo" :targetid="s.conf_id" placeholder="GOO"></ifield><br />-->
GOO {{ s.conf_goo }} <br />
<checkbox table="personnel_ext" qid="espanol" question="Speak Spanish" :answer="s.espanol" :targetid="s.ext_id"></checkbox>
</div>
<div class="pure-u-1-4">
<ifield myclass="double" table="personnel" qid="first_name" :answer="s.first_name" :targetid="s.id" placeholder="First Name"></ifield><br />
<ifield myclass="double" table="personnel" qid="last_name" :answer="s.last_name" :targetid="s.id" placeholder="Last Name"></ifield><br />
<selectmenu table="personnel_ext" qid="gtitle" :answer="s.gtitle" menu="titles_menu" :targetid="s.ext_id" labelfield="name"></selectmenu><br />
<selectmenu table="personnel_ext" qid="dept1" :answer="s.dept1" menu="depts_menu" :targetid="s.ext_id" labelfield="name"></selectmenu> <br />
<selectmenu table="personnel_ext" qid="dept2" :answer="s.dept2" menu="depts_menu" :targetid="s.ext_id" labelfield="name"></selectmenu><br />
(old dept: {{s.department}} )
</div>
<div class="pure-u-1-4">
<ifield table="personnel" qid="email" :answer="s.email" :targetid="s.id" placeholder="Email"></ifield><br />
<ifield table="personnel" qid="room" :answer="s.room" :targetid="s.id" placeholder="Room"></ifield><br />
<ifield table="personnel" qid="phone_number" :answer="s.phone_number" :targetid="s.id" placeholder="Phone Number"></ifield><br />
<ifield table="personnel_ext" qid="zoom" :answer="s.zoom" :targetid="s.ext_id" placeholder="Zoom Room"></ifield><br />
<ifield table="personnel_ext" qid="preferred_contact" :answer="s.preferred_contact" :targetid="s.ext_id" placeholder="Preferred contact"></ifield>
<div class="xxpure-u-1-24 clicky" v-on:click="done"><span class="pure-button button-inlist-action">done</span>
</div>
</div>
</div>
</form>
`
});
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
//
// STAFF DIR LISTING MAIN CONTAINER
//
const DirList = Vue.component('dirlist', {
data: function () {
return { "personnel":[], sortby:'last_name', search:'', reversed: false, id_list:[], id_dups:[],
components: {n:StaffLine, e: StaffLineEdit }, editing: -1, } },
mounted: function() {
var self = this;
this.$root.fetch_menus();
fetch('dir_api.php?a=list/staffsemester', { method: 'GET' }).then(function (response) {
if (response.ok) {
response.json().then( function(r2) {
self.personnel = r2;
_.each( self.personnel, function(x) {
if (x.sections==null) { x.num_taught=0 }
if (x.dir_photo_path==null) { x.dir_photo_path='images_sm/nobody.jpg' }
if (x.use_dir_photo == "0") { x.use_dir_photo = false } else { x.use_dir_photo = true }
if (x.espanol == "0" || !x.espanol) { x.espanol = false } else { x.espanol = true }
if (x.general_photo_release == "0") { x.general_photo_release = false
} else { x.general_photo_release = true }
if (self.id_list.includes(x.id)) { self.id_dups.push(x.id) }
else { self.id_list.push(x.id) }
x.searchable = ''
if (x.first_name) { x.searchable += ' ' + x.first_name.toLowerCase() }
if (x.last_name) { x.searchable += ' ' + x.last_name.toLowerCase() }
if (x.dept1name) { x.searchable += ' ' + x.dept1name.toLowerCase() }
if (x.titlename) { x.searchable += ' ' + x.titlename.toLowerCase() }
if (x.status == "1" || x.status == null) { x.status = "1"
} else { x.status = false; x.searchable += ' inactive' }
} ) } ) } else { return Promise.reject(response) }
}).then(function (data) { }).catch(function (err) { console.warn('Something went wrong.', err); });
},
methods: {
setsort: function(ss) {
if (this.sortby == ss) { this.reversed = ! this.reversed }
else {
this.reversed = false
this.sortby = ss
}
},
swapme: function(x) {
this.editing = x
},
done_edit: function(id) {
this.editing = -1
},
am_editing: function(id) {
if (id == this.editing) { return StaffLineEdit }
return StaffLine
},
is_dup_id_class: function(id) { return this.id_dups.includes(id) ? " dup_line" : "" },
},
computed: {
filtered: function() {
var ff = this.personnel
var self = this
if (this.search) {
var ss = self.search.toLowerCase()
ff = ff.filter(function(x) { return x.searchable.includes(ss) }) }
ff = _.sortBy(ff, function(x) {
if (x[self.sortby]) {
var s = x[self.sortby];
return s.trim().toLowerCase() }
return 'zzzzzzzzzz' })
if (this.reversed) {
ff.reverse()
}
return ff
}
},
watch: {
},
template: `<div class="">
<div class="pure-g pure-form">
<div class="pure-u-1-24"></div>
<div class="pure-u-3-4"><b>Filter: </b><input type="text" v-model="search" name="dirfilter" class="double" /></div>
</div>
<br />
<div class="pure-g pure-form">
<div class="pure-u-1-24">&nbsp;</div>
</div>
<div class="pure-g pure-form">
<div v-on:click="setsort('num_taught')" class="pure-u-1-24 clicky"><b>C</b></div>
<div class="pure-u-1-4 clicky">
<b v-on:click="setsort('last_name')">Name</b><br />
<b v-on:click="setsort('gtitle')">Title</b>
</div>
<div class="pure-u-1-6 clicky">
<b v-on:click="setsort('department')">Dept</b><br />
<b v-on:click="setsort('dept1')">Dept1</b>
</div>
<div v-on:click="setsort('email')" class="pure-u-1-6 clicky"><b>Email</b></div>
<div class="pure-u-1-6"><b>Phone</b></div>
<div v-on:click="setsort('room')" class="pure-u-1-6 clicky"><b>Room</b></div>
</div>
<!-- <staff_line_edit :s="this.$root.user" :i="0" :key="'staff_000'"></staff_line_edit> -->
<component :is="am_editing(p.id)" v-on:swapout="swapme" v-on:done_edit="done_edit" v-for="p,i in filtered" :s="p" :i="i" :dup_class="is_dup_id_class(p.id)" :key="'staff_'+p.id + '_' + i"></component>
</div>` })
// https://www.daterangepicker.com/
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local
// __ _ _ _ _ _ _
// / _| | | | (_) (_) | (_)
// | |_| | _____ __ __ _ ___| |_ ___ ___| |_ _ ___ ___
// | _| |/ _ \ \/ / / _` |/ __| __| \ \ / / | __| |/ _ \/ __|
// | | | | __/> < | (_| | (__| |_| |\ V /| | |_| | __/\__ \
// |_| |_|\___/_/\_\ \__,_|\___|\__|_| \_/ |_|\__|_|\___||___/
//
//
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
//
// EDIT / CREATE activity or event
//
const ActivityEditor = Vue.component('activityedit', {
props: [ 'which', ],
data: function () {
return { my_activity:0, creating:0,
everyname:[], everyone:[], hosts_by_sesid:{}, this_hosts:[], host_search:[], host_search_str:'',
activities:[], this_activity:{}, editing: -1, active:-1, } },
mounted: function() {
this.fetch_mypeople()
this.$root.fetch_menus()
if (this.which==0) {
this.creating = 1
this.$root.creating = 1
this.this_activity = {"title":"","desc":"","length":"1","starttime":"",
"track":"","location":"","gets_survey":"1","category":"1",
"parent":"","recording":"","instructions":"","image_url":"",
"is_flex_approved":"1","typeId":"101"}
} else { this.my_activity = this.which
this.fetch_myevents() } },
methods: {
set_id: function(new_id) { this.this_activity.id = new_id; this.creating=0; this.$root.creating=0 },
month_year: function(d) { var b = this.$root.$dj(d).format('MMM D YYYY'); return b },
setsort: function(ss) {
if (this.sortby == ss) { this.reversed = ! this.reversed }
else { this.reversed = false; this.sortby = ss } },
swapme: function(x) { this.editing = x },
done_edit: function(id) { this.editing = -1 },
am_editing: function(id) { return 0 },
remove: function(hostid) {
var self = this
basic_get('dir_api.php?a=remove/host/' + this.my_activity + '/' + hostid,
function(r2) {
self.this_hosts = _.reject(self.this_hosts, function(x) { return x.hostid == hostid} )
alert_message('Saved') } )
},
add: function(hostid) {
var self = this
basic_get('dir_api.php?a=add/host/' + this.my_activity + '/' + hostid,
function(r2) {
self.this_hosts.push( _.findWhere(self.everyone, {id:hostid} ) )
alert_message('Saved') } )
},
hostlookup: function() {
var self = this
if (this.host_search_str=='') { this.host_search=[] }
else { this.host_search_str = this.host_search_str.toLowerCase()
this.host_search = _.first(_.filter(self.everyone, function(x) { return x.name.toLowerCase().search( self.host_search_str) != -1 }),7) } },
save_new_event: function() {
this.$root.events.trigger('create_new_session',this.this_activity)
},
fetch_mypeople: function() {
var self = this;
basic_get('dir_api.php?a=get/names',
function(r2) {
self.everyone = r2.users
_.each( self.everyone, function(x) {
x.hostid = x.hostid } )
self.everyname = _.pluck(r2.users,'name') } ) },
fetch_myevents: function() {
var self = this;
basic_get('dir_api.php?a=get/sessions',
function(r2) {
self.activities = _.indexBy(r2,function(x) { return x.id } ); self.$forceUpdate()
self.active += 1;
_.each( self.activities, function(x) {
x.searchable = x.title.toLowerCase() + ' ' + x.desc.toLowerCase() } )
self.this_activity = self.activities[ self.my_activity ]
self.this_activity.starttime = self.this_activity.starttime.replace(' ','T')} )
basic_get('dir_api.php?a=get/hosts',
function(r2) {
var filtered = r2.filter(x => x.hostid != null)
self.hosts_by_sesid = _.groupBy(filtered,function(x) { return x.id } )
self.this_hosts = self.hosts_by_sesid[ self.my_activity ]
} )
},
},
template: `<div class="">
<!--<div class="pure-g pure-form pure-form-aligned">
<div class="pure-u-1-24"></div>
<div class="pure-u-23-24">
<div class="pure-control-group">
<label></label>
<a class="button-inlist" :href="'?w='+(this.my_activity-1)">Prev</a>
&nbsp; &nbsp;
<a class="button-inlist" :href="'?w='+(this.my_activity+1)">Next</a>
</div>
</div>
</div>-->
<div class="pure-g pure-form pure-form-aligned">
<div class="pure-u-1-24"></div>
<div class="pure-u-23-24">
<dtpicker table="conf_sessions" qid="starttime" :answer="this_activity.starttime"
:targetid="this_activity.id" question="Starts at"></dtpicker>
<field myclass="double" table="conf_sessions" qid="title" :answer="this_activity.title"
:targetid="this_activity.id" question="Title" placeholder="Title"></field>
<htfield_fa table="conf_sessions" qid="desc" question="Description" myclass="pell"
:answer="this_activity.desc" :targetid="this_activity.id"></htfield_fa>
<field myclass="double" table="conf_sessions" qid="location" :answer="this_activity.location"
:targetid="this_activity.id" question="Location / Zoom" placeholder="Location"></field>
<field myclass="double" table="conf_sessions" qid="location_irl" :answer="this_activity.location_irl"
:targetid="this_activity.id" question="Location / In Person" placeholder="Location"></field>
<selectmenu_fa table="conf_sessions"
qid="mode" :answer="this_activity.mode" menu="modes_menu"
:targetid="this_activity.id" question="What mode?" labelfield="string"></selectmenu_fa>
<field myclass="double" table="conf_sessions" qid="length" :answer="this_activity.length"
:targetid="this_activity.id" question="Length (in hours or minutes)" placeholder="1"></field>
<selectmenu_fa table="conf_sessions" qid="type" :answer="this_activity.type" menu="sessiontypes_menu"
:targetid="this_activity.id" question="Type" labelfield="type"></selectmenu_fa>
<selectmenu_fa v-if="this_activity.type=='101'" table="conf_sessions"
qid="parent" :answer="this_activity.parent" menu="parents_menu"
:targetid="this_activity.id" question="What flex day?" labelfield="title"></selectmenu_fa>
<field myclass="double" table="conf_sessions" qid="recording" :answer="this_activity.recording"
:targetid="this_activity.id" question="Recording Link" placeholder=""></field>
<br />
<!-- Existing Hosts List -->
<div v-if="!creating" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Hosts</label>
<div class="border rounded-md p-3 bg-gray-50">
<i v-if="this_hosts.length==0">type below to add a host</i>
<div v-for="h in this_hosts" class="flex items-center justify-between text-sm mb-1">
<span>{{ h.name }}</span>
<button
type="button"
@click="remove(h.hostid)"
class="text-red-500 hover:text-red-700 font-semibold"
>
x
</button>
</div>
</div>
</div>
<!-- Add Host Section -->
<div v-if="!creating" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Add a host</label>
<input
type="text"
id="addhost"
name="addhost"
v-model="host_search_str"
@keyup="hostlookup"
autocomplete="off"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2 text-sm mb-2"
/>
<div class="border rounded-md p-3 bg-gray-50">
<div v-for="ho in host_search" class="flex items-center justify-between text-sm mb-1">
<span
@click="add(ho.id)"
class="clickme">
{{ ho.name }}</span>
<button
type="button"
@click="add(ho.id)"
class="text-blue-500 hover:text-blue-700 font-semibold"
>
+
</button>
</div>
</div>
</div>
</div>
</div>
<!-- add: permissions track, x day if pd day
x add/remove host x New activity general list & signup
show signups, surveys instructions survey?
flex approved? image? what other permissions issues? -->
</div>` })
//
//
//
//
//
//
// ACTIVITIES LIST MAIN CONTAINER
// ------------------------------
//
//
//
const ActivityList = Vue.component('activitylist', {
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:{},
hosts_by_sesid: {}, options:{}, conference:{}, ay:{}, conf:-1, survey_on:0, zoom_on:1, } },
mounted: function() {
this.fetch_myevents()
},
methods: {
special_signup: function(activity_id) {
if (activity_id==1462 || activity_id==1455) {
return true;
}
return false;
},
get_day_index: function(dateStr) {
const date = new Date(dateStr);
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}${mm}${dd}`;
},
toggleDay: function(id) {
const group = document.getElementById(id);
const btn = document.getElementById(id + '-toggle');
group.classList.toggle('hidden');
btn.textContent = group.classList.contains('hidden') ? 'Expand Day' : 'Collapse Day';
},
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';
},
addTime: function(time, x) {
x = parseInt(x)
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
x > 8 ? dt.setMinutes(dt.getMinutes() + x) : dt.setHours(dt.getHours() + x); // add minutes or hours
var rd = dayjs(dt);
return rd.format('h:mma');
//return dt.toISOString().slice(0, 19).replace('T', ' '); // format output
},
hoststr: function(id) { var self = this
return _.reduce( self.hosts_by_sesid[id], function(mem,val) { if (val.name) { return mem + val.name + ", " } return mem }, '') },
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')
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) {
if (this.sortby == ss) { this.reversed = ! this.reversed }
else { this.reversed = false; this.sortby = ss } },
swapme: function(x) { this.editing = x },
done_edit: function(id) { this.editing = -1 },
am_editing: function(id) { return 0 },
fetch_myevents: function() {
var self = this;
basic_get('dir_api.php?a=get/hosts', function(r2) {
self.hosts_by_sesid = _.groupBy(r2,function(x) { return x.id } )
} )
basic_get('api2.php?query=app',
function(r2) {
self.mysessions = r2.mysessions
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
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.$forceUpdate();
} )
},
joinme: function(id) {
var self = this
basic_get('dir_api.php?a=signup/' + id,
function(r2) {
self.mysessions.push(_.findWhere(self.activities, {'id':id}))
self.my_ses_ids.push(id)
alert_message("Added activity") })
},
dumpme: function(id) {
var self = this
basic_get('dir_api.php?a=signdown/' + id,
function(r2) {
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") })
},
filtered: function(ff) {
var self = this
if (this.search) {
var ss = self.search.toLowerCase()
ff = ff.filter(function(x) { return ('searchable' in x ? x.searchable.includes(ss) : 0) })
}
if (this.active>0) {
var default_conf_id = _.findWhere(self.options, {label:'default_conference'}).value
self.conf = _.findWhere(self.conference, {id:default_conf_id})
var start = dayjs( self.conf.date1)
var end = dayjs(self.conf.date2)
ff = ff.filter( function(item,index) {
this_time = dayjs(item.starttime)
return this_time.isBefore(end) && start.isBefore(this_time) } )
ff = _.sortBy(ff, function(x) {
if (x[self.sortby]) { var s = x[self.sortby]; return s.trim().toLowerCase() }
return '' })
if (this.reversed) { ff.reverse() }
return ff
}
return []
}, },
computed: {
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_g_filtered: function() { if (this.active<1) { return {} }
var self=this;
return _.groupBy(self.activities_filtered, function(x) { return self.month_year(x.starttime) } ); },
mysessions_g_filtered: function() { if (this.active<1) { return {} }
var self=this; return _.groupBy(self.mysessions_filtered, function(x) { return self.month_year(x.starttime) } ); }, },
watch: { },
template: `<div class="activitylist">
<div>
<!-- Current time + Zoom info -->
<div v-if="itineraryview" class="mb-6">
<p class="text-sm text-gray-500">
<!--Current time: {{ current_time }}. -->
<span v-if="!zoom_on">
Return here on the day of your sessions for Zoom links.
</span>
</p>
</div>
<!-- No sessions signed up -->
<div v-if="itineraryview && mysessions_filtered.length === 0" class="mb-6">
<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>
</div>
<!-- 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>
<!-- 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>` })
// A CHART LABEL
//
const ChartLabel = Vue.component('chartlabel', {
props: [ 'title','start','end' ],
data: function () {
return { max_tracks: 6 } },
template: `<div class="chart_timebox" :style="'grid-column: times; grid-row: time-'+start+' / time-'+end">
<span class='chart_time'>{{title}}</span>
</div>` })
//
//
// A CHART SESSION
//
const ChartSesh = Vue.component('chartsesh', {
props: [ 'title','desc','track','start','end','start_h','end_h','mode','presenter','loc' ],
data: function () {
return { max_tracks: 6 } },
methods: {
t_start: function() {
if (this.track==0 || this.track==123) { return "track-1-start" }
return "track-" + this.track + "-start"
},
t_end: function() {
if (this.track==0 || this.track==123) { return "track-" + this.max_tracks + "-end" }
return "track-" + this.track + "-end"
},
},
template: `<div :class="'session session-1 track-'+track" :style="'grid-column: '+t_start()+' / '+t_end()+'; grid-row: time-'+start+' / time-'+end">
<h3 class="session-title">{{title}}</h3>
<span class="session-time">{{start_h}} - {{end_h}}</span>
<span class="session-desc">{{desc}}</span>
<span class="session-presenter">{{presenter}}</span>
<span class="session-mode">{{mode}}</span>
<span class="session-loc">{{loc}}</span>
</div>` })
//
//
//
//
//
//
// ACTIVITIES LIST CHART STYLE VIEW
// --------------------------------
//
//
//
const ChartView = Vue.component('chartview', {
props: [ 'day'],
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:{},
filters: {'sp23':['2023-01-01','2023-02-02'],'fa22':['2022-08-17','2022-08-20'], 'sp22':['2022-01-26','2022-01-30'], 'all':['2022-01-01','2023-11-30'] },
//day_titles: {'Aug 18 2022':" - Optional PD Day", 'Aug 19 2022':' - Convocation Day'},
day_titles: {'Jan 26 2023':" - Optional Day", 'Jan 27 2023':' - Mandatory Day'},
time_labels: [['0900','0930','9am'],
['0930','1000','9:30am'],
['1000','1030','10am'],
['1030','1100','10:30am'],
['1100','1130','11am'],
['1130','1200','11:30am'],
['1200','1230','12pm'],
['1230','1300','12:30pm'],
['1300','1330','1pm'],
['1330','1400','1:30pm'],
['1400','1430','2pm'],
['1430','1500','2:30pm'],
['1500','1530','3pm'],
['1530','1600','3:30pm'],
['1600','1630','4pm'],
['1630','1700','4:30pm'],
['1700','1730','5pm'],
['1730','1800','5:30pm'],
],
hosts_by_sesid: {}, } },
mounted: function() {
this.$root.fetch_menus();
this.fetch_myevents()
},
methods: {
hoststr: function(id) { var self = this
return _.reduce( self.hosts_by_sesid[id], function(mem,val) { if (val.name) { return mem + val.name + ", " } return mem }, '') },
mode_string: function(a) { return _.findWhere(this.$root.modes_menu, { 'id': a.mode })['string'] },
get_day_title: function(day) { if (day in this.day_titles) { return this.day_titles[day] } return '' },
month_year: function(d) { var b = this.$root.$dj(d).format('MMM D YYYY'); return b },
setsort: function(ss) {
if (this.sortby == ss) { this.reversed = ! this.reversed }
else { this.reversed = false; this.sortby = ss } },
swapme: function(x) { this.editing = x },
done_edit: function(id) { this.editing = -1 },
am_editing: function(id) { return 0 },
fetch_myevents: function() {
var self = this;
basic_get('api2.php?query=ses/'+self.day,
function(r2) {
r2 = r2.data
self.activities = _.sortBy(r2,function(x) { return x.starttime } ); self.$forceUpdate()
self.active += 1;
_.each( self.activities, function(x) {
var field = x.starttime.match(/^(\d\d\d\d)\-(\d+)\-(\d+)\s(\d+)\:(\d+)\:(\d+)$/)
var mydate = new Date(field[1], field[2] - 1 , field[3], field[4], field[5], field[6])
x.dj = dayjs(mydate)
x.searchable = x.title.toLowerCase() + ' ' + x.desc.toLowerCase() } )
self.$forceUpdate();
self.active += 1; } )
basic_get('dir_api.php?a=get/hosts', function(r2) {
self.hosts_by_sesid = _.groupBy(r2,function(x) { return x.id } )
} )
},
joinme: function(id) {
var self = this
basic_get('dir_api.php?a=signup/' + id,
function(r2) {
self.mysessions.push(_.findWhere(self.activities, {'id':id}))
self.my_ses_ids.push(id)
alert_message("Added activity") })
},
dumpme: function(id) {
var self = this
basic_get('dir_api.php?a=signdown/' + id,
function(r2) {
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") })
},
filtered: function(ff) { if (this.active<1) { return [] }
var self = this
if (this.search) {
var ss = self.search.toLowerCase()
ff = ff.filter(function(x) { return ('searchable' in x ? x.searchable.includes(ss) : 0) }) }
if (this.focus && 'options' in this.$root.$data.user ) {
var start = dayjs(this.$root.$data.user.options.conf.date1)
var end = dayjs(this.$root.$data.user.options.conf.date2)
ff = ff.filter( function(item,index) {
this_time = dayjs(item.starttime)
return this_time.isBefore(end) && start.isBefore(this_time) } )
}
ff = _.sortBy(ff, function(x) {
if (x[self.sortby]) { var s = x[self.sortby]; return s.trim().toLowerCase() }
return '' })
if (this.reversed) { ff.reverse() }
return ff }, },
computed: {
current_time: function() { return dayjs().format('MMM D, h:ma') },
activities_filtered: function() { var a = this.filtered(this.activities); return a; },
mysessions_filtered: function() { return this.filtered(this.mysessions) },
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) } ); },
mysessions_g_filtered: function() { if (this.active<1) { return {} }
var self=this; return _.groupBy(self.mysessions_filtered, function(x) { return self.month_year(x.starttime) } ); }, },
watch: { },
template: `<div class="activitylist schedule">
<chartsesh v-for="a in activities_filtered" :title="a.title" :start="$root.$dj(a.starttime).format('HHmm')"
:end="$root.$dj(a.starttime).add(a.length,a.length>8?'minute':'hour').format('HHmm')"
:start_h="$root.$dj(a.starttime).format('h:mma')"
:end_h="$root.$dj(a.starttime).add(a.length,a.length>8?'minute':'hour').format('h:mma')"
:mode="mode_string(a)" :presenter="hoststr(a.id)"
:track="a.track"
:loc="a.location_irl">
</chartsesh>
<chartlabel v-for="a in time_labels" :start="a[0]" :end="a[1]" :title="a[2]"></chartlabel>
</div>` })
const basic_getP = (url) =>
new Promise((resolve, reject) =>
basic_get(url, (r) => resolve(r), (e) => reject(e))
);
const ActivityEditorList = Vue.component('activityeditorlist', {
data() {
return {
// data
activities: [],
mysessions: [],
hosts: {}, // all hosts keyed by conf_id
users: [], // everyone (names/users)
hostsBySessionId: {}, // sessionId -> [host objects]
byId: {}, // sessionId -> activity
// ui
search: "",
sortby: "starttime",
reversed: false,
selectedIndex: 0,
editingId: null,
// filters
show_filters: "all",
filters: {
fa23: ["2023-08-21", "2023-08-26"],
sp23: ["2023-01-01", "2023-02-02"],
fa22: ["2022-08-17", "2022-08-20"],
sp22: ["2022-01-26", "2022-01-30"],
all: ["2022-01-01", "2025-12-31"],
},
day_titles: {
"Jan 26 2023": " - Optional Day",
"Jan 27 2023": " - Mandatory Day",
"Aug 24 2023": " - Optional Day",
"Aug 25 2023": " - Mandatory Day",
},
active: 0,
lastScrollY: 0,
};
},
mounted() {
this.$root.fetch_menus();
this.fetchAll();
window.addEventListener("keydown", this.onKey);
},
beforeDestroy() {
window.removeEventListener("keydown", this.onKey);
},
methods: {
async fetchAll() {
this.active = 0;
const [
mysessions,
sessions,
hostsRows,
allhosts,
namesPayload,
] = await Promise.all([
basic_getP("dir_api.php?a=get/mysessions"),
basic_getP("dir_api.php?a=get/sessions"),
basic_getP("dir_api.php?a=get/hosts"),
basic_getP("dir_api.php?a=get/allhosts"),
basic_getP("dir_api.php?a=get/names"),
]);
// normalize hosts per session
const hostsBySessionId = _.groupBy(hostsRows, (x) => x.id);
Object.keys(hostsBySessionId).forEach((sid) => {
hostsBySessionId[sid] = hostsBySessionId[sid].filter(
(x) => x.hostid != null
);
});
const byId = _.indexBy(sessions, (x) => x.id);
this.activities = _.sortBy(sessions, (x) => x.starttime || "");
this.mysessions = _.sortBy(mysessions, (x) => x.starttime || "");
this.hosts = allhosts; // as provided (conf_id -> [host ids]?) if that's your shape
this.users = namesPayload.users || [];
this.hostsBySessionId = hostsBySessionId;
this.byId = byId;
this.active = 1;
// decorate sessions
sessions.forEach((x) => {
// precompute dayjs + searchable text
if (x.starttime) {
const m = x.starttime.match(
/^(\d{4})-(\d+)-(\d+)\s(\d+):(\d+):(\d+)$/
);
if (m) {
const d = new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]);
x.dj = dayjs(d);
} else {
x.dj = dayjs(x.starttime);
}
} else {
x.dj = dayjs.invalid();
}
x.searchable = ((x.title || "") + " " + (x.desc || "")).toLowerCase();
x.missing = this.computeMissing(x);
});
},
computeMissing(a) {
const miss = [];
required_fields = ["title", "starttime", "mode", "desc"];
if (a.mode=='inperson' || a.mode=='hybrid') { required_fields.push('location_irl')}
if (a.mode=='online' || a.mode=='hybrid') { required_fields.push('location')}
required_fields.forEach((k) => {
const v = a[k];
if (v === undefined || v === null || String(v).trim() === "") miss.push(k);
});
// special: no hosts
const int_id = parseInt(a.id)
if (!this.hostsBySessionId[a.id] || this.hostsBySessionId[int_id].length === 0) {
miss.push("hosts");
}
return miss;
},
get_day_title(day) {
return this.day_titles[day] || "";
},
month_year(d) {
return this.$root.$dj(d).format("MMM D YYYY");
},
setsort(ss) {
if (this.sortby === ss) this.reversed = !this.reversed;
else {
this.reversed = false;
this.sortby = ss;
}
},
filtered(list) {
if (!this.active) return [];
let ff = list;
if (this.search) {
const q = this.search.toLowerCase();
ff = ff.filter((x) => x.searchable.includes(q));
}
// AY filter example
const ay = this.$root.settings["default_ay"];
if (ay && this.$root.ay_menu && this.$root.ay_menu[ay]) {
const start = dayjs(this.$root.ay_menu[ay].begin);
const end = dayjs(this.$root.ay_menu[ay].end);
ff = ff.filter((item) => {
const t = dayjs(item.starttime);
return t.isValid() && t.isBefore(end) && start.isBefore(t);
});
}
ff = _.sortBy(ff, (x) => {
const val = x[this.sortby];
return typeof val === "string" ? val.trim().toLowerCase() : val || "";
});
if (this.reversed) ff.reverse();
return ff;
},
onKey(e) {
// global keyboard navigation
if (!this.activitiesFiltered.length) return;
const max = this.activitiesFiltered.length - 1;
if (e.key === "ArrowDown") {
this.selectedIndex = Math.min(max, this.selectedIndex + 1);
e.preventDefault();
} else if (e.key === "ArrowUp") {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
e.preventDefault();
} else if (e.key === "Enter" || e.key.toLowerCase() === "e") {
const a = this.activitiesFiltered[this.selectedIndex];
this.startEdit(a.id);
e.preventDefault();
} else if (e.key === "Escape") {
this.closeEdit();
e.preventDefault();
} else if (e.key === "/") {
// focus search
const el = this.$refs.search;
if (el && el.focus) el.focus();
e.preventDefault();
}
},
// child emits
updateActivity(payload) {
// payload: { id, patch } — decorate + recompute missing
const a = this.byId[payload.id];
Object.assign(a, payload.patch);
a.missing = this.computeMissing(a);
},
startEdit(id) {
this.lastScrollY = window.scrollY; // remember current scroll
this.editingId = id;
},
closeEdit() {
this.editingId = null;
this.$nextTick(() => {
window.scrollTo({ top: this.lastScrollY, behavior: "smooth" });
});
},
},
computed: {
activitiesFiltered() {
return this.filtered(this.activities);
},
groupedByDay() {
return _.groupBy(this.activitiesFiltered, (x) => this.month_year(x.starttime));
},
},
template: `<div class="activitylist">
<!-- Top bar -->
<div class="flex items-center gap-3 mb-3">
<input
ref="search"
v-model.trim="search"
placeholder="Search… (press / to focus)"
class="border px-2 py-1 rounded w-64"
/>
<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>
<span class="text-sm text-gray-600">↑/↓ select • Enter/E edit • Esc close</span>
</div>
<!-- Grid (spreadsheet-like) -->
<table class="sessiongrid w-full text-sm border-collapse">
<thead>
<tr class="text-left border-b">
<th class="py-2 pr-2">Time</th>
<th class="py-2 pr-2">Title</th>
<th class="py-2 pr-2">Mode</th>
<th class="py-2 pr-2">Location</th>
<th class="py-2 pr-2">Hosts</th>
<th class="py-2 pr-2">Missing</th>
<th class="py-2 pr-2"></th>
</tr>
</thead>
<tbody>
<tr v-for="(a, idx) in activitiesFiltered"
:key="a.id"
:class="['border-b', idx===selectedIndex ? 'bg-yellow-50' : '']">
<td class="py-2 pr-2 whitespace-nowrap">{{ $root.$dj(a.starttime).format('MMM D, h:mma') }}</td>
<td class="py-2 pr-2">{{ a.title }}</td>
<td class="py-2 pr-2 capitalize">{{ a.mode }}</td>
<td class="py-2 pr-2">{{ a.location_irl }}<br />{{ a.location }}</td>
<td class="py-2 pr-2">
<span v-for="(h,i) in (hostsBySessionId[a.id]||[])"
:key="h.hostid">
{{ h.name }}<span v-if="i < (hostsBySessionId[a.id]||[]).length-1">, </span>
</span>
</td>
<td class="py-2 pr-2">
<span v-if="a.missing.length" class="inline-flex items-center gap-1">
<strong>{{ a.missing.length }}</strong>
<span class="text-gray-500">(
{{ a.missing.join(', ') }}
)</span>
</span>
<span v-else class="text-green-600">✓</span>
</td>
<td class="py-2 pr-2">
<button class="text-blue-600 underline"
@click.prevent="editingId = a.id">
Edit
</button>
</td>
</tr>
</tbody>
</table>
<!-- Inline editor (single expanded row) -->
<div v-if="editingId" class="mt-4">
<activityrow
:activity="byId[editingId]"
:hosts="hostsBySessionId[editingId] || []"
:everyone="users"
@patch="updateActivity"
@close="closeEdit"
/>
</div>
</div>
</template>`
})
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
//
// DISPLAY / EDIT activity or event
//
const ActivityRow = Vue.component('activityrow', { props: {
activity: { type: Object, required: true },
hosts: { type: Array, default: () => [] },
everyone: { type: Array, default: () => [] },
},
data() {
return {
editing: true,
host_search_str: "",
host_search: [],
this_hosts: [...this.hosts], // local copy for UX
};
},
watch: {
host_search_str(val) {
const q = (val || "").toLowerCase();
this.host_search = q
? _.first(
this.everyone.filter((x) => x.name.toLowerCase().includes(q)),
7
)
: [];
},
},
methods: {
// host ops (call API ONCE per click, not per render)
async removeHost(hostid) {
await new Promise((resolve, reject) =>
basic_get(`dir_api.php?a=remove/host/${this.activity.id}/${hostid}`, () => resolve(), reject)
);
this.this_hosts = this.this_hosts.filter((x) => x.hostid !== hostid);
this.emitPatch({ hostsChanged: true });
alert_message("Saved");
},
async addHost(hostid) {
await new Promise((resolve, reject) =>
basic_get(`dir_api.php?a=add/host/${this.activity.id}/${hostid}`, () => resolve(), reject)
);
const found = _.findWhere(this.everyone, { id: hostid });
if (found) this.this_hosts.push({ ...found, hostid: hostid });
this.host_search_str = "";
this.emitPatch({ hostsChanged: true });
alert_message("Saved");
},
emitPatch(extra = {}) {
// send minimal changes up; parent recomputes "missing"
this.$emit("patch", { id: this.activity.id, patch: { ...extra } });
},
close() {
this.editing = false;
this.$emit("close");
},
},
mounted() {
this.$nextTick(() => {
const fieldComp = this.$refs.titleField;
console.log(fieldComp)
const inputEl = fieldComp?.$el?.querySelector("input, textarea");
console.log(inputEl)
if (inputEl) {
inputEl.scrollIntoView({ behavior: "smooth", block: "center" });
inputEl.focus();
}
});
// Listen for Escape while in edit mode
this.keyHandler = (e) => {
if (e.key === "Escape") {
this.$emit("close");
}
};
window.addEventListener("keydown", this.keyHandler);
},
beforeDestroy() {
window.removeEventListener("keydown", this.keyHandler);
},
template: `<div class="bg-white rounded-lg shadow p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-base">Edit: {{ activity.title || '(untitled)' }}</h3>
<button @click.prevent="close" class="text-blue-600 underline">Done</button>
</div>
<!-- Use your existing field components so persistence stays identical -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<field myclass="double" table="conf_sessions" qid="title"
:answer="activity.title" :targetid="activity.id" question="Title"
ref="titleField"
@saved="emitPatch()" />
<dtpicker table="conf_sessions" qid="starttime"
:answer="activity.starttime"
:targetid="activity.id" question="Starts at"
@saved="emitPatch()" />
<field myclass="double" table="conf_sessions" qid="length"
:answer="activity.length" :targetid="activity.id" question="Length (hrs)"
@saved="emitPatch()" />
<selectmenu_fa table="conf_sessions" qid="mode"
:answer="activity.mode" menu="modes_menu"
:targetid="activity.id" question="Mode" labelfield="string"
@saved="emitPatch()" />
<field myclass="double" table="conf_sessions" qid="location"
:answer="activity.location" :targetid="activity.id" question="Location / Zoom"
@saved="emitPatch()" />
<field myclass="double" table="conf_sessions" qid="location_irl"
:answer="activity.location_irl" :targetid="activity.id" question="Location / In Person"
@saved="emitPatch()" />
</div>
<!-- Hosts -->
<div class="mt-4">
<label class="block text-sm font-medium mb-1">Hosts</label>
<div class="border rounded p-2 bg-gray-50 space-y-1">
<div v-for="h in this_hosts" :key="h.hostid" class="flex items-center justify-between">
<span>{{ h.name }}</span>
<button class="text-red-600 font-semibold" @click="removeHost(h.hostid)">×</button>
</div>
<div v-if="!this_hosts.length" class="text-gray-500 italic">None yet</div>
</div>
<div class="mt-2">
<input class="border rounded px-2 py-1 w-full"
v-model="host_search_str" placeholder="Type to add host…" />
<div class="border rounded p-2 bg-white mt-1" v-if="host_search.length">
<div v-for="ho in host_search" :key="ho.id"
class="flex items-center justify-between text-sm">
<span @click="addHost(ho.id)" class="cursor-pointer underline">{{ ho.name }}</span>
<button class="text-blue-600 font-semibold" @click="addHost(ho.id)">+</button>
</div>
</div>
</div>
</div>
<!-- Description / Type / Parent / Recording -->
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<htfield_fa table="conf_sessions" qid="desc" question="Description"
myclass="pell" :answer="activity.desc" :targetid="activity.id"
@saved="emitPatch()" />
<div>
<selectmenu_fa table="conf_sessions" qid="type"
:answer="activity.type" menu="sessiontypes_menu"
:targetid="activity.id" question="Type" labelfield="type"
@saved="emitPatch()" />
<selectmenu_fa v-if="String(activity.type) === '101'"
table="conf_sessions" qid="parent"
:answer="activity.parent" menu="parents_menu"
:targetid="activity.id" question="What flex day?" labelfield="title"
@saved="emitPatch()" />
</div>
<field myclass="double" table="conf_sessions" qid="recording"
:answer="activity.recording" :targetid="activity.id" question="Recording Link"
@saved="emitPatch()" />
</div>
</div>
</template>`
})
//
//
//
//
//
// Table of all signups and hosts in a conference
// ------------------------------
//
//
//
const Overview = Vue.component('overview', {
name: 'Overview',
data() {
return {
sessions: [
// Define your sessions data here
// Each session should have an id and title property
],
users: [
// Define your users data here
// Each user should have an id and name property
],
signups: [
// Define your signups data here
// Each signup should have a userId and sessionId property
],
hosts: [
// Define your hosts data here
// Each host should have a userId and sessionId property
],
fetched:0,
};
},
computed: {
sortedSessions() {
// Sort sessions by startdate (modify the property name as per your data)
return this.sessions.sort((a, b) => a.starttime.localeCompare(b.starttime));
},
sortedUsers() {
// Sort users by name (modify the property name as per your data)
return this.users.sort((a, b) => a.name.localeCompare(b.name));
},
filteredUsers() {
// Filter users who have signup or host entries
var sorted = this.users.sort((a, b) => a.name.localeCompare(b.name))
return sorted.filter((user) =>
this.signups.some((signup) => signup.user === user.id) ||
this.hosts.some((host) => host.user === user.id)
);
},
},
methods: {
start: function() {
var self = this
basic_get('dir_api.php?a=get/sessions',
function(r2) { self.sessions = r2; self.fetched += 1; })
basic_get('dir_api.php?a=get/hosttable',
function(r2) { self.hosts = r2; self.fetched += 1; })
basic_get('dir_api.php?a=get/signups',
function(r2) { self.signups = r2; self.fetched += 1; })
basic_get('dir_api.php?a=get/users',
function(r2) { self.users = r2; self.fetched += 1; })
},
getSignupHostStatus(userId, sessionId) {
// Check if the user signed up for the session
// Check if the user hosted the session
const host = this.hosts.find((host) => host.host === userId && host.session === sessionId);
if (host) {
return 'H'; // User hosted
}
const signup = this.signups.find((signup) => signup.user === userId && signup.session === sessionId);
if (signup) {
return 'S'; // User signed up
}
return ''; // User didn't sign up or host
}
},
mounted: function() { this.start() },
template: `<div><table v-if="fetched>3" class="overviewtable">
<thead>
<tr>
<th></th> <!-- Empty header for user names column -->
<!-- Iterate over sessions and display as columns -->
<th v-for="session in sortedSessions" :key="session.id" class="rotate"><div>{{ session.title }}</div></th>
</tr>
</thead>
<tbody>
<!-- Iterate over users and display as rows -->
<tr v-for="user in filteredUsers" :key="user.id">
<td>{{ user.name }}</td> <!-- User name column -->
<!-- Iterate over sessions and display signup/host status -->
<td v-for="session in sortedSessions" :key="session.id">
{{ getSignupHostStatus(user.id, session.id) }}
</td>
</tr>
</tbody>
</table>
</div>`
});
/*
- show upcoming, past, dropdown for category
- show signup status, signup/cancel button
- navigation: check permission, show edit / new button
- page: create / edit activity.
*/
const AskSurvey = Vue.component('asksurvey', {
name: 'AskSurvey',
components: { 'essay-question': STQuestion, 'number-question': NQuestioon },
props: ['id', ],
data () { return {
questions: [] } },
methods: {
start: function() {
var self = this
basic_get('dir_api.php?a=get/questions/'+self.id,
function(r2) {
self.questions = r2 })
},
},
mounted: function() { this.start() },
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>`
});
const ShowSurveys = Vue.component('show-survey', {
name: 'ShowSurveys',
//components: { 'essay-question': EssayQuestion, 'number-question': NumberQuestion },
props: ['answer', ],
methods: {
logger: function() { console.log('logger'); console.log(this); console.log( this.answer ); return ''; },
ses_name: function() {
if (this.answer) {
return this.answer[0][0].s_title }
return '' }
},
template: `<div>
{{ logger() }}
<div class="session-survey" v-for="q in answer">
<b>{{ q[0]['question'] }}</b>
<number-question v-if="q[0]['type']=='2'" :aa="q"></number-question>
<essay-question v-if="q[0]['type']=='1'" :aa="q"></essay-question>
</div>
</div>`
});
const Survey = Vue.component('surveydisplay', {
props: [ 'answers' ],
data () {
return {
qa_all: [ ],
sesh: {},
questions: [],
one: {},
qdata: [],
answer: ""
}
},
created: function() {
var self = this
self.questions = _.where(this.$root.$data.myquestions,
{'ses_id':self.$route.params.take_ses_id})
self.$forceUpdate()
/*this.$axios.get(this.$server + this.$api + '?a=get/questions', {withCredentials: true}).then( function(resp2) {
self.qdata = resp2.data
setTimeout(self.continueExecution, 1000) //wait 1 seconds before continuing
} )*/
this.$axios.get(this.$server + this.$api + '?a=get/answers/all', {withCredentials: true}).then( function(resp2) {
} ) },
methods: {
continueExecution: function() {self.questions = _.where( this.qdata, {'ses_id': this.$route.params.take_ses_id } )},
one_session: function() {
if (this.$route.params.take_ses_id) {
var self = this
var my_id = self.$route.params.take_ses_id
var my_ses = _.find(this.$root.$data.activities, function(x) { return x.id==my_id } )
return my_ses }
return {'title':''}
},
answers: function() {
var a = this.sesh[parseInt( this.$route.params.ses_id)]
if (a) { return a }
return 0
},
requested_session: function() {
var self = this
return _.find(this.$root.$data.activities, function(x) { return x.id == self.$route.params.ses_id } )
/*
a = _.find(this.$root.$data.activities, function(x) { return x.id == self.$route.params.ses_id } )
return a */
}
},
template: `<div class="session-survey" v-for="q in answer">
<b>{{ q[0]['question'] }}</b>
<number-question v-if="q[0]['type']=='2'" :aa="q"></number-question>
<essay-question v-if="q[0]['type']=='1'" :aa="q"></essay-question>
</div>
<!-- <div>
<show-survey v-if="answers()" :answer="answers()"></show-survey>
<span v-else><ul><li>No responses yet</li></ul>
</div>
Show admin all classes -->
<br /> &nbsp;
<br /> &nbsp;
<br /> &nbsp;
</div>
`
})
// ACTIVITIY REPORT - SINGLE - SIGNUPS & SURVEYS
// -----------------------------------
//
//
//
const ActivityInfoReport = Vue.component('activityinforeport', {
props: [ 'a', 'host','num','user','emails','survey' ],
methods: {
mode_string: function(a) { return _.findWhere(this.$root.modes_menu, { 'id': a.mode })['string'] },
},
computed: {
},
watch: { },
template: `<div class="report">
<div class="pure-g pure-form">
<div class="pure-u-7-24">
{{ $root.$dj(a.starttime).format('YYYY MMM DD dd h:mma') }} - {{mode_string(a)}}
</div>
<div class="pure-u-17-24">
<b>{{a.title}}</b>
<span v-if="a.location"><br />{{ a.location }}</span>
<div class="rhs_grey">{{a.typeStr}}</div>
</div>
</div>
<div class="pure-g"><div class="pure-u-1-1">
<a :href="'ed_act.php?w='+a.id" class="button-inlist">Edit</a>
</div></div>
<expandybox v-if="a.desc" header="Description" :body="a.desc"></expandybox>
<expandybox v-if="host" header="Hosts" :body="host"></expandybox>
<expandybox v-if="user" :header="'Attendees (' + num + ')'" :body="user"></expandybox>
<expandybox v-if="emails" :header="'Emails'" :body="emails"></expandybox>
<expandybox v-if="survey" header="Surveys" :body="survey"></expandybox>
</div>
</div>` })
// A single page version, suitable for emailing.
const ActivityInfoReport2 = Vue.component('activityinforeport2', {
props: [ 'a', 'host','num','user','emails','survey' ],
methods: {
mode_string: function(a) { return _.findWhere(this.$root.modes_menu, { 'id': a.mode })['string'] },
},
computed: {
},
watch: { },
template: `<div class="bg-white rounded-md shadow-md p-6 text-sm text-gray-800 space-y-4">
<!-- Title -->
<table class="w-full border-collapse table-fixed text-sm">
<tbody>
<tr>
<th class="text-left align-top pr-4 py-1 w-32">Title:</th>
<td class="py-1 font-semibold">{{ $wrap(a.title) }}</td>
</tr>
<tr>
<th class="text-left align-top pr-4 py-1">Date:</th>
<td class="py-1">{{ $root.$dj(a.starttime).format('YYYY MMM DD dd h:mma') }}</td>
</tr>
<tr>
<th class="text-left align-top pr-4 py-1">Mode / Location:</th>
<td class="py-1">
{{ mode_string(a) }}
<div v-if="a.location" class="text-gray-600">{{ $wrap(a.location) }}</div>
<div v-if="a.location_irl" class="text-gray-600">{{ $wrap(a.location_irl) }}</div>
</td>
</tr>
<tr v-if="a.desc">
<th class="text-left align-top pr-4 py-1">Description:</th>
<td class="py-1" v-html="a.desc"></td>
</tr>
<tr v-if="host">
<th class="text-left align-top pr-4 py-1">Hosts:</th>
<td class="py-1">{{ host }}</td>
</tr>
<tr>
<th class="text-left align-top pr-4 py-1">Attendees:</th>
<td class="py-1">{{ user }}</td>
</tr>
<tr v-if="survey">
<th class="text-left align-top pr-4 py-1">Survey Results:</th>
<td class="py-1" v-html="survey"></td>
</tr>
</tbody>
</table>
</div>
` })
// Copy-friendly table layout for Word/Docs
const ActivityInfoReportTable = Vue.component('activityinforeport_table', {
props: [ 'a', 'host','num','user','emails','survey' ],
methods: {
mode_string: function(a) { return _.findWhere(this.$root.modes_menu, { 'id': a.mode })['string'] },
},
template: `<div class="bg-white rounded-md shadow-md p-4 text-sm text-gray-800 space-y-2">
<table class="w-full border-collapse table-fixed">
<tbody>
<tr>
<th class="text-left align-top pr-4 py-1 w-28">Title:</th>
<td class="py-1 font-semibold">{{ $wrap(a.title) }}</td>
</tr>
<tr>
<th class="text-left align-top pr-4 py-1">Date:</th>
<td class="py-1">{{ $root.$dj(a.starttime).format('YYYY MMM DD dd h:mma') }}</td>
</tr>
<tr>
<th class="text-left align-top pr-4 py-1">Mode / Location:</th>
<td class="py-1">
{{ mode_string(a) }}
<div v-if="a.location">{{ $wrap(a.location) }}</div>
<div v-if="a.location_irl">{{ $wrap(a.location_irl) }}</div>
</td>
</tr>
<tr v-if="a.desc">
<th class="text-left align-top pr-4 py-1">Description:</th>
<td class="py-1" v-html="a.desc"></td>
</tr>
<tr v-if="host">
<th class="text-left align-top pr-4 py-1">Hosts:</th>
<td class="py-1">{{ host }}</td>
</tr>
<tr>
<th class="text-left align-top pr-4 py-1">Attendees:</th>
<td class="py-1">{{ user }}</td>
</tr>
<tr v-if="survey">
<th class="text-left align-top pr-4 py-1">Survey Results:</th>
<td class="py-1" v-html="survey"></td>
</tr>
</tbody>
</table>
</div>`
})
// Workshop history (signed up and hosted), grouped by semester
const WorkshopHistory = Vue.component('workshophistory', {
props: [ '' ],
data: function () { return {
ready: false,
sessionsLoaded: false,
sessions: [], // sessions user signed up for or hosted
hostsBySes: {},
rostersBySes: {},
answersBySes: {}, // numeric-only for averages
grouped: {}, // { 'Fall 2024': [sessions...] }
collapsed: {}, // { 'Fall 2024': true|false }
} },
mounted: function() {
var self = this
const init = function() { self.loadData() }
if (this.$root && this.$root.active) { init() } else { this.$root.do_after_load(init) }
},
methods: {
semesterLabel(dt) {
const d = dayjs(dt)
const y = d.year()
const m = d.month() + 1
if (m >= 8) return `Fall ${y}`
return `Spring ${y}`
},
loadData() {
var self = this
const uid = this.$root.user.conf_id
if (!uid) { this.ready = true; return }
// 1) sessions for this user (signed up or hosted) across all time
basic_get(`dir_api.php?a=get/sessions/${uid}`,
function(r2) {
self.sessions = _.sortBy(r2, s => s.starttime)
self.buildGroups()
self.sessionsLoaded = true
self.ready = true
self.$forceUpdate()
})
// 2) support data for hosts, rosters, survey answers
basic_get('dir_api.php?a=get/hosts&all=1', function(r2) {
self.hostsBySes = _.groupBy(r2, x => x.id)
self.$forceUpdate()
})
basic_get('dir_api.php?a=get/rosters&all=1', function(r2) {
self.rostersBySes = _.groupBy(r2, x => x.sesid)
self.$forceUpdate()
})
basic_get('dir_api.php?a=get/answers/all&all=1', function(r2) {
// keep only numeric answers per session
const bySes = _.groupBy(r2, a => a.ses_id)
_.each(bySes, function(list, sid) {
const nums = []
_.each(list, function(a) {
const v = parseFloat(a.answer)
if (!isNaN(v)) nums.push(v)
})
if (nums.length) self.answersBySes[sid] = nums
})
self.$forceUpdate()
})
// ready is set when sessions load; keep other data loading async
},
buildGroups() {
const self = this
const out = {}
_.each(this.sessions, function(s) {
const label = self.semesterLabel(s.starttime)
if (!out[label]) out[label] = []
out[label].push(s)
})
// sort newest semester first, and sessions within by date desc
this.grouped = {}
const order = _.sortBy(Object.keys(out), lbl => {
const m = lbl.match(/(Fall|Spring)\s(\d{4})/)
if (!m) return 0
const season = m[1] === 'Fall' ? 2 : 1
const year = parseInt(m[2])
return -(year*10 + season)
})
_.each(order, function(lbl) {
self.grouped[lbl] = _.sortBy(out[lbl], s => -dayjs(s.starttime).valueOf())
if (self.collapsed[lbl] === undefined) self.$set(self.collapsed, lbl, false)
})
},
isHost(sessionId) {
const uid = this.$root.user.conf_id
const hosts = this.hostsBySes[sessionId] || []
return _.some(hosts, h => String(h.hostid) === String(uid))
},
hostNames(sessionId) {
const hosts = this.hostsBySes[sessionId] || []
// Filter out null/empty names and dedupe
const names = _.chain(hosts)
.pluck('name')
.filter(n => n && String(n).trim().length)
.uniq()
.value()
return names.join(', ')
},
rosterCount(sessionId) {
return (this.rostersBySes[sessionId] || []).length
},
avgRating(sessionId) {
const nums = this.answersBySes[sessionId]
if (!nums || !nums.length) return null
const sum = _.reduce(nums, (m,v)=>m+v, 0)
return (sum/nums.length).toFixed(2)
},
toggle(label) { this.$set(this.collapsed, label, !this.collapsed[label]) }
},
template: `<div class="space-y-6">
<div v-if="!sessionsLoaded" class="text-gray-600 text-sm">Loading...</div>
<div v-else-if="Object.keys(grouped).length === 0" class="text-gray-600 text-sm">No workshop history yet.</div>
<div v-for="(list, label) in grouped" :key="label" class="">
<div class="flex items-center justify-between border-b pb-1 mb-3 clickme" @click="toggle(label)">
<div class="text-lg font-semibold text-gray-700">{{ label }}</div>
<button @click.stop="toggle(label)" class="text-sm text-blue-600 hover:underline">
{{ collapsed[label] ? 'Expand' : 'Collapse' }}
</button>
</div>
<div v-show="!collapsed[label]" class="grid gap-4 md:grid-cols-2">
<div v-for="s in list" :key="s.id" class="bg-white rounded-md shadow p-4">
<div class="flex items-start justify-between">
<div>
<div class="text-base font-semibold text-gray-900">{{ s.title }}</div>
<div class="text-sm text-gray-600">{{ $root.$dj(s.starttime).format('YYYY MMM DD, ddd h:mma') }}</div>
<div v-if="hostNames(s.id)" class="text-xs text-gray-500">Hosts: {{ hostNames(s.id) }}</div>
</div>
<div v-if="isHost(s.id)" class="ml-3 inline-flex items-center px-2 py-0.5 text-xs font-medium rounded bg-purple-100 text-purple-700">Host</div>
</div>
<div class="mt-3 flex flex-wrap gap-4 text-sm text-gray-700">
<div>Signups: <span class="font-medium">{{ rosterCount(s.id) }}</span></div>
<div v-if="avgRating(s.id)">Avg Rating: <span class="font-medium">{{ avgRating(s.id) }}</span></div>
</div>
<div class="mt-3">
<a v-if="isHost(s.id)" :href="'report.php?s=' + s.id" class="text-sm text-blue-600 hover:underline">Info</a>
</div>
</div>
</div>
</div>
</div>`
})
//
//
//
//
//
//
// ACTIVITIES LIST - SIGNUPS & SURVEYS
// -----------------------------------
//
//
//
const ActivityReport = Vue.component('activityreport', {
props: [ 'which' ],
data: function () {
return { activities:[],hosts:[], hosts_by_sesid:[], everyone:[], questions:[], answers:{}, answers2:{}, rosters:[],
semesters:[], selectedSemesterKey:'', q:'', single:false } },
mounted: function() {
var self = this
self.buildSemesters()
self.single = (self.which && String(self.which).match(/^\d+$/)) ? true : false
self.fetchRange()
basic_get('dir_api.php?a=get/names', function(r2) {
self.everyone = []
_.each( r2.users, function(x) { self.everyone[x.id] = x.name } ) } )
basic_get('dir_api.php?a=get/questions', function(r2) {
self.questions = r2 })
},
methods: {
buildSemesters: function() {
// Generate semesters: Spring (Jan 1Jul 31) and Fall (Aug 1Dec 31), starting 8/2023
const out = []
const start = dayjs('2023-08-01')
const endLimit = dayjs().add(2, 'year')
let cursor = start
while (cursor.isBefore(endLimit)) {
const y = cursor.year()
// Fall
const fallStart = dayjs(`${y}-08-01`)
const fallEnd = dayjs(`${y}-12-31`).endOf('day')
if (fallStart.isAfter(start)) {
out.push({ key: `fall-${y}`, label: `Fall ${y}`, start: fallStart, end: fallEnd })
} else {
// include Fall 2023 explicitly
out.push({ key: `fall-2023`, label: `Fall 2023`, start: dayjs('2023-08-01'), end: dayjs('2023-12-31').endOf('day') })
}
// Spring of next year
const sy = y + 1
const springStart = dayjs(`${sy}-01-01`)
const springEnd = dayjs(`${sy}-07-31`).endOf('day')
out.push({ key: `spring-${sy}`, label: `Spring ${sy}`, start: springStart, end: springEnd })
cursor = dayjs(`${sy}-08-01`)
}
// Sort newest first
this.semesters = _.sortBy(out, s => -s.start.unix())
// Default to current semester
const now = dayjs()
const current = this.semesters.find(s => now.isAfter(s.start) && now.isBefore(s.end))
this.selectedSemesterKey = current ? current.key : (this.semesters[0] ? this.semesters[0].key : '')
},
fetchRange: function() {
const sem = this.selectedSemester
let q = 'all=1'
if (this.single) {
q = `id=${this.which}`
} else if (sem) {
q = `begin=${sem.start.format('YYYY-MM-DD')}&end=${sem.end.format('YYYY-MM-DD')}`
}
var self = this
basic_get(`dir_api.php?a=get/sessions&${q}`, function(r2) {
self.activities = _.sortBy(r2,function(x) { return x.starttime } )
_.each( self.activities, function(x) {
var field = x.starttime && x.starttime.match(/^(\d\d\d\d)\-(\d+)\-(\d+)\s(\d+)\:(\d+)\:(\d+)$/)
if (field) {
var mydate = new Date(field[1], field[2] - 1 , field[3], field[4], field[5], field[6])
x.dj = dayjs(mydate)
}
x.searchable = ((x.title||'') + ' ' + (x.desc||'')).toLowerCase() } )
self.$forceUpdate();
})
basic_get(`dir_api.php?a=get/hosts&${this.single ? ('id=' + this.which) : q}`, function(r2) {
self.hosts_by_sesid = _.groupBy(r2,function(x) { return x.id } )
})
basic_get(`dir_api.php?a=get/rosters&${this.single ? ('id=' + this.which) : q}`, function(r2) {
self.rosters = _.groupBy(r2,function(x) { return x.sesid } )
})
basic_get(`dir_api.php?a=get/answers/all&${this.single ? ('id=' + this.which) : q}`, function(r2) {
var organized = _.groupBy(r2, function(x) { return x.ses_id; } )
self.answers = {}
self.answers2 = {}
_.each( organized, function(val,key,lis) { self.answers[key] = _.groupBy( val, function(y) { return y.q_id; }); } )
_.each( self.answers, function(val,key,lis) { self.answers2[key] = _.sortBy( val, "q_id" ) } )
self.$forceUpdate()
})
},
hoststr: function(id) {
var self = this
return _.reduce( self.hosts_by_sesid[id], function(mem,val) { if (val.name) { return mem + val.name + ", " } return mem }, '')
},
userstr: function(id) { return _.pluck(this.rosters[id], "name").join(', ') },
usernum: function(id) { if (id in this.rosters) { return this.rosters[id].length } return 0 },
useremails: function(id) { return _.pluck(this.rosters[id], "email").join('; ') },
surveystr: function(id) {
var self = this
var result = ""
if (this.answers2[id]) {
_.each( this.answers2[id], function (qlist) {
// try to build numeric histogram 15
var nums = []
_.each( qlist, function(qanswer) {
var v = parseInt(qanswer['answer'])
if (!isNaN(v) && v>=1 && v<=5) { nums.push(v) }
})
result += "<div style=\"margin:8px 0;\">"
result += "<b class='survey_q'>" + qlist[0]['question'] + "</b><br/>"
if (nums.length) {
var counts = [0,0,0,0,0,0] // index 1..5
var sum = 0
_.each(nums, function(v){ counts[v]+=1; sum+=v })
var maxc = _.max(counts.slice(1)) || 1
var avg = (sum/nums.length).toFixed(2)
// table-based bars with inline width; good for copy/paste
result += "<table style=\"border-collapse:collapse; margin-top:4px;\">"
for (var i=1;i<=5;i++) {
var c = counts[i]
var width = Math.round((c/maxc)*200) // px
result += "<tr>"
+ "<td style=\"padding:2px 6px 2px 0; font-weight:600; color:#374151;\">"+i+"</td>"
+ "<td style=\"padding:2px 6px;\">"
+ "<div style=\"width:200px; height:10px; background:#e5e7eb; display:inline-block; vertical-align:middle;\">"
+ "<div style=\"height:10px; width:"+width+"px; background:#3b82f6;\"></div>"
+ "</div>"
+ "<span style=\"display:inline-block; min-width:28px; margin-left:6px; color:#374151;\">"+c+"</span>"
+ "</td>"
+ "</tr>"
}
result += "</table>"
result += "<div style=\"font-size:12px; color:#6b7280; margin-top:2px;\">Responses: "+nums.length+", Average: "+avg+"</div>"
} else {
// fallback: list non-numeric answers
result += "<ul>"
_.each( qlist, function(qanswer) {
var txt = (qanswer['answer']||'')
// escape < and > to avoid breaking markup, then wrap long tokens
txt = String(txt).replace(/</g,'&lt;').replace(/>/g,'&gt;')
txt = wrapLongTokens(txt)
result += "<li>" + txt + "</li>\n"
})
result += "</ul>\n"
}
result += "</div>"
})
return result
}
return "<i>no survey results?</i>"
},
},
computed: {
selectedSemester() {
return this.semesters.find(s => s.key === this.selectedSemesterKey) || null
},
filteredActivities() {
const q = (this.q || '').toLowerCase().trim()
const sem = this.selectedSemester
return this.activities.filter(a => {
// semester filter
let ok = true
if (!this.single && sem) {
const t = dayjs(a.starttime)
ok = t.isAfter(sem.start) && t.isBefore(sem.end)
}
if (!ok) return false
// explicit id filter via prop 'which'
if (this.which && String(this.which).match(/^\d+$/) && String(a.id) !== String(this.which)) {
return false
}
// text filter against title/desc/hosts
if (this.single) return true
if (!q) return true
const hay = (a.title + ' ' + (a.desc||'') + ' ' + (this.hoststr(a.id)||'')).toLowerCase()
return hay.indexOf(q) !== -1
})
}
},
watch: {
selectedSemesterKey() { if (!this.single) this.fetchRange() }
},
template: `<div class="activityreport space-y-4">
<!-- Controls -->
<div v-if="!single" class="bg-white rounded-md shadow p-3 sticky top-16 md:static z-40">
<div class="flex flex-col gap-2 md:flex-row md:items-center">
<label class="text-sm text-gray-700">Semester
<select v-model="selectedSemesterKey" class="ml-2 border rounded px-2 py-1 text-sm">
<option :value="''">All</option>
<option v-for="s in semesters" :key="s.key" :value="s.key">{{ s.label }}</option>
</select>
</label>
<input type="search" v-model="q" placeholder="Search host or title" class="border rounded px-3 py-1 text-sm w-full md:w-64"/>
</div>
<div class="text-xs text-gray-500 mt-1">Showing {{ filteredActivities.length }} session(s)</div>
</div>
<!-- Report Items (table layout for copy/paste) -->
<div v-for="a in filteredActivities" :key="a.id">
<activityinforeport_table :a="a" :host="hoststr(a.id)" :num="usernum(a.id)" :user="userstr(a.id)" :emails="useremails(a.id)" :survey="surveystr(a.id)"></activityinforeport_table>
</div>
</div>` })
//
//
//
//
//
//
// TRAINING HISTORY - GOTT COURSES
// -------------------------------
//
//
//
const TrainingHistory = Vue.component('traininghistory', {
props: [ '' ],
data: function () {
return { training: {}, mycourses: {}, msg:"Loading..." } },
mounted: function() {
var self = this
basic_get('gott_by_goo.json', function(r2) {
self.training = r2;
setTimeout(() => { self.msg = "" }, 1500);
} )
},
computed: {
user_courses() {
if ('user' in this.$root && this.$root.active && this.training) { return this.training[this.$root.user.conf_goo] || null }
return {}
}
},
methods: {
formatDate(dateString) {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
},
watch: { },
template: `<div class="space-y-4">
<div class="bg-blue-50 text-blue-900 p-4 rounded-md shadow-sm">
<p class="font-medium">These are the GOTT courses you have taken.</p>
</div>
<div v-if="Object.keys(training).length > 0">
<p class="text-sm text-gray-700 mb-2">{{ msg }}</p>
<div class="space-y-3">
<div
v-for="(date, course) in user_courses"
:key="course"
class="flex flex-col md:flex-row md:items-center md:justify-between bg-white p-4 border rounded shadow-sm"
>
<div class="text-gray-800 font-medium">Course: {{ course }}</div>
<div class="text-gray-600 mt-1 md:mt-0">Completed: {{ formatDate(date) }}</div>
</div>
</div>
</div>
<div v-else>
<p class="text-gray-500 italic">No training history found.</p>
</div>
</div>` })
//
//
// _ _ _
// | | | | (_)
// ___ ___| |_| |_ _ _ __ __ _ ___
// / __|/ _ \ __| __| | '_ \ / _` / __|
// \__ \ __/ |_| |_| | | | | (_| \__ \
// |___/\___|\__|\__|_|_| |_|\__, |___/
// __/ |
// |___/
//
//
const Settings = Vue.component('settings', {
props: [ ],
data: function () {
return { 'zoom_on':'', 'survey_on':'', 'ay':'', 'default_conference':'' } },
mounted: function() {
var self = this
this.zoom_on = this.$parent.settings.zoom_on
this.survey_on = this.$parent.settings.survey_on
this.ay = this.$parent.settings.default_ay
this.default_conference = this.$parent.settings.default_conference
},
methods: {
},
computed: {
},
watch: { },
template: `<div class="settingspanel">
<ul>
<li>
<input class="form-check-input" type="checkbox" id="zoom_on" v-model="zoom_on">
<label class="icert form-check-label" for="zoom_on">Zoom Links Visible</label><br />
</li>
<li>
<input class="form-check-input" type="checkbox" id="survey_on" v-model="survey_on">
<label class="icert form-check-label" for="survey_on">Surveys Available</label><br />
</li>
<li>
<selectmenu table="conf_uinforecord" qid="value" :answer="this.ay" menu="ay_menu" :targetid="4" question="Academic Year: " labelfield="label"></selectmenu>
</li>
</ul>
</div>` })
// _ _
// | | | |
// ___ __ _| | ___ _ __ __| | __ _ _ __
// / __/ _` | |/ _ \ '_ \ / _` |/ _` | '__|
// | (_| (_| | | __/ | | | (_| | (_| | |
// \___\__,_|_|\___|_| |_|\__,_|\__,_|_|
//
//
// VERY SIMPLE CALENDAR
//
const MyCal = Vue.component('mycal', {
data: function () {
return { today:new Date(), currentMonth:0, currentYear:0, selectYear:'', selectMonth:'',
months:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
activities:[],
by_date: {},
editing: -1, } },
mounted: function() {
var self = this
this.currentYear = this.today.getFullYear()
this.currentMonth = this.today.getMonth()
this.selectYear = this.currentYear
this.selectMonth = this.currentMonth
basic_get('dir_api.php?a=get/sessions',
function(r2) { self.activities = _.map(r2, function(x) {
var dd = new Date(x.starttime);
var [m,d,y] = [dd.getMonth(), dd.getDate(), dd.getFullYear()] // months start at 0....
var d_string = d + "-" + m + "-" + y
if (self.by_date[d_string]) { self.by_date[d_string].push(x) }
else { self.by_date[d_string] = [x, ] } } )
self.$forceUpdate() } )
},
methods: {
thisDay: function(i,j) {
var dayOfMonth = ((7*(i-1))+j)-this.firstDay()
return dayOfMonth },
eventsThisDay: function(i,j) {
var dayOfMonth = ((7*(i-1))+j)-this.firstDay()
var d_string = dayOfMonth + "-" + this.selectMonth + "-" + this.selectYear
var day_text = ''
if (this.by_date[d_string]) {
evts = _.filter( this.by_date[d_string], function(x) { return x.typeId!="101" } )
return evts } return [] },
cleanTime: function(e) {
var dd = new Date(e.starttime);
var [h,m] = [dd.getHours(), dd.getMinutes()]
var ampm = 'am'
if (h > 12) { h -= 12; ampm = 'pm' }
if (m == 0) { m = '' }
else { m = ':' + m }
return h + m + ampm },
daysInMonth: function() {
return 32 - new Date(this.selectYear, this.selectMonth, 32).getDate() },
firstDay: function() {
return (new Date(this.selectYear, this.selectMonth)).getDay() },
next: function() {
this.selectYear = (this.selectYear === 11) ? this.selectYear + 1 : this.selectYear;
this.selectMonth = (this.selectMonth + 1) % 12; },
previous: function() {
this.selectYear = (this.selectYear === 0) ? this.selectYear - 1 : this.selectYear;
this.selectMonth = (this.selectMonth === 0) ? 11 : this.selectMonth - 1; },
},
computed: {
},
watch: {
},
template:`<div class="calendar">
<h3>{{months[selectMonth] + " " + selectYear}}
<div class="btn_container"><div class="btn_float">
<button class="pure-button" v-on:click="previous">Previous</button>
<button class="pure-button" v-on:click="next">Next</button>
</div></div>
</h3>
<table><thead><tr>
<th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th>
</tr></thead>
<tbody id="calendar-body">
<tr v-for="i in 6" v-if="(((7*(i-1)))-firstDay() < daysInMonth()+1)">
<td v-for="j in 7">
<span v-if="( ((7*(i-1))+j)-firstDay() > 0) && (((7*(i-1))+j)-firstDay() < daysInMonth()+1)">
<div class="do_month">{{ thisDay(i,j) }}</div>
<div class="cal_evt" v-for="ev in eventsThisDay(i,j)">{{cleanTime(ev)}} {{ ev.title }}</div>
</span>
</td>
</tr>
</tbody></table>
</div>` })
// _ _ _ _ _
// | | | | | | | | | |
// __ _____| | ___ ___ _ __ ___ ___ | | ___| |_| |_ ___ _ __ | |
// \ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \ | |/ _ \ __| __/ _ \ '__| | |
// \ V V / __/ | (_| (_) | | | | | | __/ | | __/ |_| || __/ | |_|
// \_/\_/ \___|_|\___\___/|_| |_| |_|\___| |_|\___|\__|\__\___|_| (_)
//
//
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
//
// WELCOME LETTER main component
//
const WelcomeLetter = Vue.component('welcomeletter', {
props: [ 'mysem','mycrn', 'teacher_ext_id', ],
data: function () {
return { courses_by_semester:[], wl_sem:'', wl_crn:'', section_wl:{}, sortby:'code', reversed:false, active:-1, } },
watch: {
"teacher_ext_id": function (val, oldVal) { this.fetch_sections() },
},
mounted: function() {
var self = this
if (this.mysem && this.mycrn) { this.fetch_wletters(this.mysem, this.mycrn) }
else { this.$root.do_after_load( self.fetch_sections) }
},
methods: {
swap_section: function(section_obj) { this.fetch_wletters(section_obj.sem,section_obj.crn) },
show_list: function() { this.section_wl = {} },
fetch_wletters: function(ss,cc) {
var self = this;
fetch('dir_api.php?a=get/section/' + ss + '/' + cc,
{ method: 'GET' }).then(function (response) {
// The API call was successful!
if (response.ok) {
response.json().then( function(r2) {
self.section_wl = r2;
} )
} else { return Promise.reject(response) }
}).then(function (data) {
}).catch(function (err) { console.warn('Something went wrong.', err); });
},
fetch_sections: function() {
var self = this;
basic_get('dir_api.php?a=get/sections/' + this.$root.user.ext_id,
function(r2) { self.courses_by_semester = _.groupBy(r2,function(x) { return x.sem || 0} ); self.$forceUpdate(); self.active += 1; } )
},
},
template: `<div class="">
<div v-if="section_wl.id" class="pure-g pure-form">
<div class="pure-u-24-24">
<div class="clicky" v-on:click="show_list()">back to list of sections</div>
<div class="">
Course:
</div>
<div class="">
Description:
</div>
<div class="">
Delivery Format:
</div>
<div class="">
Schedule:
</div>
<div class="">
Length:
</div>
<form class="pure-form pure-form-aligned">
<tfield table="welcome_letters" qid="introduction" question="Introduction" :answer="this.section_wl.introduction" :targetid="this.section_wl.wl_id"></tfield>
<tfield table="welcome_letters" qid="what_expect" question="What to Expect" :answer="this.section_wl.what_expect" :targetid="this.section_wl.wl_id"></tfield>
<tfield table="welcome_letters" qid="textbook" question="Textbook Information" :answer="this.section_wl.textbook" :targetid="this.section_wl.wl_id"></tfield>
<tfield table="welcome_letters" qid="assessments" question="Tests and Assessments" :answer="this.section_wl.assessments" :targetid="this.section_wl.wl_id"></tfield>
<tfield table="welcome_letters" qid="other_info" question="Other Useful Information" :answer="this.section_wl.other_info" :targetid="this.section_wl.wl_id"></tfield>
<tfield table="welcome_letters" qid="additional_resources" question="Additional Resources" :answer="this.section_wl.additional_resources" :targetid="this.section_wl.wl_id"></tfield>
</form>
</div>
</div>
<div v-else class="pure-g pure-form">
<div v-for="sect_list,sm in courses_by_semester" class="pure-u-24-24">
<b> {{sm}}</b><br />
<div v-for="crs in sect_list" class="clicky" v-on:click="swap_section(crs)">
&nbsp; &nbsp; &nbsp; {{crs.code}} - {{crs.crn}} - {{crs.name}}
</div>
</div>
</div>
</div>` })
// _ _ _
// | | | | | |
// _____ _____ _ __ | |_ ___ | |__ ___| |_ __ ___ _ __
// / _ \ \ / / _ \ '_ \| __/ __| | '_ \ / _ \ | '_ \ / _ \ '__|
// | __/\ V / __/ | | | |_\__ \ | | | | __/ | |_) | __/ |
// \___| \_/ \___|_| |_|\__|___/ |_| |_|\___|_| .__/ \___|_|
// | |
// |_| //
// AJAX POST UPDATES TO API
//
function post_update(table,cols,vals,id=0) {
action = "nothing"
//if (table=="update_survey") { action = "update" }
if (table=="personnel") { action = "update" }
if (table=="personnel_ext") { action = "update_xt" }
if (table=="conf_users") { action = "update_cf" }
if (table=="conf_sessions") { action = "update/activity" }
if (table=="webpages") { action = "update_web" }
if (table=="welcome_letters") { action = "update/letter" } // or insert?
if (table=="uniforecord") { action = "update/settings" }
var idstr = ""
if (id) { idstr = "&id=" + id }
fetch('dir_api.php', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' }),
body: "a="+action+"&cols="+cols+"&vals="+vals+idstr,
}).then(function (response) {
if (response.ok) {
response.json().then( function(r2) {
// display success alert
alert_message('Saved.')
} )
} else { return Promise.reject(response) }
}).then(function (data) {
}).catch(function (err) { alert_message("Couldn't save!",pink); console.warn('Something went wrong.', err); });
}
function generic_fail(err,x="Something went wrong with an ajax fetch") {
console.log(x); console.log(err) }
function basic_get( url, after_fxn, fail_fxn=generic_fail ) {
fetch(url, { method: 'GET' }).then(function (response) {
if (response.ok) {response.json().then( function(r2) { after_fxn(r2) } )
} else { return Promise.reject(response) }
}).then(function (data) { }).catch(function (err) { fail_fxn(err) } ) }
var evt = {
clear_tables: function() {
var self = this
_.each( this.tables, function(x) { self[x] = [] } )
this.data = {}
this.target_ids = {} },
send_update: function() {
var self = this
_.each( this.tables, function(x) {
if (self[x].length) {
var cols = ""
var vals = ""
_.each(self[x], function(y) {
if (cols.length) { cols += "," }
if (vals.length) { vals += "," }
cols += y
if (typeof self.data[y] == "string") {
re = /,/g
vals += encodeURIComponent(self.data[y].replace(re,'[CMA]') ) }
else { vals += self.data[y] }
})
var edit_other = 0
if (self.target_ids[x]) { edit_other = self.target_ids[x] }
post_update(x, cols, vals, edit_other)
}
} ) },
data: {},
target_ids: {},
tables: ['personnel','personnel_ext','webpages','welcome_letters','conf_sessions','conf_hosts',
'pers_departments','pers_committees','pers_titles'],
}
evt.clear_tables()
MicroEvent.mixin(evt)
// _ _____ _____
// (_) /\ | __ \| __ \
// _ __ ___ __ _ _ _ __ / \ | |__) | |__) |
// | '_ ` _ \ / _` | | '_ \ / /\ \ | ___/| ___/
// | | | | | | (_| | | | | | / ____ \| | | |
// |_| |_| |_|\__,_|_|_| |_| /_/ \_\_| |_|
//
//
var app = new Vue({
el: '#dir_editor',
data: { events: evt,
msg: 'hello', active: false, creating:0,
user: {'last_name':'', 'first_name':'', 'department':'', 'extension':'', 'phone_number':'', 'email':'',
'staff_type':'', 'room':'', 'status':'', 'user_id':'', 'password':'', 'time_created':'', 'time_updated':'',
'id':'', ext_id:false, 'web_on':'', use_dir_photo:0, general_photo_release:0, espanol:0, zoom:'', preferred_contact:'',
officehours:'', title:'', picture:'', education:'', bio:'', courses:'', personal_page:'', changed:'' },
settings:{},
filter: [],
roles_menu: [],
depts_menu: [],
titles_menu: [],
sessiontypes_menu: [],
parents_menu: [],
ay_menu: [],
modes_menu: [ {'id': 'Pending', 'string':'Pending'}, {'id':'online','string':'Online'}, {'id':'inperson','string':'In Person'}, {'id':'hybrid','string':'Hyflex'}, {'id':'','string':''}, ],
waiting_fxns: [],
data_loaded: 0,
committees_menu: [],
menus_fetched: false,
},
watch: {
'data_loaded': function(newVal,oldVal) {
if (newVal > 0) { _.each( this.waiting_fxns, function(fx) { fx() }) } }, },
methods: {
do_after_load: function(do_fxn) { this.waiting_fxns.push(do_fxn) /*....*/ },
fetch_menus: function() {
if (! this.menus_fetched) {
var self = this;
fetch('dir_api.php?a=menus', { method: 'GET' }).then(function (response) {
// The API call was successful!
if (response.ok) {
response.json().then( function(r2) {
self.depts_menu = r2.departments;
self.roles_menu = r2.roles;
self.titles_menu = r2.titles;
self.committees_menu = r2.committees;
self.sessiontypes_menu = r2.sessiontypes;
self.parents_menu = r2.parents;
self.menus_fetched = true;
} )
} else { return Promise.reject(response) }
}).then(function (data) {
}).catch(function (err) {
// FAILED TO LOAD. MOST LIKELY THE SSO/SESSION TIMED OUT
// .... reload whole page to get redirect...?
console.warn('Something went wrong.', err);
});
}
},
my_subscribe_calendar: function() { return "webcal://hhh.gavilan.edu/phowell/map/calendar" + this.user.conf_id + ".ics" },
clip_copy: function(x) {
// see the .htaccess file for the mod_rewrite that makes the ics file work. //
var data = [new ClipboardItem({ "text/plain": new Blob([this.my_subscribe_calendar()], { type: "text/plain" }) })];
navigator.clipboard.write(data).then(function() {
fadein_message()
}, function() { console.error("Unable to write to clipboard. :-("); }) }, },
computed: { },
mounted: function() {
var self = this;
fetch('api2.php?query=start', { method: 'GET' }).then(function (response) {
// The API call was successful!
if (response.ok) {
response.json().then( function(r2) {
var x = self.user.mysessions
self.user = r2.user;
self.depts_menu = r2.departments;
self.roles_menu = r2.roles;
self.titles_menu = r2.titles;
self.committees_menu = r2.committees;
self.sessiontypes_menu = r2.sessiontypes;
self.parents_menu = r2.parents;
self.ay_menu = r2.ay;
self.settings = r2.settings;
self.menus_fetched = true;
self.data_loaded += 1
// pause half a second for the children to get populated before registering update events...
setTimeout(function() {
self.active = true;
// fancier text editors...
//pell.init( { element: document.getElementById('bio2'), onChange: function(h) { console.log(h) } } )
}, 1600);
} )
} else { return Promise.reject(response) }
}).then(function (data) {
}).catch(function (err) { console.warn('Something went wrong.', err); }) } })
// _
// | |
// _____ _____ _ __ | |_ ___
// / _ \ \ / / _ \ '_ \| __/ __|
// | __/\ V / __/ | | | |_\__ \
// \___| \_/ \___|_| |_|\__|___/
//
//
//
// SIMPLE EVENTS
//
var update_fxn = _.debounce( function() {
alert_message('saving...','lightgreen')
evt.send_update(); evt.clear_tables(); }, 1300 )
var update_survey_fxn = function() {
}
evt.bind('update_survey',_.debounce( function(dat) {
if (app.active) {
fetch('dir_api.php?a=update/answers', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' }),
body: "session=" +dat[1] + "&user=" +dat[0] + "&qid=" +dat[2] + "&answer="+dat[3],
}).then(function (response) {
if (response.ok) {
response.json().then( function(r2) {
// display success alert
alert_message('Saved.')
} )
} else { return Promise.reject(response) }
}).then(function (data) {
}).catch(function (err) { alert_message("Couldn't save!",pink); console.warn('Something went wrong.', err); });
} } , 1300 ) );
evt.bind('changed', function(dat) {
if (app.active) {
var column = dat[0]
var table = dat[1]
var value = dat[2]
var target = dat[3]
this.data[column] = value
if (!this[table].includes(column) ) {this[table].push(column)}
if (target) { this.target_ids[table] = target } }
if (app.active && !app.creating) { update_fxn() }
});
evt.bind('create_new_session', function(dat) {
var default_activity = {"title":"","desc":"","length":"1","starttime":"","track":"","location":"","gets_survey":"1","category":"1",
"parent":"","recording":"","instructions":"","image_url":"","is_flex_approved":"1","typeId":"101"}
var new_activity = _.extend(default_activity, evt.data)
if ('typeId' in new_activity) { new_activity.type = new_activity.typeId; delete new_activity.typeId; }
let formData = new FormData();
_.each( Object.keys(new_activity), function(x) { formData.append(x, new_activity[x]) } )
fetch('dir_api.php?a=set/newsession', {
method: 'POST',
body: formData, }).then(function (response) {
if (response.ok) {
response.json().then( function(r2) {
alert_message('Saved new activity.')
app.$children[0].set_id(r2.new_id) } )
} else { return Promise.reject(response) }
}).then(function (data) {
}).catch(function (err) { alert_message("Couldn't create the activity!",pink); console.warn('Something went wrong.', err); }) })
// bold the current page
function bold_nav() {
var currentFileName = window.location.pathname.split('/').pop();
// Select the <a> tag with the matching href value and apply the class
$('#nav a[href="' + currentFileName + '"]').addClass('highlight');
}
$(document).ready(function() {
bold_nav()
})
//
//
// MISC
//
//
//
// <img :src="'//www.gavilan.edu/staff/' + s.dir_photo_path" width="25" height="auto" />
// v-lazy-container="{ selector: 'img' }"
//