Compare commits
	
		
			2 Commits
		
	
	
		
			main
			...
			0881069b11
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						0881069b11 | |
| 
							
							
								
								 | 
						30173ec87d | 
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue