change edit screen to grid style

This commit is contained in:
Peter Howell 2025-08-12 13:39:31 -07:00
parent d23d529f34
commit 833a4d689d
5 changed files with 459 additions and 388 deletions

View File

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

View File

@ -101,6 +101,8 @@ const TQuestion = Vue.component('field', {
"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 }}
@ -829,289 +831,6 @@ const ActivityEditor = Vue.component('activityedit', {
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
//
// DISPLAY / EDIT activity or event
//
const ActivityInlineEditor = Vue.component('activityinlineedit', {
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: 0, 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.id } )
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 + ' ' + x.desc
x.searchable = x.searchable.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) {
self.hosts_by_sesid = _.groupBy(r2,function(x) { return x.id } )
var unfiltered = self.hosts_by_sesid[ self.my_activity ]
self.this_hosts = unfiltered.filter(x => x.hostid != null)
} )
},
},
template: `<div class="">
<div v-if="editing" class="bg-white rounded-lg shadow p-4 mb-6">
<!-- Top action bar -->
<div class="flex justify-end mb-4">
<button
@click.prevent="editing = 0"
class="text-sm text-blue-600 hover:underline font-medium"
>
Done
</button>
</div>
<!-- Form fields -->
<div class="space-y-4">
<field
myclass="double"
table="conf_sessions"
qid="title"
:answer="this_activity.title"
:targetid="this_activity.id"
question="Title"
placeholder="Title"
/>
<dtpicker
table="conf_sessions"
qid="starttime"
:answer="this_activity.starttime"
:targetid="this_activity.id"
question="Starts at"
/>
<field
myclass="double"
table="conf_sessions"
qid="length"
:answer="this_activity.length"
:targetid="this_activity.id"
question="Length (in hours)"
placeholder="1"
/>
<selectmenu_fa
table="conf_sessions"
qid="mode"
:answer="this_activity.mode"
menu="modes_menu"
:targetid="this_activity.id"
question="What mode?"
labelfield="string"
/>
<field
myclass="double"
table="conf_sessions"
qid="location"
:answer="this_activity.location"
:targetid="this_activity.id"
question="Location / Zoom"
placeholder="Location"
/>
<field
myclass="double"
table="conf_sessions"
qid="location_irl"
:answer="this_activity.location_irl"
:targetid="this_activity.id"
question="Location / In Person"
placeholder="Location"
/>
<!-- Host List -->
<div v-if="!creating">
<label class="block text-sm font-medium text-gray-700 mb-1">Hosts</label>
<div class="border rounded-md p-3 bg-gray-50 space-y-1">
<div
v-for="h in this_hosts"
:key="h.hostid"
class="flex items-center justify-between text-sm"
>
<span>{{ h.name }}</span>
<button
type="button"
@click="remove(h.hostid)"
class="text-red-500 hover:text-red-700 font-semibold"
>
×
</button>
</div>
<div v-if="this_hosts.length === 0" class="text-sm text-gray-500 italic">
Add a host by typing below
</div>
</div>
</div>
<!-- Host Add Input + Suggestions -->
<div v-if="!creating">
<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 border-gray-500 bg-white text-gray-900 shadow focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 px-4 py-2 text-base mb-2"
/>
<div class="border rounded-md p-3 bg-gray-50 space-y-1">
<div
v-for="ho in host_search"
:key="ho.id"
class="flex items-center justify-between text-sm"
>
<span @click="add(ho.id)" class="cursor-pointer hover:underline">
{{ ho.name }}
</span>
<button
type="button"
@click="add(ho.id)"
class="text-blue-500 hover:text-blue-700 font-semibold"
>
+
</button>
</div>
</div>
</div>
<htfield_fa
table="conf_sessions"
qid="desc"
question="Description"
myclass="pell"
:answer="this_activity.desc"
:targetid="this_activity.id"
/>
<selectmenu_fa
table="conf_sessions"
qid="type"
:answer="this_activity.type"
menu="sessiontypes_menu"
:targetid="this_activity.id"
question="Type"
labelfield="type"
/>
<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"
/>
<field
myclass="double"
table="conf_sessions"
qid="recording"
:answer="this_activity.recording"
:targetid="this_activity.id"
question="Recording Link"
placeholder=""
/>
</div>
</div>
<div v-else class="flex flex-col md:flex-row gap-4 bg-white rounded-md shadow p-4 mb-4">
<!-- Left: Edit Button -->
<div class="md:w-40 shrink-0">
<a href="#" @click.prevent="editing = 1" class="text-sm text-blue-600 hover:underline font-medium">
Edit
</a>
</div>
<!-- Right: Session Info -->
<div class="flex-grow text-sm text-gray-800">
<p class="text-base font-semibold text-gray-900 mb-1">{{ this_activity.title }}</p>
<p class="text-gray-600 mb-1">
{{ $root.$dj(this_activity.starttime).format('h:mma') }} /
<span class="capitalize">{{ this_activity.mode }}</span>
</p>
<p class="text-gray-600 mb-1">
Presented by:
<span v-for="(h, i) in this_hosts" :key="h.hostid">
{{ h.name }}<span v-if="i < this_hosts.length - 1">, </span>
</span>
</p>
<div class="mt-2 text-gray-700" v-html="this_activity.desc"></div>
</div>
</div>
</div>` })
//
@ -1584,120 +1303,468 @@ const ChartView = Vue.component('chartview', {
const basic_getP = (url) =>
new Promise((resolve, reject) =>
basic_get(url, (r) => resolve(r), (e) => reject(e))
);
//
//
//
//
//
//
// Activity EDITOR List
// ------------------------------
//
//
//
const ActivityEditorList = Vue.component('activityeditorlist', {
props: [ ],
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: {'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','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',
'Aug 24 2023':" - Optional Day", 'Aug 25 2023':' - Mandatory Day'},
hosts_by_sesid: {}, } },
mounted: function() {
this.$root.fetch_menus();
this.fetch_myevents()
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: {
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 } },
fetch_myevents: function() {
var self = this;
basic_get('dir_api.php?a=get/mysessions',
function(r2) {
self.mysessions = _.sortBy(r2,function(x) { return x.starttime || 0} );
self.my_ses_ids = _.pluck(r2, 'id')
self.$forceUpdate();
self.active += 1;
} )
basic_get('dir_api.php?a=get/sessions',
function(r2) {
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 + ' ' + x.desc
x.searchable = x.searchable.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 } )
} )
basic_get('dir_api.php?a=get/allhosts',
function(r2) {
self.hosts = r2
setTimeout(function () {
self.my_host_ids = r2[ self.$root.user.conf_id ]
if (! self.my_host_ids) { self.my_host_ids = [] }
}, 750);
self.active += 1;
} )
},
filtered: function(ff) { if (this.active<1) { return [] }
var self = this
var ay = this.$root.settings['default_ay']
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) {
var start = dayjs(this.$root.ay_menu[ay].begin ) /* UPDATE this.filters.sp23[0]*/
var end = dayjs(this.$root.ay_menu[ay].end)
ff = ff.filter( function(item,index) {
this_time = dayjs(item.starttime)
return this_time.isBefore(end) && start.isBefore(this_time) } )
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();
}
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 }, },
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: {
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: { },
activitiesFiltered() {
return this.filtered(this.activities);
},
groupedByDay() {
return _.groupBy(this.activitiesFiltered, (x) => this.month_year(x.starttime));
},
},
template: `<div class="activitylist">
<div v-for="items,mmyy in activities_g_filtered">
<div class="pure-g">
<div class="day_title pure-u-1-1">{{mmyy}} {{get_day_title(mmyy)}}</div>
</div>
<div v-for="a in items" class="activitylist">
<activityinlineedit :which="a.id"></activityinlineedit>
</div>
</div>
</div>` })
<!-- 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>`
})
//
//
//
//

View File

@ -72,5 +72,6 @@ $MOD_DATE = file_exists(__FILE__) ? date("F d Y H:i.", filemtime(__FILE__)) : "(
<br /> &nbsp;
<script src="js/intranet_libs_bottom.js"/></script>
<script src="js/dir_app.js"></script>
<script src="<?= $XTRAJS ?>"></script>
</body>
</html>

View File

@ -9,7 +9,7 @@ class peter_db {
// Constructor to initialize database connection variables
public function __construct() {
$this->DBServer = '192.168.1.6'; // Your DB server (example: localhost)
$this->DBServer = '192.168.1.198'; // Your DB server (example: localhost)
$this->DBUser = 'phowell'; // Your DB username
$this->DBPass = 'rolley34'; // Your DB password
$this->DBName = 'db'; // Your DB name

View File

@ -1,5 +1,7 @@
.clickme, .clicky {cursor: pointer;}
.sessiongrid td, .sessiongrid th { vertical-align: top; text-align: left; }
.pell{
border:1px solid hsla(0,0%,4%,.1)
}