<template> <q-page padding> <q-table v-model:pagination="pagination" title="Dienstanfragen" :rows="rows" :columns="columns" row-key="id" :loading="loading" binary-state-sort @request="onRequest" > <template #top-right> <q-toggle v-model="showSent" dense label="Gesendete anzeigen" /> </template> <template #body-cell-inviter="props"> <q-td :props="props"> <div> {{ props.value.with }}<q-icon v-if="props.value.sender" name="mdi-account-alert"> <q-tooltip>Gesendet von {{ props.value.sender }}</q-tooltip> </q-icon> </div> </q-td> </template> <template #body-cell-type="props"> <q-td :props="props"> <q-icon size="sm" :name="types[props.value].icon"> <q-tooltip>{{ types[props.value].tooltip }}</q-tooltip> </q-icon> </q-td> </template> <template #body-cell-actions="props"> <q-td :props="props"> <!-- <q-btn v-for="action in props.value" :key="action.icon" :icon="action.icon" dense /> --> <div class="row justify-end"> <div v-for="action in props.value" :key="action.icon"> <q-btn class="q-mx-xs" :icon="action.icon" dense @click="action.onClick" round :color="action.color" > <q-tooltip>{{ action.tooltip }}</q-tooltip> </q-btn> </div> </div> </q-td> </template> </q-table> </q-page> </template> <script lang="ts"> import { formatStartEnd, useMainStore, useUserStore } from '@flaschengeist/api'; import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue'; import { QTableProps } from 'quasar'; import { Job } from '../store/models'; import { useEventStore } from '../store'; import { EventNotification, InvitationData, InvitationResponseData } from '../events'; export default defineComponent({ name: 'PageEventRequests', setup() { const store = useEventStore(); const userStore = useUserStore(); const mainStore = useMainStore(); interface RowData extends FG.Invitation { inviter: FG.User; invitee: FG.User; transferee?: FG.User; job: Job; } // Generated data used for the table const rows = ref([] as RowData[]); // Loading state for data generation (getting Job information) const loading = ref(false); // Which "page" of invitations to show (we load all, but do not fetch all jobs) const pagination = ref({ sortBy: 'desc', descending: false, page: 1, rowsPerPage: 3, rowsNumber: 4, }); // Real invitations const invitations = computed(() => showSent.value ? store.invitations : store.invitations.filter((i) => i.inviter_id !== mainStore.currentUser.userid) ); const all_notifications = computed<EventNotification[]>(() => { return mainStore.notifications.filter((n) => n.plugin === 'events') as EventNotification[]; }); const showSent = ref(false); async function fillRows(data: FG.Invitation[]) { const res = [] as RowData[]; for (let i = 0; i < data.length; ++i) { res.push( Object.assign({}, data[i], { inviter: <FG.User>await userStore.getUser(data[i].inviter_id), invitee: <FG.User>await userStore.getUser(data[i].invitee_id), transferee: data[i].transferee_id ? await userStore.getUser(<string>data[i].transferee_id) : undefined, job: new Job(await store.getJob(data[i].job_id)), }) ); } rows.value = res; } type onRequestType = QTableProps['onRequest']; const onRequest: onRequestType = (requestProp) => { const { page, rowsPerPage, sortBy, descending } = requestProp.pagination; loading.value = true; // Number of total invitations pagination.value.rowsNumber = invitations.value.length; // calculate starting row of data const startRow = (page - 1) * rowsPerPage; // get all rows if "All" (0) is selected const fetchCount = rowsPerPage === 0 ? pagination.value.rowsNumber : Math.min(pagination.value.rowsNumber - startRow, rowsPerPage); // copy array, as sort is in-place function sorting<T = any>(key: string | keyof T, descending = true) { return (a: T, b: T) => { const v1 = a[key as keyof T]; if (v1 === undefined) return descending ? -1 : 1; const v2 = b[key as keyof T]; if (v2 === undefined) return descending ? 1 : -1; return (v1 < v2 ? -1 : 1) * (descending ? -1 : 1); }; } // Set table data fillRows( [...invitations.value] .sort(sorting(sortBy, descending)) .slice(startRow, startRow + fetchCount) ) .then(() => { pagination.value.page = page; pagination.value.rowsPerPage = rowsPerPage; pagination.value.sortBy = sortBy; pagination.value.descending = descending; }) .finally(() => (loading.value = false)); }; onBeforeMount(() => { void Promise.allSettled([ userStore.getUsers(), store.getInvitations(), store.getJobTypes(), ]).then(() => onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] }) ); }); watch(showSent, () => { onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] }); }); function getType(row: RowData) { var idx = row.transferee === undefined ? 0 : 1; if (row.inviter.userid === mainStore.currentUser.userid) idx += 2; return idx; } const dimmed = (row: RowData) => (getType(row) >= types.length / 2 ? 'dimmed' : undefined); const columns = [ { label: 'Type', name: 'type', align: 'left', field: getType, sortable: true, classes: dimmed, }, { label: 'Dienstart', align: 'left', name: 'job_type', sortable: true, classes: dimmed, field: (row: RowData) => store.jobTypes.find((t) => t.id == row.job.type)?.name || 'Unbekannt', }, { label: 'Wann', align: 'center', sortable: true, name: 'job_start', classes: dimmed, field: (row: RowData) => formatStartEnd(row.job.start, row.job.end) + ' Uhr', }, { label: 'Von / Mit', name: 'inviter', align: 'center', classes: dimmed, field: (row: RowData) => { const sender = row.transferee_id && row.transferee_id !== row.inviter_id ? row.inviter.display_name : undefined; if (row.invitee_id === mainStore.currentUser.userid) { return { with: row.transferee ? row.transferee.display_name : row.inviter.display_name, sender, }; } if (row.transferee_id === mainStore.currentUser.userid) { return { with: row.invitee.display_name, sender, }; } return { with: !row.transferee ? row.invitee.display_name : `${row.transferee.display_name} <-> ${row.invitee.display_name}`, }; }, }, { label: 'Aktionen', align: 'right', name: 'actions', classes: dimmed, field: (row: RowData) => { const sender = row.inviter_id === mainStore.currentUser.userid; let actions = []; const reject = { icon: 'mdi-delete', tooltip: 'Einladung löschen', color: 'negative', onClick: () => { void store.rejectInvitation(row.id); onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [], }); const notification = all_notifications.value.find( (n) => (<InvitationData>n.data).invitation === row.id ); if (notification !== undefined) { void mainStore.removeNotification(notification.id); } }, }; const accept = { icon: 'mdi-check', tooltip: 'Einladung annehmen', color: 'primary', onClick: () => { void store.acceptInvitation(row.id); onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [], }); const notification = all_notifications.value.find( (n) => (<InvitationData>n.data).invitation === row.id ); if (notification !== undefined) { void mainStore.removeNotification(notification.id); } }, }; if (sender) { actions.push(reject); } else if (row.invitee_id === mainStore.currentUser.userid) { actions.push(accept); actions.push({ ...reject, icon: 'mdi-close' }); } return actions; }, }, ]; const types = [ { icon: 'mdi-calendar', tooltip: 'Einladung' }, { icon: 'mdi-calendar-sync', tooltip: 'Tauschanfrage' }, { icon: 'mdi-calendar-outline', tooltip: 'Einladung (von dir)' }, { icon: 'mdi-calendar-sync-outline', tooltip: 'Tauschanfrage (von dir)' }, ]; return { columns, loading, onRequest, pagination, rows, showSent, types, }; }, }); </script>