Compare commits

...

2 Commits

Author SHA1 Message Date
Ferdinand Thiessen 0881069b11 feat(ui): Implemented job invitations / transfer page
continuous-integration/woodpecker the build was successful Details
2021-12-23 03:25:41 +01:00
Ferdinand Thiessen 30173ec87d feat(ui): models: Provide Job class 2021-12-23 03:25:18 +01:00
3 changed files with 297 additions and 23 deletions

View File

@ -111,7 +111,7 @@ import { notEmpty } from '@flaschengeist/api';
import { IsoDateInput } from '@flaschengeist/api/components';
import { useEventStore } from '../../store';
import { emptyEvent, emptyJob, EditableEvent } from '../../store/models';
import { emptyEvent, Job, EditableEvent } from '../../store/models';
import { date, ModifyDateOptions } from 'quasar';
import { computed, defineComponent, PropType, ref, onBeforeMount, watch } from 'vue';
@ -164,11 +164,11 @@ export default defineComponent({
);
function addJob() {
if (!activeJob.value) event.value.jobs.push(emptyJob());
if (!activeJob.value) event.value.jobs.push(new Job());
else
void activeJob.value.validate().then((success) => {
if (success) {
event.value.jobs.push(emptyJob());
event.value.jobs.push(new Job());
active.value = event.value.jobs.length - 1;
}
});

View File

@ -1,8 +1,246 @@
<template>
<q-page padding>
<q-card>
<q-card-section class="row"> </q-card-section>
<q-card-section> </q-card-section>
<q-card-section>
<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>
</q-table>
</q-card-section>
</q-card>
</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';
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 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) => ({
job: row.job_id,
sender: row.inviter_id === mainStore.currentUser.userid,
}),
},
];
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>

View File

@ -4,32 +4,68 @@ import { date } from 'quasar';
export type EditableEvent = Omit<Omit<Omit<FG.Event, 'jobs'>, 'type'>, 'id'> & {
type?: FG.EventType | number;
id?: number;
jobs: EditableJob[];
jobs: Job[];
};
/** A new job does not have an id or type assigned */
export type EditableJob = Omit<Omit<FG.Job, 'type'>, 'id'> & {
type?: FG.EventType | number;
id?: number;
};
export class Job implements FG.Job {
id = NaN;
start: Date;
end?: Date = undefined;
type: FG.JobType | number = NaN;
comment?: string = undefined;
locked = false;
services = [] as FG.Service[];
required_services = 0;
export function emptyJob(startDate = new Date()): EditableJob {
const start = date.adjustDate(startDate, {
hours: new Date().getHours(),
});
return {
start: start,
end: date.addToDate(start, { hours: 1 }),
services: [],
locked: false,
required_services: 2,
};
/**
* Build Job from API Job interface
* @param iJob Object following the API Job interface
*/
constructor(iJob?: Partial<FG.Job>) {
if (!iJob || iJob.start === undefined)
this.start = date.buildDate({
hours: new Date().getHours(),
minutes: 0,
seconds: 0,
milliseconds: 0,
});
else this.start = new Date(); // <-- make TS happy "no initalizer"
if (iJob !== undefined) Object.assign(this, iJob);
}
/**
* Create Job from start Date
* @param start when does the event start?
* @param adjustTime Set hours to current value, zero minutes and seconds
* @param duration How long should the job go? Value in hours or undefined
* @returns new Job event
*/
static fromDate(start: Date, adjustTime = true, duration?: number) {
if (adjustTime)
start = date.adjustDate(start, {
hours: new Date().getHours(),
minutes: 0,
seconds: 0,
milliseconds: 0,
});
return new Job({
start: start,
end: duration === undefined ? undefined : date.addToDate(start, { hours: duration }),
});
}
/**
* Check if this instance was loaded from API
*/
isPersistent() {
return !isNaN(this.id);
}
}
export function emptyEvent(startDate: Date = new Date()): EditableEvent {
return {
start: date.adjustDate(startDate, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }),
jobs: [emptyJob(startDate)],
jobs: [Job.fromDate(startDate, true, 4)],
is_template: false,
};
}