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
21 changed files with 234 additions and 727 deletions

6
.gitignore vendored
View File

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

View File

@ -2,7 +2,6 @@
Provides duty schedule / duty roster functions
"""
import pkg_resources
from flask import Blueprint, current_app
from werkzeug.local import LocalProxy
@ -10,28 +9,15 @@ from flaschengeist.plugins import Plugin
from . import permissions, models
__version__ = pkg_resources.get_distribution("flaschengeist_events").version
class EventPlugin(Plugin):
# id = "dev.flaschengeist.events"
# provided resources
# permissions = permissions.permissions
name = "events"
id = "dev.flaschengeist.plugins.events"
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name])
permissions = permissions.permissions
blueprint = Blueprint(name, __name__)
models = models
# def __init__(self, cfg):
# super(EventPlugin, self).__init__(cfg)
# from . import routes
# from .event_controller import clear_services
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"])
def __init__(self, cfg):
super(EventPlugin, self).__init__(cfg)
from . import routes
from .event_controller import clear_services

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
@ -17,7 +16,6 @@ from flaschengeist.plugins.scheduler import scheduled
from . import EventPlugin
from .models import EventType, Event, Invitation, Job, JobType, Service
# STUB
def _(x):
return x
@ -28,11 +26,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
@ -48,8 +43,8 @@ def clear_services(user):
db.session.commit()
# def update():
# db.session.commit()
def update():
db.session.commit()
def get_event_types():
@ -197,9 +192,9 @@ def get_events(
if not with_backup:
for event in events:
clear_backup(event)
# logger.debug(end)
# for event in events:
# logger.debug(f"{event.start} < {end} = {event.start < end}")
logger.debug(end)
for event in events:
logger.debug(f"{event.start} < {end} = {event.start < end}")
return count, events
@ -239,10 +234,10 @@ def create_event(event_type, start, end=None, jobs=[], is_template=None, name=No
def get_job(job_id, event_id=None) -> Job:
query = db.select(Job).where(Job.id == job_id)
query = Job.query.filter(Job.id == job_id)
if event_id is not None:
query = query.where(Job.event_id_ == event_id)
job = db.session.execute(query).scalar_one_or_none()
query = query.filter(Job.event_id_ == event_id)
job = query.one_or_none()
if job is None:
raise NotFound
return job
@ -299,7 +294,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 +302,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 +319,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 +346,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 +361,7 @@ def respond_invitation(invite: Invitation, accepted=True):
raise Conflict
if not accepted:
EventPlugin.getPlugin().notify(
EventPlugin.plugin.notify(
inviter,
_("Invitation rejected"),
{
@ -406,12 +375,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 +401,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 +411,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

@ -1,105 +0,0 @@
"""init events
Revision ID: e70508bd8cb4
Revises: 20482a003db8
Create Date: 2023-04-10 14:21:47.007251
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
# revision identifiers, used by Alembic.
revision = "e70508bd8cb4"
down_revision = None
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_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_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_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")
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime
from typing import Optional, Union, List
from typing import Optional, Union
from sqlalchemy import UniqueConstraint
@ -17,7 +17,6 @@ _table_prefix_ = "events_"
class EventType(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "event_type"
id: int = db.Column(Serial, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
@ -35,7 +34,6 @@ class JobType(db.Model, ModelSerializeMixin):
class Service(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "service"
userid: str = ""
is_backup: bool = db.Column(db.Boolean, default=False)
@ -59,7 +57,6 @@ class Service(db.Model, ModelSerializeMixin):
class Job(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "job"
id: int = db.Column(Serial, primary_key=True)
@ -68,7 +65,7 @@ class Job(db.Model, ModelSerializeMixin):
comment: Optional[str] = db.Column(db.String(256))
type: int = db.Column("type_id", Serial, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
locked: bool = db.Column(db.Boolean(), default=False, nullable=False)
services: List[Service] = db.relationship(
services: list[Service] = db.relationship(
"Service", back_populates="job_", cascade="save-update, merge, delete, delete-orphan"
)
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
@ -88,7 +85,6 @@ class Job(db.Model, ModelSerializeMixin):
class Event(db.Model, ModelSerializeMixin):
"""Model for an Event"""
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "event"
id: int = db.Column(Serial, primary_key=True)
start: datetime = db.Column(UtcDateTime, nullable=False)
@ -97,7 +93,7 @@ class Event(db.Model, ModelSerializeMixin):
description: Optional[str] = db.Column(db.String(512))
type: Union[EventType, int] = db.relationship("EventType")
is_template: bool = db.Column(db.Boolean, default=False)
jobs: List[Job] = db.relationship(
jobs: list[Job] = db.relationship(
"Job",
back_populates="event_",
cascade="all,delete,delete-orphan",
@ -113,7 +109,6 @@ class Event(db.Model, ModelSerializeMixin):
class Invitation(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "invitation"
id: int = db.Column(Serial, primary_key=True)

View File

@ -1,5 +1,5 @@
from http.client import NO_CONTENT
from flask import request, jsonify, Blueprint
from flask import request, jsonify
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
from flaschengeist.models.session import Session
@ -7,14 +7,10 @@ from flaschengeist.controller import userController
from flaschengeist.utils.decorators import login_required
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.utils.HTTP import get_filter_args, no_content
from flaschengeist import logger
from . import event_controller, permissions, EventPlugin
blueprint = Blueprint("events", __name__)
def dict_get(self, key, default=None, type=None):
"""Same as .get from MultiDict"""
try:
@ -24,20 +20,19 @@ 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
@blueprint.route("/events/templates", methods=["GET"])
@EventPlugin.blueprint.route("/events/templates", methods=["GET"])
@login_required()
def get_templates(current_session):
return jsonify(event_controller.get_templates())
@blueprint.route("/events/event-types", methods=["GET"])
@blueprint.route("/events/event-types/<int:identifier>", methods=["GET"])
@EventPlugin.blueprint.route("/events/event-types", methods=["GET"])
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["GET"])
@login_required()
def get_event_types(current_session, identifier=None):
"""Get EventType(s)
@ -59,7 +54,7 @@ def get_event_types(current_session, identifier=None):
return jsonify(result)
@blueprint.route("/events/event-types", methods=["POST"])
@EventPlugin.blueprint.route("/events/event-types", methods=["POST"])
@login_required(permission=permissions.EVENT_TYPE)
def new_event_type(current_session):
"""Create a new EventType
@ -81,7 +76,7 @@ def new_event_type(current_session):
return jsonify(event_type)
@blueprint.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.EVENT_TYPE)
def modify_event_type(identifier, current_session):
"""Rename or delete an event type
@ -107,7 +102,7 @@ def modify_event_type(identifier, current_session):
return "", NO_CONTENT
@blueprint.route("/events/job-types", methods=["GET"])
@EventPlugin.blueprint.route("/events/job-types", methods=["GET"])
@login_required()
def get_job_types(current_session):
"""Get all JobTypes
@ -124,7 +119,7 @@ def get_job_types(current_session):
return jsonify(types)
@blueprint.route("/events/job-types", methods=["POST"])
@EventPlugin.blueprint.route("/events/job-types", methods=["POST"])
@login_required(permission=permissions.JOB_TYPE)
def new_job_type(current_session):
"""Create a new JobType
@ -146,7 +141,7 @@ def new_job_type(current_session):
return jsonify(jt)
@blueprint.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
@EventPlugin.blueprint.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.JOB_TYPE)
def modify_job_type(type_id, current_session):
"""Rename or delete a JobType
@ -172,7 +167,7 @@ def modify_job_type(type_id, current_session):
return "", NO_CONTENT
@blueprint.route("/events/<int:event_id>", methods=["GET"])
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["GET"])
@login_required()
def get_event(event_id, current_session):
"""Get event by id
@ -193,7 +188,7 @@ def get_event(event_id, current_session):
return jsonify(event)
@blueprint.route("/events", methods=["GET"])
@EventPlugin.blueprint.route("/events", methods=["GET"])
@login_required()
def get_events(current_session):
count, result = event_controller.get_events(
@ -209,39 +204,21 @@ def _add_job(event, data):
end = dict_get(data, "end", None, type=from_iso_format)
required_services = data["required_services"]
job_type = int(data["type"])
job_type = event_controller.get_job_type(job_type)
job_id = dict_get(data, "id", None, int)
if job_id:
job = next(job for job in event.jobs if job.id == job_id)
job.event = event
job.job_type = job_type
job.start = start
job.end = end
job.required_services = required_services
else:
event_controller.add_job(
event,
job_type,
required_services,
start,
end,
comment=dict_get(data, "comment", None, str),
)
except (KeyError, ValueError):
raise BadRequest("Missing or invalid POST parameter")
except StopIteration:
raise BadRequest("Job not in event")
job_type = event_controller.get_job_type(job_type)
event_controller.add_job(
event,
job_type,
required_services,
start,
end,
comment=dict_get(data, "comment", None, str),
)
def _delete_jobs_from_event(event, data):
job_ids = [x["id"] for x in data if "id" in x]
for job in event.jobs:
if job.id not in job_ids:
event.jobs.remove(job)
@blueprint.route("/events", methods=["POST"])
@EventPlugin.blueprint.route("/events", methods=["POST"])
@login_required(permission=permissions.CREATE)
def create_event(current_session):
"""Create an new event
@ -282,7 +259,7 @@ def create_event(current_session):
raise BadRequest("Invalid parameter")
@blueprint.route("/events/<int:event_id>", methods=["PUT"])
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
def modify_event(event_id, current_session):
"""Modify an event
@ -300,7 +277,6 @@ def modify_event(event_id, current_session):
"""
event = event_controller.get_event(event_id)
data = request.get_json()
logger.debug("PUT data: %s", data)
event.start = dict_get(data, "start", event.start, type=from_iso_format)
event.end = dict_get(data, "end", event.end, type=from_iso_format)
event.name = dict_get(data, "name", event.name, type=str)
@ -308,15 +284,12 @@ def modify_event(event_id, current_session):
if "type" in data:
event_type = event_controller.get_event_type(data["type"])
event.type = event_type
if "jobs" in data:
_delete_jobs_from_event(event, data["jobs"])
for job in data["jobs"]:
_add_job(event, job)
event_controller.update()
return jsonify(event)
@blueprint.route("/events/<int:event_id>", methods=["DELETE"])
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_event(event_id, current_session):
"""Delete an event
@ -334,7 +307,7 @@ def delete_event(event_id, current_session):
return "", NO_CONTENT
@blueprint.route("/events/<int:event_id>/jobs", methods=["POST"])
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs", methods=["POST"])
@login_required(permission=permissions.EDIT)
def add_job(event_id, current_session):
"""Add an new Job to an Event / EventSlot
@ -355,7 +328,7 @@ def add_job(event_id, current_session):
return jsonify(event)
@blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_job(event_id, job_id, current_session):
"""Delete a Job
@ -375,7 +348,7 @@ def delete_job(event_id, job_id, current_session):
return no_content()
@blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
@login_required()
def update_job(event_id, job_id, current_session: Session):
"""Edit Job
@ -417,20 +390,20 @@ def update_job(event_id, job_id, current_session: Session):
return jsonify(job)
@blueprint.route("/events/jobs", methods=["GET"])
@EventPlugin.blueprint.route("/events/jobs", methods=["GET"])
@login_required()
def get_jobs(current_session: Session):
count, result = event_controller.get_jobs(current_session.user_, *get_filter_args())
return jsonify({"count": count, "result": result})
@blueprint.route("/events/jobs/<int:job_id>", methods=["GET"])
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>", methods=["GET"])
@login_required()
def get_job(job_id, current_session: Session):
return jsonify(event_controller.get_job(job_id))
@blueprint.route("/events/jobs/<int:job_id>/assign", methods=["POST"])
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/assign", methods=["POST"])
@login_required()
def assign_job(job_id, current_session: Session):
"""Assign / unassign user to the Job
@ -458,9 +431,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):
@ -468,7 +439,7 @@ def assign_job(job_id, current_session: Session):
return jsonify(job)
@blueprint.route("/events/jobs/<int:job_id>/lock", methods=["POST"])
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/lock", methods=["POST"])
@login_required(permissions.LOCK_JOBS)
def lock_job(job_id, current_session: Session):
"""Lock / unlock the Job
@ -495,7 +466,7 @@ def lock_job(job_id, current_session: Session):
return no_content()
@blueprint.route("/events/invitations", methods=["POST"])
@EventPlugin.blueprint.route("/events/invitations", methods=["POST"])
@login_required()
def invite(current_session: Session):
"""Invite an user to a job or transfer job
@ -533,13 +504,13 @@ def invite(current_session: Session):
raise BadRequest
@blueprint.route("/events/invitations", methods=["GET"])
@EventPlugin.blueprint.route("/events/invitations", methods=["GET"])
@login_required()
def get_invitations(current_session: Session):
return jsonify(event_controller.get_invitations(current_session.user_))
@blueprint.route("/events/invitations/<int:invitation_id>", methods=["GET"])
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["GET"])
@login_required()
def get_invitation(invitation_id: int, current_session: Session):
inv = event_controller.get_invitation(invitation_id)
@ -548,7 +519,7 @@ def get_invitation(invitation_id: int, current_session: Session):
return jsonify(inv)
@blueprint.route("/events/invitations/<int:invitation_id>", methods=["DELETE", "PUT"])
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["DELETE", "PUT"])
@login_required()
def respond_invitation(invitation_id: int, current_session: Session):
inv = event_controller.get_invitation(invitation_id)

View File

@ -1,6 +1,6 @@
[metadata]
license = MIT
version = 1.0.0
version = 0.0.1-dev.1
name = flaschengeist-events
description = Events plugin for Flaschengeist
url = https://flaschengeist.dev/Flaschengeist/flaschengeist-schedule
@ -24,10 +24,7 @@ python_requires = >=3.8
packages =
flaschengeist_events
install_requires =
flaschengeist >= 2.0.0
[options.package_data]
* = *.toml, script.py.mako, *.ini, migrations/*.py
flaschengeist == 2.0.*
[options.entry_points]
flaschengeist.plugins =

View File

@ -1,6 +1,6 @@
{
"license": "MIT",
"version": "1.1.0",
"version": "1.0.0-alpha.6",
"name": "@flaschengeist/schedule",
"author": "Ferdinand Thiessen <rpm@fthiessen.de>",
"homepage": "https://flaschengeist.dev/Flaschengeist",
@ -22,24 +22,24 @@
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.11"
},
"devDependencies": {
"@flaschengeist/api": "^1.0.0",
"@flaschengeist/types": "^1.0.0",
"@quasar/app-webpack": "^3.7.2",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"@flaschengeist/api": "^1.0.0-alpha.7",
"@flaschengeist/types": "^1.0.0-alpha.10",
"@quasar/app": "^3.2.5",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"axios": "^0.24.0",
"eslint": "^8.5.0",
"eslint": "^8.4.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.2.0",
"pinia": "^2.0.8",
"pinia": "^2.0.6",
"prettier": "^2.5.1",
"quasar": "^2.11.10",
"typescript": "^4.5.4"
"quasar": "^2.3.4",
"typescript": "^4.5.2"
},
"peerDependencies": {
"@flaschengeist/api": "^1.0.0",
"@flaschengeist/users": "^1.0.0"
"@flaschengeist/api": "^1.0.0-alpha.7",
"@flaschengeist/users": "^1.0.0-alpha.3"
},
"prettier": {
"singleQuote": true,

View File

@ -52,7 +52,6 @@
class="col-xs-12 col-sm-6 q-pa-sm"
label="Veranstaltungsende"
:rules="[afterStart]"
:key="update_time"
/>
<q-input
v-model="event.description"
@ -62,7 +61,7 @@
filled
/>
</q-card-section>
<q-card-section v-if="modelValue === undefined">
<q-card-section v-if="event.is_template !== true && modelValue === undefined">
<q-btn-toggle
v-model="recurrent"
spread
@ -82,14 +81,14 @@
<q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
</div>
</div>
<template v-for="(job, index) in event.jobs" :key="index + update_time">
<template v-for="(job, index) in event.jobs" :key="index">
<edit-job-slot
ref="activeJob"
:ref="active === index ? 'activeJob' : undefined"
v-model="event.jobs[index]"
:active="index === active"
class="q-mb-md"
@remove-job="removeJob(index)"
@activate="activate(index)"
@remove-job="removeJob(index)"
/>
</template>
</q-card-section>
@ -114,7 +113,7 @@ import { IsoDateInput } from '@flaschengeist/api/components';
import { useEventStore } from '../../store';
import { emptyEvent, Job, EditableEvent } from '../../store/models';
import { date, DateOptions } from 'quasar';
import { date, ModifyDateOptions } from 'quasar';
import { computed, defineComponent, PropType, ref, onBeforeMount, watch } from 'vue';
import EditJobSlot from './EditJobSlot.vue';
@ -138,13 +137,12 @@ export default defineComponent({
},
emits: {
done: (val: boolean) => typeof val === 'boolean',
'update:modelValue': (val?: FG.Event) => typeof val === 'object',
},
setup(props, { emit }) {
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,10 +164,9 @@ export default defineComponent({
);
function addJob() {
if (!activeJob.value[active.value]) {
event.value.jobs.push(new Job());
} else
void activeJob.value[active.value].validate().then((success) => {
if (!activeJob.value) event.value.jobs.push(new Job());
else
void activeJob.value.validate().then((success) => {
if (success) {
event.value.jobs.push(new Job());
active.value = event.value.jobs.length - 1;
@ -183,7 +180,7 @@ export default defineComponent({
}
function fromTemplate(tpl: FG.Event) {
const today = props.modelValue?.start || new Date();
const today = new Date();
template.value = tpl;
event.value = Object.assign({}, tpl, { id: undefined });
@ -197,31 +194,23 @@ export default defineComponent({
const diff = event.value.start.getTime() - tpl.start.getTime();
// Adjust end of event and all jobs
if (event.value.end) event.value.end.setTime(event.value.end.getTime() + diff);
event.value.jobs = [];
tpl.jobs.forEach((job) => {
const copied_job: FG.Job = Object.assign({}, job, {
id: NaN,
start: new Date(),
end: undefined,
});
copied_job.start.setTime(job.start.getTime() + diff);
if (job.end) {
copied_job.end = new Date();
copied_job.end.setTime(job.end.getTime() + diff);
}
event.value.jobs.push(<Job>copied_job);
event.value.jobs.forEach((job) => {
job.start.setTime(job.start.getTime() + diff);
if (job.end) job.end.setTime(job.end.getTime() + diff);
});
}
async function save(is_template = false) {
event.value.is_template = is_template;
async function save(template = false) {
event.value.is_template = template;
try {
const _event = await store.addEvent(event.value);
emit('update:modelValue', _event);
if (event.value?.id !== undefined) {
//fix
}
await store.addEvent(event.value);
if (props.modelValue === undefined && recurrent.value && !event.value.is_template) {
let count = 0;
const options: DateOptions = {};
const options: ModifyDateOptions = {};
switch (recurrenceRule.value.frequency) {
case 'daily':
options['days'] = 1 * recurrenceRule.value.interval;
@ -274,60 +263,12 @@ export default defineComponent({
!d || event.value.start <= d || 'Das Veranstaltungsende muss vor dem Beginn liegen';
function activate(idx: number) {
void activeJob.value[active.value]?.validate().then((s) => {
void activeJob.value?.validate().then((s) => {
if (s) active.value = idx;
});
}
const computed_start = computed({
get: () => event.value?.start,
set: (value) => {
event.value.start = value;
},
});
const computed_end = computed({
get: () => event.value?.end,
set: (value) => {
event.value.end = value;
},
});
const update_time = ref(false);
watch(computed_start, (newValue, oldValue) => {
update_time.value = true;
const diff = newValue.getTime() - oldValue.getTime();
event.value?.jobs.forEach((job) => {
job.start.setTime(job.start.getTime() + diff);
job.end?.setTime(job.end.getTime() + diff);
});
computed_end.value?.setTime(computed_end.value?.getTime() + diff);
setTimeout(() => {
update_time.value = false;
}, 0);
});
watch(computed_end, (newValue, oldValue) => {
if (newValue && oldValue) {
update_time.value = true;
if (!newValue || !oldValue) return;
const diff = newValue.getTime() - oldValue.getTime();
event.value?.jobs.forEach((job) => {
if (job.end) job.end.setTime(job.end.getTime() + diff);
else job.end = new Date(newValue.getTime());
});
} else if (newValue && !oldValue) {
event.value?.jobs.forEach((job) => {
if (!job.end) job.end = new Date(newValue.getTime());
});
}
setTimeout(() => {
update_time.value = false;
}, 0);
});
return {
update_time,
activate,
active,
addJob,

View File

@ -33,19 +33,19 @@
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
:rules="[notEmpty]"
/>
<q-input
v-model.number="job.required_services"
v-model="job.required_services"
filled
class="col-xs-12 col-sm-6 q-pa-sm"
label="Dienstanzahl"
type="number"
:rules="[minOneService, notEmpty]"
:rules="[notEmpty]"
/>
<q-input
v-model="job.comment"
@ -134,10 +134,6 @@ export default defineComponent({
validate: () => form.value?.validate() || Promise.resolve(true),
});
function minOneService(val: number) {
return parseInt(val) > 0 || 'Mindestens ein Dienst nötig';
}
return {
form,
formatStartEnd,
@ -145,7 +141,6 @@ export default defineComponent({
job,
jobtypes,
notEmpty,
minOneService,
typeName,
};
},

View File

@ -28,28 +28,31 @@
</q-card>
</q-dialog>
<q-table :title="title" :rows="rows" row-key="id" :columns="columns">
<template #top-right>
<q-input
ref="input"
v-model="actualType.name"
:rules="rules"
dense
filled
placeholder="Neuer Typ"
>
<template #after
><q-btn color="primary" icon="mdi-plus" title="Hinzufügen" round @click="addType"
/></template>
</q-input>
</template>
<template #body-cell-actions="props">
<q-td :props="props" align="right" :auto-width="true">
<q-btn round icon="mdi-pencil" @click="editType(props.row.id)" />
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
</q-td>
</template>
</q-table>
<q-card>
<q-card-section>
<q-table :title="title" :rows="rows" row-key="id" :columns="columns">
<template #top-right>
<q-input
ref="input"
v-model="actualType.name"
:rules="rules"
dense
placeholder="Neuer Typ"
>
<slot name="after"
><q-btn color="primary" icon="mdi-plus" title="Hinzufügen" @click="addType"
/></slot>
</q-input>
</template>
<template #body-cell-actions="props">
<q-td :props="props" align="right" :auto-width="true">
<q-btn round icon="mdi-pencil" @click="editType(props.row.id)" />
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</template>

View File

@ -88,10 +88,9 @@
</template>
<script lang="ts">
import { ComponentPublicInstance, computed, defineComponent, onBeforeMount, ref, watch } from 'vue';
import { ComponentPublicInstance, computed, defineComponent, onBeforeMount, ref } from 'vue';
import { QCalendarAgenda } from '@quasar/quasar-ui-qcalendar';
import { date, QDate, QPopupProxy, useQuasar } from 'quasar';
import { useRoute, useRouter } from 'vue-router';
import { EditableEvent, emptyEvent } from '../../store/models';
import { startOfWeek } from '@flaschengeist/api';
import { useEventStore } from '../../store';
@ -106,8 +105,6 @@ export default defineComponent({
setup() {
const store = useEventStore();
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const datepicker = ref<QDate>();
const proxy = ref<QPopupProxy>();
@ -144,19 +141,6 @@ export default defineComponent({
}
onBeforeMount(async () => {
await router.replace({ query: { ...route.query, q_tab: 'agendaView' } });
if (!Object.keys(route.query).includes('q_date')) {
const q_date = date.formatDate(selectedDate.value, 'YYYY-MM-DD');
await router.replace({ query: { ...route.query, q_date } });
} else {
selectedDate.value = date.extractDate(route.query.q_date as string, 'YYYY-MM-DD');
}
if (!Object.keys(route.query).includes('q_view')) {
const q_view = calendarView.value;
await router.replace({ query: { ...route.query, q_view } });
} else {
calendarView.value = route.query.q_view as string;
}
await loadAgendas();
});
@ -187,11 +171,7 @@ export default defineComponent({
}
}
const loading = ref(false);
async function loadAgendas() {
if (loading.value) return;
loading.value = true;
const from =
calendarView.value === 'day' ? selectedDate.value : startOfWeek(selectedDate.value);
const to = date.addToDate(from, { days: calendarView.value === 'day' ? 1 : 7 });
@ -204,11 +184,8 @@ export default defineComponent({
if (!events.value[day]) {
events.value[day] = [];
}
const idx = events.value[day].findIndex((e) => e.id === event.id);
if (idx === -1) events.value[day].push(event);
else events.value[day][idx] = event;
events.value[day].push(event);
});
loading.value = false;
}
function addDays(reverse: boolean) {
@ -235,24 +212,12 @@ export default defineComponent({
'November',
'Dezember',
'-',
][value?.getMonth() === 0 ? 0 : value?.getMonth() || 12];
][value?.getMonth() || 12];
}
function asYear(value?: Date) {
return value?.getFullYear() || '-';
}
watch(selectedDate, async (newValue) => {
const q_date = date.formatDate(newValue, 'YYYY-MM-DD');
await router.replace({ query: { ...route.query, q_date } });
await loadAgendas();
});
watch(calendarView, async (newValue) => {
const q_view = newValue;
await router.replace({ query: { ...route.query, q_view } });
await loadAgendas();
});
return {
asYear,
asMonth,

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 v-model="events[idx]" @edit-event="edit" @remove-event="remove" />
</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">
@ -88,10 +88,6 @@ export default defineComponent({
function editDone(changed: boolean) {
//if (changed) void loadAgendas();
const idx = events.value.findIndex((event) => event.id === editor.value?.id);
if (idx >= 0) {
events.value[idx] = editor.value as FG.Event;
}
editor.value = undefined;
}
@ -99,12 +95,7 @@ export default defineComponent({
const start = new Date();
if (index < 0) {
const { result } = await store.getEvents({ to: start, limit: 5, descending: true });
//events.value.unshift(...result);
for (const event of result) {
const idx = events.value.findIndex((e) => e.id === event.id);
if (idx === -1) events.value.unshift(event);
else events.value[idx] = event;
}
events.value.unshift(...result);
if (done) done(false);
} else {
const len = events.value.length;
@ -113,12 +104,7 @@ export default defineComponent({
offset: (index - 1) * 10,
limit: 10,
});
for (const event of result) {
const idx = events.value.findIndex((e) => e.id === event.id);
if (idx === -1) events.value.unshift(event);
else events.value[idx] = event;
}
if (len == events.value.length) {
if (len == events.value.push(...result)) {
if (done) return done(true);
} else if (done) done(false);
}
@ -131,10 +117,16 @@ export default defineComponent({
async function remove(id: number) {
if (await store.removeEvent(id)) {
const idx = events.value.findIndex((event) => event.id === id);
if (idx !== -1) {
events.value.splice(idx, 1);
// Successfull removed
for (const idx in agendas.value) {
const i = agendas.value[idx].findIndex((event) => event.id === id);
if (i !== -1) {
agendas.value[idx].splice(i, 1);
break;
}
}
} else {
// Not found, this means our eventa are outdated
}
}
@ -165,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,
@ -184,7 +167,6 @@ export default defineComponent({
editDone,
load,
remove,
asDate,
};
},
});

View File

@ -54,7 +54,7 @@
</q-select>
<div class="row col-12 justify-end">
<q-btn
v-if="!modelValue.locked && !isEnrolled && !isFull && canAssign"
v-if="!modelValue.locked && !isEnrolled && !isFull"
flat
color="primary"
label="Eintragen"
@ -101,7 +101,6 @@ import { PERMISSIONS } from '../../../permissions';
import TransferInviteDialog from './TransferInviteDialog.vue';
import ServiceUserChip from './ServiceUserChip.vue';
import { UserAvatar } from '@flaschengeist/api/components';
import { DisplayNameMode } from '@flaschengeist/users';
export default defineComponent({
name: 'JobSlot',
@ -124,34 +123,11 @@ export default defineComponent({
const quasar = useQuasar();
// Make sure users are loaded if we can assign them
onBeforeMount(() => {
void userStore.getUsers();
void userStore.getDisplayNameModeSetting(true);
});
onBeforeMount(() => void userStore.getUsers());
/* Stuff used for general display */
// Get displayname of user
function userDisplay(id: string) {
switch (userStore.userSettings.display_name) {
case DisplayNameMode.FIRSTNAME:
return userStore.findUser(id)?.firstname || id;
case DisplayNameMode.LASTNAME:
return userStore.findUser(id)?.lastname || id;
case DisplayNameMode.DISPLAYNAME:
return userStore.findUser(id)?.display_name || id;
case DisplayNameMode.FIRSTNAME_LASTNAME:
return (
`${<string>userStore.findUser(id)?.firstname} ${<string>(
userStore.findUser(id)?.lastname
)}` || id
);
case DisplayNameMode.LASTNAME_FIRSTNAME:
return (
`${<string>userStore.findUser(id)?.lastname}, ${<string>(
userStore.findUser(id)?.firstname
)}` || id
);
}
return userStore.findUser(id)?.display_name || id;
}
@ -229,8 +205,6 @@ export default defineComponent({
// Current user can assign other users
const canAssignOther = computed(() => hasPermission(PERMISSIONS.ASSIGN_OTHER));
const canAssign = computed(() => hasPermission(PERMISSIONS.ASSIGN));
// options shown in the select
const options = ref([] as FG.Service[]);
@ -283,7 +257,6 @@ export default defineComponent({
unassign: (s?: FG.Service) => assign(Object.assign({}, s || service.value, { value: -1 })),
backup: (is_backup: boolean) => assign(Object.assign({}, service.value, { is_backup })),
canAssignOther,
canAssign,
canInvite,
filterUsers,
isBackup,

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

@ -44,8 +44,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { computed, defineComponent, ref } from 'vue';
import ManageTypes from '../components/management/ManageTypes.vue';
import EditEvent from '../components/management/EditEvent.vue';
import { hasPermission } from '@flaschengeist/api';
@ -71,21 +70,6 @@ export default defineComponent({
: []),
]);
const route = useRoute();
const router = useRouter();
onBeforeMount(async () => {
if (
(route.query.q_tab && route.query.q_tab === 'create') ||
route.query.q_tab === 'jobtypes' ||
route.query.q_tab === 'eventtypes'
) {
tab.value = route.query.q_tab;
} else {
await router.replace({ query: { q_tab: tab.value } });
}
});
const drawer = ref<boolean>(false);
const tab = ref<string>('create');
@ -98,10 +82,6 @@ export default defineComponent({
},
});
watch(tab, async (val) => {
await router.replace({ query: { q_tab: val } });
});
return {
showDrawer,
tab,

View File

@ -43,33 +43,15 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
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();
const route = useRoute();
const router = useRouter();
const tab = ref<string>('agendaView');
onBeforeMount(async () => {
void store.getJobTypes();
if (
Object.keys(route.query).includes('q_tab') &&
(route.query.q_tab === 'listView' || route.query.q_tab === 'agendaView')
) {
tab.value = route.query.q_tab as string;
} else {
await router.replace({ query: { ...route.query, q_tab: tab.value } });
}
});
const quasar = useQuasar();
const tabs = computed(() => [
@ -86,10 +68,7 @@ export default defineComponent({
},
});
watch(tab, async (val) => {
console.log(val);
await router.replace({ query: { ...route.query, q_tab: val } });
});
const tab = ref<string>('agendaView');
return {
showDrawer,

View File

@ -1,66 +1,49 @@
<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>
<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 { useRoute, useRouter } from 'vue-router';
import { QTableProps } from 'quasar';
import { Job } from '../store/models';
import { useEventStore } from '../store';
import { EventNotification, InvitationData, InvitationResponseData } from '../events';
export default defineComponent({
name: 'PageEventRequests',
@ -68,8 +51,6 @@ export default defineComponent({
const store = useEventStore();
const userStore = useUserStore();
const mainStore = useMainStore();
const router = useRouter();
const route = useRoute();
interface RowData extends FG.Invitation {
inviter: FG.User;
@ -96,9 +77,6 @@ export default defineComponent({
? 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[]) {
@ -158,11 +136,7 @@ export default defineComponent({
.finally(() => (loading.value = false));
};
onBeforeMount(async () => {
if (route.query.sent === 'true') {
showSent.value = true;
}
onBeforeMount(() => {
void Promise.allSettled([
userStore.getUsers(),
store.getInvitations(),
@ -172,9 +146,8 @@ export default defineComponent({
);
});
watch(showSent, async () => {
watch(showSent, () => {
onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] });
await router.replace({ query: { sent: showSent.value ? 'true' : 'false' } });
});
function getType(row: RowData) {
@ -245,57 +218,10 @@ export default defineComponent({
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;
},
field: (row: RowData) => ({
job: row.job_id,
sender: row.inviter_id === mainStore.currentUser.userid,
}),
},
];

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)
@ -147,16 +146,19 @@ export const useEventStore = defineStore({
},
async addEvent(event: EditableEvent) {
console.log('addEvent', event);
if (event?.id === undefined) {
const { data } = await api.post<FG.Event>('/events', event);
if (data.is_template) this.templates.push(data);
fixEvent(data);
return data;
} else {
if (typeof event.type === 'object') event.type = event.type.id;
const { data } = await api.put<FG.Event>(`/events/${event.id}`, event);
const { data } = await api.put<FG.Event>(
`/events/${event.id}`,
Object.assign(event, { jobs: undefined })
);
if (data.is_template) this.templates.push(data);
fixEvent(data);
return data;
}
},
@ -203,47 +205,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

@ -30,14 +30,6 @@ export class Job implements FG.Job {
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);
}

View File

@ -1,5 +1,5 @@
{
"extends": "@quasar/app-webpack/tsconfig-preset",
"extends": "@quasar/app/tsconfig-preset",
"target": "esnext",
"compilerOptions": {
"baseUrl": "./src/",