Compare commits

..

No commits in common. "c767d924426bd12fda8d7a1cbcf36edc346d306f" and "b7741cfa376e0ea21c2e58a769adde79dc50fced" have entirely different histories.

13 changed files with 141 additions and 599 deletions

6
.gitignore vendored
View File

@ -6,8 +6,4 @@ yarn.lock
# Backend
*.egg-info
__pycache__
# IDE
.idea
*.swp
__pycache__

View File

@ -14,9 +14,10 @@ __version__ = pkg_resources.get_distribution("flaschengeist_events").version
class EventPlugin(Plugin):
# id = "dev.flaschengeist.events"
#id = "dev.flaschengeist.events"
#plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.id])
# provided resources
# permissions = permissions.permissions
#permissions = permissions.permissions
models = models
# def __init__(self, cfg):
@ -26,12 +27,7 @@ class EventPlugin(Plugin):
def load(self):
from .routes import blueprint
self.blueprint = blueprint
def install(self):
self.install_permissions(permissions.permissions)
@staticmethod
def getPlugin() -> LocalProxy["EventPlugin"]:
return LocalProxy(lambda: current_app.config["FG_PLUGINS"]["events"])

View File

@ -1,8 +1,7 @@
from datetime import datetime, timedelta, timezone
from enum import IntEnum
from typing import Optional, Tuple, Union
from flaschengeist.controller import userController
from flaschengeist.models import Notification, UtcDateTime
from typing import Optional, Tuple
from flaschengeist.models import UtcDateTime
from flaschengeist.models.user import User
from werkzeug.exceptions import BadRequest, Conflict, NotFound
@ -28,11 +27,8 @@ class NotifyType(IntEnum):
INVITE = 0x01
TRANSFER = 0x02
# Invitation responsed 0x10..0x1F
INVITATION_ACCEPTED = 0x11
INVITATION_REJECTED = 0x12
# Information responses 0x20..0x2F
INFO_ACCEPTED = 0x21
INFO_REJECTED = 0x22
INVITATION_ACCEPTED = 0x10
INVITATION_REJECTED = 0x11
@before_delete_user
@ -299,7 +295,7 @@ def delete_job(job: Job):
db.session.commit()
def assign_job(job: Job, user, value, is_backup=False, notify=False):
def assign_job(job: Job, user, value, is_backup=False):
assert value > 0
service = Service.query.get((job.id, user.id_))
if service:
@ -307,17 +303,10 @@ def assign_job(job: Job, user, value, is_backup=False, notify=False):
service.is_backup = is_backup
else:
job.services.append(Service(user_=user, value=value, is_backup=is_backup, job_=job))
if notify:
EventPlugin.getPlugin().notify(
user,
f"You were assigned to a job\n{job.start.strftime('%d.%m.%Y')}",
{"type": NotifyType.INFO_ACCEPTED, "event_id": job.event_id_},
)
db.session.commit()
def unassign_job(job: Job = None, user=None, service=None, notify=False):
_date = job.start.strftime("%d.%m.%Y")
if service is None:
assert job is not None and user is not None
service = Service.query.get((job.id, user.id_))
@ -331,24 +320,17 @@ def unassign_job(job: Job = None, user=None, service=None, notify=False):
db.session.delete(service)
db.session.commit()
if notify:
EventPlugin.getPlugin().notify(
user, f"Your assignmet was cancelled\n{_date}", {"type": NotifyType.INFO_REJECTED, "event_id": event_id}
)
EventPlugin.plugin.notify(user, "Your assignmet was cancelled", {"event_id": event_id})
def invite(job: Job, invitee, inviter, transferee=None):
inv = Invitation(job_=job, inviter_=inviter, invitee_=invitee, transferee_=transferee)
db.session.add(inv)
update()
_date = job.start.strftime("%d.%m.%Y")
if transferee is None:
EventPlugin.getPlugin().notify(
invitee, _(f"Job invitation\n{_date}"), {"type": NotifyType.INVITE, "invitation": inv.id}
)
EventPlugin.plugin.notify(invitee, _("Job invitation"), {"type": NotifyType.INVITE, "invitation": inv.id})
else:
EventPlugin.getPlugin().notify(
invitee, _(f"Job transfer\n{_date}"), {"type": NotifyType.TRANSFER, "invitation": inv.id}
)
EventPlugin.plugin.notify(invitee, _("Job transfer"), {"type": NotifyType.TRANSFER, "invitation": inv.id})
return inv
@ -365,21 +347,9 @@ def get_invitations(user: User):
).all()
def cleanup_notifications(inv: Invitation):
notifications = tuple(
filter(
lambda notification: notification.data.get("invitation") == inv.id, EventPlugin.getPlugin().notifications
)
)
for notification in notifications:
db.session.delete(notification)
db.session.commit()
def cancel_invitation(inv: Invitation):
db.session.delete(inv)
db.session.commit()
cleanup_notifications(inv)
def respond_invitation(invite: Invitation, accepted=True):
@ -392,7 +362,7 @@ def respond_invitation(invite: Invitation, accepted=True):
raise Conflict
if not accepted:
EventPlugin.getPlugin().notify(
EventPlugin.plugin.notify(
inviter,
_("Invitation rejected"),
{
@ -406,12 +376,12 @@ def respond_invitation(invite: Invitation, accepted=True):
if invite.transferee_id is None:
assign_job(job, invite.invitee_, 1)
else:
service = tuple(filter(lambda s: s.userid == invite.transferee_id, job.services))
service = filter(lambda s: s.userid == invite.transferee_id, job.services)
if not service:
raise Conflict
unassign_job(job, invite.transferee_, service[0], True)
assign_job(job, invite.invitee_, service[0].value)
EventPlugin.getPlugin().notify(
EventPlugin.plugin.notify(
inviter,
_("Invitation accepted"),
{
@ -432,7 +402,7 @@ def assign_backups():
services = Service.query.filter(Service.is_backup == True).join(Job).filter(Job.start <= start).all()
for service in services:
if service.job_.start <= now or service.job_.is_full():
EventPlugin.getPlugin().notify(
EventPlugin.plugin.notify(
service.user_,
"Your backup assignment was cancelled.",
{"event_id": service.job_.event_id_},
@ -442,7 +412,7 @@ def assign_backups():
else:
service.is_backup = False
logger.debug(f"Service not full, assigning backup. {service.serialize()}")
EventPlugin.getPlugin().notify(
EventPlugin.plugin.notify(
service.user_,
"Your backup assignment was accepted.",
{"event_id": service.job_.event_id_},

View File

@ -11,95 +11,82 @@ import flaschengeist
# revision identifiers, used by Alembic.
revision = "e70508bd8cb4"
revision = 'e70508bd8cb4'
down_revision = None
branch_labels = ("events",)
branch_labels = ('events',)
depends_on = "flaschengeist"
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"events_event_type",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_events_event_type")),
sa.UniqueConstraint("name", name=op.f("uq_events_event_type_name")),
op.create_table('events_event_type',
sa.Column('id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('name', sa.String(length=30), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_events_event_type')),
sa.UniqueConstraint('name', name=op.f('uq_events_event_type_name'))
)
op.create_table(
"events_job_type",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_events_job_type")),
sa.UniqueConstraint("name", name=op.f("uq_events_job_type_name")),
op.create_table('events_job_type',
sa.Column('id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('name', sa.String(length=30), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_events_job_type')),
sa.UniqueConstraint('name', name=op.f('uq_events_job_type_name'))
)
op.create_table(
"events_event",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("start", flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column("end", flaschengeist.database.types.UtcDateTime(), nullable=True),
sa.Column("name", sa.String(length=255), nullable=True),
sa.Column("description", sa.String(length=512), nullable=True),
sa.Column("is_template", sa.Boolean(), nullable=True),
sa.Column("type_id", flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(
["type_id"],
["events_event_type.id"],
name=op.f("fk_events_event_type_id_events_event_type"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_events_event")),
op.create_table('events_event',
sa.Column('id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('start', flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column('end', flaschengeist.database.types.UtcDateTime(), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('description', sa.String(length=512), nullable=True),
sa.Column('is_template', sa.Boolean(), nullable=True),
sa.Column('type_id', flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(['type_id'], ['events_event_type.id'], name=op.f('fk_events_event_type_id_events_event_type'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_events_event'))
)
op.create_table(
"events_job",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("start", flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column("end", flaschengeist.database.types.UtcDateTime(), nullable=True),
sa.Column("comment", sa.String(length=256), nullable=True),
sa.Column("type_id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("locked", sa.Boolean(), nullable=False),
sa.Column("required_services", sa.Numeric(precision=4, scale=2, asdecimal=False), nullable=False),
sa.Column("event_id", flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(["event_id"], ["events_event.id"], name=op.f("fk_events_job_event_id_events_event")),
sa.ForeignKeyConstraint(
["type_id"], ["events_job_type.id"], name=op.f("fk_events_job_type_id_events_job_type")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_events_job")),
sa.UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),
op.create_table('events_job',
sa.Column('id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('start', flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column('end', flaschengeist.database.types.UtcDateTime(), nullable=True),
sa.Column('comment', sa.String(length=256), nullable=True),
sa.Column('type_id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('locked', sa.Boolean(), nullable=False),
sa.Column('required_services', sa.Numeric(precision=4, scale=2, asdecimal=False), nullable=False),
sa.Column('event_id', flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(['event_id'], ['events_event.id'], name=op.f('fk_events_job_event_id_events_event')),
sa.ForeignKeyConstraint(['type_id'], ['events_job_type.id'], name=op.f('fk_events_job_type_id_events_job_type')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_events_job')),
sa.UniqueConstraint('type_id', 'start', 'event_id', name='_type_start_uc')
)
op.create_table(
"events_invitation",
sa.Column("id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("time", flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column("job_id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("invitee_id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("inviter_id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("transferee_id", flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(["invitee_id"], ["user.id"], name=op.f("fk_events_invitation_invitee_id_user")),
sa.ForeignKeyConstraint(["inviter_id"], ["user.id"], name=op.f("fk_events_invitation_inviter_id_user")),
sa.ForeignKeyConstraint(["job_id"], ["events_job.id"], name=op.f("fk_events_invitation_job_id_events_job")),
sa.ForeignKeyConstraint(["transferee_id"], ["user.id"], name=op.f("fk_events_invitation_transferee_id_user")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_events_invitation")),
op.create_table('events_invitation',
sa.Column('id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('time', flaschengeist.database.types.UtcDateTime(), nullable=False),
sa.Column('job_id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('invitee_id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('inviter_id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('transferee_id', flaschengeist.database.types.Serial(), nullable=True),
sa.ForeignKeyConstraint(['invitee_id'], ['user.id'], name=op.f('fk_events_invitation_invitee_id_user')),
sa.ForeignKeyConstraint(['inviter_id'], ['user.id'], name=op.f('fk_events_invitation_inviter_id_user')),
sa.ForeignKeyConstraint(['job_id'], ['events_job.id'], name=op.f('fk_events_invitation_job_id_events_job')),
sa.ForeignKeyConstraint(['transferee_id'], ['user.id'], name=op.f('fk_events_invitation_transferee_id_user')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_events_invitation'))
)
op.create_table(
"events_service",
sa.Column("is_backup", sa.Boolean(), nullable=True),
sa.Column("value", sa.Numeric(precision=3, scale=2, asdecimal=False), nullable=False),
sa.Column("job_id", flaschengeist.database.types.Serial(), nullable=False),
sa.Column("user_id", flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(["job_id"], ["events_job.id"], name=op.f("fk_events_service_job_id_events_job")),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name=op.f("fk_events_service_user_id_user")),
sa.PrimaryKeyConstraint("job_id", "user_id", name=op.f("pk_events_service")),
op.create_table('events_service',
sa.Column('is_backup', sa.Boolean(), nullable=True),
sa.Column('value', sa.Numeric(precision=3, scale=2, asdecimal=False), nullable=False),
sa.Column('job_id', flaschengeist.database.types.Serial(), nullable=False),
sa.Column('user_id', flaschengeist.database.types.Serial(), nullable=False),
sa.ForeignKeyConstraint(['job_id'], ['events_job.id'], name=op.f('fk_events_service_job_id_events_job')),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_events_service_user_id_user')),
sa.PrimaryKeyConstraint('job_id', 'user_id', name=op.f('pk_events_service'))
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("events_service")
op.drop_table("events_invitation")
op.drop_table("events_job")
op.drop_table("events_event")
op.drop_table("events_job_type")
op.drop_table("events_event_type")
op.drop_table('events_service')
op.drop_table('events_invitation')
op.drop_table('events_job')
op.drop_table('events_event')
op.drop_table('events_job_type')
op.drop_table('events_event_type')
# ### end Alembic commands ###

View File

@ -24,9 +24,8 @@ def dict_get(self, key, default=None, type=None):
if type is not None:
try:
rv = type(rv)
except (ValueError, TypeError):
except ValueError:
rv = default
return rv
@ -458,9 +457,7 @@ def assign_job(job_id, current_session: Session):
):
raise Forbidden
if value > 0:
event_controller.assign_job(
job, user, value, data.get("is_backup", False), notify=user != current_session.user_
)
event_controller.assign_job(job, user, value, data.get("is_backup", False))
else:
event_controller.unassign_job(job, user, notify=user != current_session.user_)
except (TypeError, KeyError, ValueError):

View File

@ -112,7 +112,7 @@ import { notEmpty } from '@flaschengeist/api';
import { IsoDateInput } from '@flaschengeist/api/components';
import { useEventStore } from '../../store';
import { emptyEvent, Job, EditableEvent } from '../../store/models';
import { emptyEvent, emptyJob, EditableEvent } from '../../store/models';
import { date, DateOptions } from 'quasar';
import { computed, defineComponent, PropType, ref, onBeforeMount, watch } from 'vue';
@ -143,7 +143,7 @@ export default defineComponent({
const store = useEventStore();
const active = ref(0);
const activeJob = ref<{ validate: () => Promise<boolean> }[]>([]);
const activeJob = ref<{ validate: () => Promise<boolean> }>();
const templates = computed(() => store.templates);
const template = ref<FG.Event>();
const event = ref<EditableEvent>(props.modelValue || emptyEvent());
@ -166,11 +166,12 @@ export default defineComponent({
function addJob() {
if (!activeJob.value[active.value]) {
event.value.jobs.push(new Job());
event.value.jobs.push(emptyJob());
active.value = event.value.jobs.length - 1;
} else
void activeJob.value[active.value].validate().then((success) => {
if (success) {
event.value.jobs.push(new Job());
event.value.jobs.push(emptyJob());
active.value = event.value.jobs.length - 1;
}
});

View File

@ -33,7 +33,7 @@
input-debounce="0"
class="col-xs-12 col-sm-6 q-pa-sm"
:options="jobtypes"
:option-label="(jobtype) => (typeof jobtype === 'number' ? '' : jobtype.name)"
option-label="name"
option-value="id"
map-options
clearable

View File

@ -16,31 +16,31 @@
</div>
</q-card>
</q-dialog>
<!-- <div class="q-pa-md"> -->
<!-- <q-card style="height: 70vh; max-width: 1800px" class="q-pa-md"> -->
<div ref="scrollDiv" class="scroll" style="height: 100%">
<q-infinite-scroll :offset="250" @load="load">
<q-list>
<q-item id="bbb">
<q-btn label="Ältere Veranstaltungen laden" @click="load(-1)" />
</q-item>
<template v-for="(events, index) in agendas" :key="index">
<q-separator />
<q-item-label header>{{ asDate(index) }}</q-item-label>
<q-item v-for="(event, idx) in events" :key="idx">
<event-slot :model-value="event" />
</q-item>
</template>
</q-list>
<template #loading>
<div class="row justify-center q-my-md">
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
<div class="q-pa-md">
<q-card style="height: 70vh; max-width: 1800px" class="q-pa-md">
<div ref="scrollDiv" class="scroll" style="height: 100%">
<q-infinite-scroll :offset="250" @load="load">
<q-list>
<q-item id="bbb">
<q-btn label="Ältere Veranstaltungen laden" @click="load(-1)" />
</q-item>
<template v-for="(events, index) in agendas" :key="index">
<q-separator />
<q-item-label overline>{{ index }}</q-item-label>
<q-item v-for="(event, idx) in events" :key="idx"
><event-slot :model-value="event" />{{ idx }}</q-item
>
</template>
</q-list>
<template #loading>
<div class="row justify-center q-my-md">
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</div>
</q-card>
</div>
<!-- </q-card> -->
<!-- </div> -->
</template>
<script lang="ts">
@ -157,15 +157,6 @@ export default defineComponent({
}
}
function asDate(value: string) {
if (value) {
const year = parseInt(value.substring(0, 4));
const month = parseInt(value.substring(4, 6)) - 1;
const day = parseInt(value.substring(6, 8));
return date.formatDate(new Date(year, month, day), 'DD.MM.YYYY');
}
}
return {
agendas,
asYear,
@ -176,7 +167,6 @@ export default defineComponent({
editDone,
load,
remove,
asDate,
};
},
});

View File

@ -10,11 +10,8 @@ const EventTypes = {
invite: 0x01,
transfer: 0x02,
invitation_response: 0x10,
invitation_accepted: 0x11,
invitation_rejected: 0x12,
info: 0x20,
info_accepted: 0x21,
info_rejected: 0x22,
invitation_accepted: 0x10,
invitation_rejected: 0x11,
};
function transpile(msg: FG_Plugin.Notification) {
@ -32,10 +29,7 @@ function transpile(msg: FG_Plugin.Notification) {
};
message.link = { name: 'events-requests' };
} else if (
(message.data.type & EventTypes._mask_) === EventTypes.invitation_response ||
(message.data.type & EventTypes._mask_) === EventTypes.info
) {
} else if ((message.data.type & EventTypes._mask_) === EventTypes.invitation_response) {
message.link = {
name: 'events-single-view',
params: { id: (<InvitationResponseData>message.data).event },
@ -45,7 +39,7 @@ function transpile(msg: FG_Plugin.Notification) {
}
const plugin: FG_Plugin.Plugin = {
id: 'events',
id: 'dev.flaschengeist.events',
name: 'Event schedule',
innerRoutes,
internalRoutes: privateRoutes,

View File

@ -43,21 +43,15 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref, onBeforeMount } from 'vue';
import { computed, defineComponent, ref } from 'vue';
import { useQuasar } from 'quasar';
import AgendaView from '../components/overview/AgendaView.vue';
import ListView from '../components/overview/ListView.vue';
import { useEventStore } from '../store';
export default defineComponent({
name: 'EventOverview',
components: { AgendaView, ListView },
setup() {
const store = useEventStore();
onBeforeMount(() => {
void store.getJobTypes();
});
const quasar = useQuasar();
const tabs = computed(() => [

View File

@ -1,312 +1,8 @@
<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-card>
<q-card-section class="row"> </q-card-section>
<q-card-section> </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';
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).then(() => {
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).then(() => {
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>

View File

@ -1,7 +1,6 @@
import { api, isAxiosError } from '@flaschengeist/api';
import { defineStore } from 'pinia';
import { EditableEvent } from './models';
import { Notify } from 'quasar';
/**
* Convert JSON decoded Job to real job (fix Date object)
@ -205,47 +204,13 @@ export const useEventStore = defineStore({
},
async rejectInvitation(invite: FG.Invitation | number) {
try {
await api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
const idx = this.invitations.findIndex((v) => v.id === (invite.id || invite));
if (idx >= 0) this.invitations.splice(idx, 1);
notify_success('Einladung für erfolgreich abgelehnt');
} catch (e) {
notify_failure();
}
return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
},
async acceptInvitation(invite: FG.Invitation | number) {
try {
await api.put(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`, {
accept: true,
});
const idx = this.invitations.findIndex((v) => v.id === (invite.id || invite));
if (idx >= 0) this.invitations.splice(idx, 1);
notify_success('Einladung für erfolgreich angenommen');
} catch (e) {
notify_failure();
}
return api.put(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`, {
accept: true,
});
},
},
});
function notify_failure() {
Notify.create({
message: 'Es ist ein Fehler aufgetreten.',
color: 'negative',
group: false,
timeout: 10000,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}
function notify_success(msg: string) {
Notify.create({
message: msg,
color: 'positive',
group: false,
timeout: 5000,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}

View File

@ -4,76 +4,32 @@ import { date } from 'quasar';
export type EditableEvent = Omit<Omit<Omit<FG.Event, 'jobs'>, 'type'>, 'id'> & {
type?: FG.EventType | number;
id?: number;
jobs: Job[];
jobs: EditableJob[];
};
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;
/** 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;
};
/**
* 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 || iJob.end === undefined) {
this.end = date.buildDate({
hours: new Date().getHours() + 4,
minutes: 0,
seconds: 0,
milliseconds: 0,
});
}
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 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,
};
}
export function emptyEvent(startDate: Date = new Date()): EditableEvent {
return {
start: date.adjustDate(startDate, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }),
jobs: [Job.fromDate(startDate, true, 4)],
jobs: [emptyJob(startDate)],
is_template: false,
};
}