import "./DesktopPeopleToolbar.js" import "./DesktopPeopleTable.js" import "./DesktopPeopleDetail.js" import server from "/people/@server/index.js" css(` people- { font-family: 'Arial'; scrollbar-width: none; -ms-overflow-style: none; } people- select { appearance: none; -webkit-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.65em center; padding-right: 1.8em !important; } people- textarea { font-family: 'Arial'; } people- input::placeholder { color: var(--headertext); opacity: 0.35; } `) class People extends Shadow { _people = (global.currentNetwork.data.members || []).map(p => ({ ...p, _online: this.isOnline(p) })) #lastUpdated; selectedMemberId = null searchText = "" filterRole = "" filterTier = "" sortKey = "joined" sortDir = "asc" tableEl = null get people() { return this._people } set people(val) { this._people = val this.#lastUpdated = Date.now() global.currentNetwork.data.members = val } constructor() { super() } isOnline(member) { return global.socket.connectedUsers.includes(member.email) } get filteredSortedPeople() { let list = this.people; if (this.searchText) { const q = this.searchText.toLowerCase(); list = list.filter(p => [p.first_name, p.last_name, p.email, p.title, p.county, p.city, p.state] .some(v => v?.toLowerCase().includes(q)) ); } if (this.filterRole) { list = list.filter(p => (p.roles || []).includes(this.filterRole)); } if (this.filterTier) { list = list.filter(p => String(p.plan_name) === this.filterTier); } // Sort const key = this.sortKey; const dir = this.sortDir === "asc" ? 1 : -1; list = [...list].sort((a, b) => { let av, bv; if (key === "name") { av = `${a.first_name} ${a.last_name}`; bv = `${b.first_name} ${b.last_name}`; } else if (key === "email") { av = a.email || ""; bv = b.email || ""; } else if (key === "title") { av = a.title || ""; bv = b.title || ""; } else if (key === "county") { av = a.county || ""; bv = b.county || ""; } else if (key === "roles") { av = (a.roles || [])[0] || ""; bv = (b.roles || [])[0] || ""; } else if (key === "tier") { av = a.plan_name || 0; bv = b.plan_name || 0; } else if (key === "joined") { av = new Date(a.joined_network || a.created || 0).getTime(); bv = new Date(b.joined_network || b.created || 0).getTime(); } if (typeof av === "string") return av.localeCompare(bv) * dir; return (av - bv) * dir; }); return list; } get selectedMember() { return this.people.find(p => p.id === this.selectedMemberId) || null; } handleSort(key) { if (this.sortKey === key) { this.sortDir = this.sortDir === "asc" ? "desc" : "asc"; } else { this.sortKey = key; this.sortDir = "asc"; } this.rerender(); } render() { const filtered = this.filteredSortedPeople; const detailOpen = !!this.selectedMember; VStack(() => { DesktopPeopleToolbar( filtered, this.searchText, this.filterRole, this.filterTier, (text) => { this.searchText = text this.tableEl.people = this.filteredSortedPeople this.tableEl.rerender() }, (role) => { this.filterRole = role; this.rerender(); }, (tier) => { this.filterTier = tier; this.rerender(); } ) HStack(() => { // Table — shrinks when drawer open VStack(() => { this.tableEl = DesktopPeopleTable( filtered, this.selectedMemberId, this.sortKey, this.sortDir, (id) => { this.selectedMemberId = this.selectedMemberId === id ? null : id; this.rerender(); }, (key) => this.handleSort(key) ) }) .flex(1).height(100, pct).overflow("hidden") // Slide-out detail drawer if (detailOpen) { VStack(() => { // Drawer header with close button HStack(() => { p(`${this.selectedMember.first_name} ${this.selectedMember.last_name}`) .margin(0).fontSize(0.88, em).fontWeight("600") .color("var(--headertext)").flex(1).minWidth(0) .overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis") button("✕") .border("none").background("transparent") .color("var(--headertext)").opacity(0.4) .fontSize(0.82, em).cursor("pointer").padding(0.25, em) .borderRadius(0.3, em) .onClick((done) => {if(!done) return; this.selectedMemberId = null; this.rerender(); }) }) .paddingHorizontal(1.25, em).paddingVertical(0.78, em) .borderBottom("1px solid var(--divider)") .alignItems("center").flexShrink(0) VStack(() => { DesktopPeopleDetail(this.selectedMember, (updatedPerson) => this.updatePeople(updatedPerson) ) }) .flex(1).overflow("hidden") }) .width(360, px) .height(100, pct) .borderLeft("1px solid var(--divider)") .flexShrink(0) .overflow("hidden") } }) .flex(1).minHeight(0).width(100, pct).overflow("hidden") }) .height(100, pct).width(100, pct).overflow("hidden") .onAppear(async () => { const res = await server.getPeople(global.currentNetwork.id); if (!res.error && res.length > 0) { if((this.people.length !== res.length) || !this.#lastUpdated) { this.people = res.map(p => ({ ...p, _online: this.isOnline(p) })); this.rerender(); } } }) } updatePeople(person) { this.people = this.people.map(p => p.id === person.id ? person : p) this.rerender() } } register(People)