TableAppointments.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <template>
  2. <div v-if="hasData">
  3. <h1>Appointments {{ title }}</h1>
  4. <div v-if="!detail">
  5. <div aria-label="Table Widgets, Section 1" class="mfsw-row">
  6. <div class="column left">
  7. <label
  8. >Show
  9. <select aria-controls="queueRange" v-model="limit">
  10. <option value="5">5</option>
  11. <option value="10">10</option>
  12. <option value="25">25</option>
  13. <option value="50">50</option>
  14. <option value="100">100</option>
  15. </select>
  16. entries
  17. </label>
  18. </div>
  19. <div class="column right">
  20. <label
  21. >Filter Results&#58;
  22. <input type="search" aria-controls="queueRange" v-model="search" />
  23. </label>
  24. <span>&nbsp;</span>
  25. <button
  26. type="button"
  27. class="btn"
  28. :class="{ 'btn-success': filterAnd, 'btn-default': !filterAnd }"
  29. v-on:click="toggleFilterAnd()"
  30. >
  31. {{ filterAnd ? "Requiring All" : "Matching Any" }}
  32. </button>
  33. <div v-if="allowRefresh">
  34. <span>&nbsp;</span>
  35. <button class="btn btn-primary" v-on:click="getAppointments">
  36. <span class="glyphicon glyphicon-refresh"></span>
  37. </button>
  38. </div>
  39. </div>
  40. </div>
  41. <table aria-label="Table" class="table">
  42. <thead>
  43. <tr>
  44. <th>Time</th>
  45. <th>Student</th>
  46. <th>Here to see</th>
  47. <th>On Team</th>
  48. <th>Meeting with</th>
  49. <th>Subject</th>
  50. <th>Status</th>
  51. </tr>
  52. </thead>
  53. <tbody v-for="(entry, index) in output" v-bind:key="index">
  54. <tr>
  55. <td>
  56. <button
  57. type="button"
  58. class="btn btn-default"
  59. v-on:click="detail = entry"
  60. >
  61. <span class="glyphicon glyphicon-search"></span>
  62. </button>
  63. {{ entry.time }}
  64. </td>
  65. <td v-if="entry.contactId">
  66. <router-link
  67. :to="{ name: 'Record', params: { id: entry.contactId } }"
  68. >{{ entry.name }}</router-link
  69. >
  70. </td>
  71. <td v-else>{{ entry.name }}</td>
  72. <td>{{ entry.ownerName }}</td>
  73. <td>{{ entry.teamName }}</td>
  74. <td>{{ entry.interactionOwnerName }}</td>
  75. <td>
  76. <router-link
  77. v-if="entry.interactionId"
  78. class="btn btn-primary"
  79. :to="{
  80. name: 'Appointment',
  81. params: { id: entry.interactionId }
  82. }"
  83. >
  84. <span class="glyphicon glyphicon-link"></span>
  85. </router-link>
  86. {{ entry.subject }}
  87. </td>
  88. <td v-if="hasPermission(entry.teamId)">
  89. <span v-show="false">{{ entry.condition }},{{ entry.id }}</span>
  90. <DropDownWithActions
  91. v-bind:condition="entry.condition"
  92. v-bind:id="entry.id"
  93. v-bind:endpoint="api.updateCondition"
  94. v-bind:actions="[
  95. {
  96. id: '1',
  97. value: 'reset',
  98. name: 'Reset',
  99. result: 'Scheduled'
  100. },
  101. {
  102. id: '2',
  103. value: 'checkin',
  104. name: 'Check-in',
  105. result: 'Checked In'
  106. },
  107. { id: '3', value: 'begin', name: 'Begin', result: 'Active' },
  108. {
  109. id: '4',
  110. value: 'finish',
  111. name: 'Finish',
  112. result: 'Finished'
  113. },
  114. {
  115. id: '5',
  116. value: 'cancel',
  117. name: 'Cancel',
  118. result: 'Canceled'
  119. },
  120. {
  121. id: '6',
  122. value: 'noshow',
  123. name: 'Noshow',
  124. result: 'No-show'
  125. }
  126. ]"
  127. @update="val => (entry.condition = val)"
  128. />
  129. </td>
  130. <td v-else>{{ entry.condition }}</td>
  131. </tr>
  132. </tbody>
  133. </table>
  134. <div aria-label="Table Widgets, Section 2" class="mfsw-row">
  135. <div class="column left">
  136. Showing {{ entryBeg }} to {{ entryEnd }} of {{ entryNum }} entries
  137. </div>
  138. <div class="column right">
  139. <button type="button" class="btn btn-primary" v-on:click="prevPage()">
  140. Prev</button
  141. >&nbsp;
  142. <button type="button" class="btn btn-primary" v-on:click="nextPage()">
  143. Next
  144. </button>
  145. <span>&nbsp;Page {{ currentFiltered }}/{{ sizeFiltered }}&nbsp;</span>
  146. </div>
  147. </div>
  148. </div>
  149. <br />
  150. <br />
  151. <br />
  152. </div>
  153. <div v-else>
  154. <h1>No data for {{ title }}</h1>
  155. <div class="alert alert-info">
  156. <!-- <p>{{loading ? "Loading..." :"No data to display."}}</p> -->
  157. <p>{{ loadingPrompt }}</p>
  158. </div>
  159. <button
  160. v-if="allowRefresh"
  161. class="btn btn-warning btn-block"
  162. v-on:click="getAppointments"
  163. >
  164. <span class="glyphicon glyphicon-refresh"></span>
  165. <span>&nbsp;Refresh</span>
  166. </button>
  167. </div>
  168. </template>
  169. <script>
  170. import DropDownWithActions from "@/components/DropDownWithActions.vue";
  171. export default {
  172. name: "TableAppointments",
  173. props: [
  174. "allowRefresh",
  175. "api",
  176. "appts",
  177. "currentUserId",
  178. "devmode",
  179. "filters",
  180. "loading",
  181. "searchState",
  182. "teamsForUser",
  183. "title",
  184. "users"
  185. ],
  186. components: {
  187. DropDownWithActions
  188. },
  189. computed: {
  190. subSearch() {
  191. return this.search.split(" ", this.subStringMax).filter(foo => foo);
  192. },
  193. subFilter() {
  194. return this.filters.split(" ", this.subStringMax).filter(foo => foo);
  195. },
  196. currentId: function() {
  197. return this.detail.id;
  198. },
  199. hasDataLogic: function() {
  200. return x => y => z => x && x.length > 0 && y && z;
  201. },
  202. hasData: function() {
  203. return this.hasDataLogic(this.appts)(this.api)(this.currentUserId);
  204. },
  205. numFiltered: function() {
  206. return this.getRange.length;
  207. },
  208. sizeFiltered: function() {
  209. return (
  210. Math.floor(this.numFiltered / this.limit) +
  211. (this.numFiltered % this.limit > 0 ? 1 : 0)
  212. );
  213. },
  214. currentFiltered: function() {
  215. return this.offset + 1;
  216. },
  217. output: function() {
  218. return this.getRange.slice(this.entryOff, this.entryEnd);
  219. },
  220. getRangeSubFilter: function() {
  221. return x => y =>
  222. !x ||
  223. x.length < 1 ||
  224. x.some(
  225. elem => y.condition.toLowerCase().indexOf(elem.toLowerCase()) > -1
  226. );
  227. },
  228. getRangeSubSearchAnd: function() {
  229. return x => y =>
  230. !x ||
  231. x.length < 1 ||
  232. x.every(elem =>
  233. Object.values(y).some(
  234. val => val.toLowerCase().indexOf(elem.toLowerCase()) > -1
  235. )
  236. );
  237. },
  238. getRangeSubSearchIor: function() {
  239. return x => y =>
  240. !x ||
  241. x.length < 1 ||
  242. x.some(elem =>
  243. Object.values(y).some(
  244. val => val.toLowerCase().indexOf(elem.toLowerCase()) > -1
  245. )
  246. );
  247. },
  248. getRange: function() {
  249. return this.appts && this.appts[0]
  250. ? this.filterAnd
  251. ? this.appts
  252. .filter(item => this.getRangeSubFilter(this.subFilter)(item))
  253. .filter(item => this.getRangeSubSearchAnd(this.subSearch)(item))
  254. : this.appts
  255. .filter(item => this.getRangeSubFilter(this.subFilter)(item))
  256. .filter(item => this.getRangeSubSearchIor(this.subSearch)(item))
  257. : this.appts;
  258. },
  259. entryNum() {
  260. return this.getRange.length;
  261. },
  262. entryPos() {
  263. return this.offset < 0
  264. ? 0
  265. : this.offset > this.entryNum / this.limit
  266. ? Math.floor(this.entryNum / this.limit)
  267. : this.offset;
  268. },
  269. entryOff() {
  270. return this.entryPos * this.limit;
  271. },
  272. entryBeg() {
  273. return this.entryOff + 1;
  274. },
  275. entryMod() {
  276. return (this.entryNum - this.entryOff) % this.limit;
  277. },
  278. entryEnd() {
  279. return (
  280. (this.entryNum - this.entryOff < this.limit
  281. ? this.entryMod
  282. : this.limit) *
  283. 1 +
  284. this.entryOff
  285. );
  286. }
  287. },
  288. data() {
  289. return {
  290. detail: "",
  291. filterAnd: true,
  292. loadingPrompt: "",
  293. limit: 25,
  294. mode: "table",
  295. offset: 0,
  296. search: ""
  297. };
  298. },
  299. methods: {
  300. toggleFilterAnd() {
  301. this.filterAnd = !this.filterAnd;
  302. },
  303. hasPermission(teamId) {
  304. return this.teamsForUser.map(id => id * 1).includes(teamId * 1);
  305. },
  306. getAppointments() {
  307. this.$emit("refresh");
  308. },
  309. prevPage: function() {
  310. if (this.offset > 0) {
  311. --this.offset;
  312. }
  313. },
  314. nextPage: function() {
  315. if (this.currentFiltered < this.sizeFiltered) {
  316. ++this.offset;
  317. }
  318. },
  319. updateLoadingPrompt: function() {
  320. this.loadingPrompt = "Loading...";
  321. if (!this.loading) {
  322. setTimeout(() => {
  323. this.loadingPrompt = "No data to display.";
  324. }, 2000);
  325. }
  326. }
  327. },
  328. mounted() {
  329. this.updateLoadingPrompt();
  330. this.search = this.searchState;
  331. },
  332. watch: {
  333. search() {
  334. this.offset = 0;
  335. this.$emit("updateSearchState", this.search);
  336. },
  337. detail() {
  338. this.$emit("updateDetail", this.detail);
  339. },
  340. loading() {
  341. this.updateLoadingPrompt();
  342. }
  343. }
  344. };
  345. </script>
  346. <style scoped>
  347. /*NOTE: Copied from Quickview.vue*/
  348. .mfsw-row .column {
  349. float: left;
  350. padding: 0 10px;
  351. min-width: 120px;
  352. }
  353. .mfsw-row .left {
  354. width: 30%;
  355. }
  356. .mfsw-row .right {
  357. width: 70%;
  358. text-align: right;
  359. }
  360. .mfsw-row:after {
  361. content: "";
  362. display: block;
  363. clear: both;
  364. }
  365. </style>