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 233 additions and 705 deletions

4
.gitignore vendored
View File

@ -7,7 +7,3 @@ yarn.lock
# Backend # Backend
*.egg-info *.egg-info
__pycache__ __pycache__
# IDE
.idea
*.swp

View File

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

View File

@ -1,8 +1,7 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from enum import IntEnum from enum import IntEnum
from typing import Optional, Tuple, Union from typing import Optional, Tuple
from flaschengeist.controller import userController from flaschengeist.models import UtcDateTime
from flaschengeist.models import Notification, UtcDateTime
from flaschengeist.models.user import User from flaschengeist.models.user import User
from werkzeug.exceptions import BadRequest, Conflict, NotFound from werkzeug.exceptions import BadRequest, Conflict, NotFound
@ -17,7 +16,6 @@ from flaschengeist.plugins.scheduler import scheduled
from . import EventPlugin from . import EventPlugin
from .models import EventType, Event, Invitation, Job, JobType, Service from .models import EventType, Event, Invitation, Job, JobType, Service
# STUB # STUB
def _(x): def _(x):
return x return x
@ -28,11 +26,8 @@ class NotifyType(IntEnum):
INVITE = 0x01 INVITE = 0x01
TRANSFER = 0x02 TRANSFER = 0x02
# Invitation responsed 0x10..0x1F # Invitation responsed 0x10..0x1F
INVITATION_ACCEPTED = 0x11 INVITATION_ACCEPTED = 0x10
INVITATION_REJECTED = 0x12 INVITATION_REJECTED = 0x11
# Information responses 0x20..0x2F
INFO_ACCEPTED = 0x21
INFO_REJECTED = 0x22
@before_delete_user @before_delete_user
@ -48,8 +43,8 @@ def clear_services(user):
db.session.commit() db.session.commit()
# def update(): def update():
# db.session.commit() db.session.commit()
def get_event_types(): def get_event_types():
@ -197,9 +192,9 @@ def get_events(
if not with_backup: if not with_backup:
for event in events: for event in events:
clear_backup(event) clear_backup(event)
# logger.debug(end) logger.debug(end)
# for event in events: for event in events:
# logger.debug(f"{event.start} < {end} = {event.start < end}") logger.debug(f"{event.start} < {end} = {event.start < end}")
return count, events 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: 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: if event_id is not None:
query = query.where(Job.event_id_ == event_id) query = query.filter(Job.event_id_ == event_id)
job = db.session.execute(query).scalar_one_or_none() job = query.one_or_none()
if job is None: if job is None:
raise NotFound raise NotFound
return job return job
@ -299,7 +294,7 @@ def delete_job(job: Job):
db.session.commit() 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 assert value > 0
service = Service.query.get((job.id, user.id_)) service = Service.query.get((job.id, user.id_))
if service: if service:
@ -307,17 +302,10 @@ def assign_job(job: Job, user, value, is_backup=False, notify=False):
service.is_backup = is_backup service.is_backup = is_backup
else: else:
job.services.append(Service(user_=user, value=value, is_backup=is_backup, job_=job)) 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() db.session.commit()
def unassign_job(job: Job = None, user=None, service=None, notify=False): def unassign_job(job: Job = None, user=None, service=None, notify=False):
_date = job.start.strftime("%d.%m.%Y")
if service is None: if service is None:
assert job is not None and user is not None assert job is not None and user is not None
service = Service.query.get((job.id, user.id_)) 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.delete(service)
db.session.commit() db.session.commit()
if notify: if notify:
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(user, "Your assignmet was cancelled", {"event_id": event_id})
user, f"Your assignmet was cancelled\n{_date}", {"type": NotifyType.INFO_REJECTED, "event_id": event_id}
)
def invite(job: Job, invitee, inviter, transferee=None): def invite(job: Job, invitee, inviter, transferee=None):
inv = Invitation(job_=job, inviter_=inviter, invitee_=invitee, transferee_=transferee) inv = Invitation(job_=job, inviter_=inviter, invitee_=invitee, transferee_=transferee)
db.session.add(inv) db.session.add(inv)
update() update()
_date = job.start.strftime("%d.%m.%Y")
if transferee is None: if transferee is None:
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(invitee, _("Job invitation"), {"type": NotifyType.INVITE, "invitation": inv.id})
invitee, _(f"Job invitation\n{_date}"), {"type": NotifyType.INVITE, "invitation": inv.id}
)
else: else:
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(invitee, _("Job transfer"), {"type": NotifyType.TRANSFER, "invitation": inv.id})
invitee, _(f"Job transfer\n{_date}"), {"type": NotifyType.TRANSFER, "invitation": inv.id}
)
return inv return inv
@ -365,21 +346,9 @@ def get_invitations(user: User):
).all() ).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): def cancel_invitation(inv: Invitation):
db.session.delete(inv) db.session.delete(inv)
db.session.commit() db.session.commit()
cleanup_notifications(inv)
def respond_invitation(invite: Invitation, accepted=True): def respond_invitation(invite: Invitation, accepted=True):
@ -392,7 +361,7 @@ def respond_invitation(invite: Invitation, accepted=True):
raise Conflict raise Conflict
if not accepted: if not accepted:
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(
inviter, inviter,
_("Invitation rejected"), _("Invitation rejected"),
{ {
@ -406,12 +375,12 @@ def respond_invitation(invite: Invitation, accepted=True):
if invite.transferee_id is None: if invite.transferee_id is None:
assign_job(job, invite.invitee_, 1) assign_job(job, invite.invitee_, 1)
else: 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: if not service:
raise Conflict raise Conflict
unassign_job(job, invite.transferee_, service[0], True) unassign_job(job, invite.transferee_, service[0], True)
assign_job(job, invite.invitee_, service[0].value) assign_job(job, invite.invitee_, service[0].value)
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(
inviter, inviter,
_("Invitation accepted"), _("Invitation accepted"),
{ {
@ -432,7 +401,7 @@ def assign_backups():
services = Service.query.filter(Service.is_backup == True).join(Job).filter(Job.start <= start).all() services = Service.query.filter(Service.is_backup == True).join(Job).filter(Job.start <= start).all()
for service in services: for service in services:
if service.job_.start <= now or service.job_.is_full(): if service.job_.start <= now or service.job_.is_full():
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(
service.user_, service.user_,
"Your backup assignment was cancelled.", "Your backup assignment was cancelled.",
{"event_id": service.job_.event_id_}, {"event_id": service.job_.event_id_},
@ -442,7 +411,7 @@ def assign_backups():
else: else:
service.is_backup = False service.is_backup = False
logger.debug(f"Service not full, assigning backup. {service.serialize()}") logger.debug(f"Service not full, assigning backup. {service.serialize()}")
EventPlugin.getPlugin().notify( EventPlugin.plugin.notify(
service.user_, service.user_,
"Your backup assignment was accepted.", "Your backup assignment was accepted.",
{"event_id": service.job_.event_id_}, {"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 __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime from datetime import datetime
from typing import Optional, Union, List from typing import Optional, Union
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
@ -17,7 +17,6 @@ _table_prefix_ = "events_"
class EventType(db.Model, ModelSerializeMixin): class EventType(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "event_type" __tablename__ = _table_prefix_ + "event_type"
id: int = db.Column(Serial, primary_key=True) id: int = db.Column(Serial, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=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): class Service(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "service" __tablename__ = _table_prefix_ + "service"
userid: str = "" userid: str = ""
is_backup: bool = db.Column(db.Boolean, default=False) is_backup: bool = db.Column(db.Boolean, default=False)
@ -59,7 +57,6 @@ class Service(db.Model, ModelSerializeMixin):
class Job(db.Model, ModelSerializeMixin): class Job(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "job" __tablename__ = _table_prefix_ + "job"
id: int = db.Column(Serial, primary_key=True) 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)) 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) 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) 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" "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) 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): class Event(db.Model, ModelSerializeMixin):
"""Model for an Event""" """Model for an Event"""
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "event" __tablename__ = _table_prefix_ + "event"
id: int = db.Column(Serial, primary_key=True) id: int = db.Column(Serial, primary_key=True)
start: datetime = db.Column(UtcDateTime, nullable=False) start: datetime = db.Column(UtcDateTime, nullable=False)
@ -97,7 +93,7 @@ class Event(db.Model, ModelSerializeMixin):
description: Optional[str] = db.Column(db.String(512)) description: Optional[str] = db.Column(db.String(512))
type: Union[EventType, int] = db.relationship("EventType") type: Union[EventType, int] = db.relationship("EventType")
is_template: bool = db.Column(db.Boolean, default=False) is_template: bool = db.Column(db.Boolean, default=False)
jobs: List[Job] = db.relationship( jobs: list[Job] = db.relationship(
"Job", "Job",
back_populates="event_", back_populates="event_",
cascade="all,delete,delete-orphan", cascade="all,delete,delete-orphan",
@ -113,7 +109,6 @@ class Event(db.Model, ModelSerializeMixin):
class Invitation(db.Model, ModelSerializeMixin): class Invitation(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = _table_prefix_ + "invitation" __tablename__ = _table_prefix_ + "invitation"
id: int = db.Column(Serial, primary_key=True) id: int = db.Column(Serial, primary_key=True)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,8 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-card>
<q-card-section>
<q-table :title="title" :rows="rows" row-key="id" :columns="columns"> <q-table :title="title" :rows="rows" row-key="id" :columns="columns">
<template #top-right> <template #top-right>
<q-input <q-input
@ -35,12 +37,11 @@
v-model="actualType.name" v-model="actualType.name"
:rules="rules" :rules="rules"
dense dense
filled
placeholder="Neuer Typ" placeholder="Neuer Typ"
> >
<template #after <slot name="after"
><q-btn color="primary" icon="mdi-plus" title="Hinzufügen" round @click="addType" ><q-btn color="primary" icon="mdi-plus" title="Hinzufügen" @click="addType"
/></template> /></slot>
</q-input> </q-input>
</template> </template>
<template #body-cell-actions="props"> <template #body-cell-actions="props">
@ -50,6 +51,8 @@
</q-td> </q-td>
</template> </template>
</q-table> </q-table>
</q-card-section>
</q-card>
</div> </div>
</template> </template>

View File

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

View File

@ -16,8 +16,8 @@
</div> </div>
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- <div class="q-pa-md"> --> <div class="q-pa-md">
<!-- <q-card style="height: 70vh; max-width: 1800px" class="q-pa-md"> --> <q-card style="height: 70vh; max-width: 1800px" class="q-pa-md">
<div ref="scrollDiv" class="scroll" style="height: 100%"> <div ref="scrollDiv" class="scroll" style="height: 100%">
<q-infinite-scroll :offset="250" @load="load"> <q-infinite-scroll :offset="250" @load="load">
<q-list> <q-list>
@ -26,10 +26,10 @@
</q-item> </q-item>
<template v-for="(events, index) in agendas" :key="index"> <template v-for="(events, index) in agendas" :key="index">
<q-separator /> <q-separator />
<q-item-label header>{{ asDate(index) }}</q-item-label> <q-item-label overline>{{ index }}</q-item-label>
<q-item v-for="(event, idx) in events" :key="idx"> <q-item v-for="(event, idx) in events" :key="idx"
<event-slot v-model="events[idx]" @edit-event="edit" @remove-event="remove" /> ><event-slot :model-value="event" />{{ idx }}</q-item
</q-item> >
</template> </template>
</q-list> </q-list>
<template #loading> <template #loading>
@ -39,8 +39,8 @@
</template> </template>
</q-infinite-scroll> </q-infinite-scroll>
</div> </div>
<!-- </q-card> --> </q-card>
<!-- </div> --> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -88,10 +88,6 @@ export default defineComponent({
function editDone(changed: boolean) { function editDone(changed: boolean) {
//if (changed) void loadAgendas(); //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; editor.value = undefined;
} }
@ -99,12 +95,7 @@ export default defineComponent({
const start = new Date(); const start = new Date();
if (index < 0) { if (index < 0) {
const { result } = await store.getEvents({ to: start, limit: 5, descending: true }); const { result } = await store.getEvents({ to: start, limit: 5, descending: true });
//events.value.unshift(...result); 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;
}
if (done) done(false); if (done) done(false);
} else { } else {
const len = events.value.length; const len = events.value.length;
@ -113,12 +104,7 @@ export default defineComponent({
offset: (index - 1) * 10, offset: (index - 1) * 10,
limit: 10, limit: 10,
}); });
for (const event of result) { if (len == events.value.push(...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 (done) return done(true); if (done) return done(true);
} else if (done) done(false); } else if (done) done(false);
} }
@ -131,11 +117,17 @@ export default defineComponent({
async function remove(id: number) { async function remove(id: number) {
if (await store.removeEvent(id)) { if (await store.removeEvent(id)) {
const idx = events.value.findIndex((event) => event.id === id); // Successfull removed
if (idx !== -1) { for (const idx in agendas.value) {
events.value.splice(idx, 1); 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
}
} }
function asMonth(value: string) { function asMonth(value: string) {
@ -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 { return {
agendas, agendas,
asYear, asYear,
@ -184,7 +167,6 @@ export default defineComponent({
editDone, editDone,
load, load,
remove, remove,
asDate,
}; };
}, },
}); });

View File

@ -54,7 +54,7 @@
</q-select> </q-select>
<div class="row col-12 justify-end"> <div class="row col-12 justify-end">
<q-btn <q-btn
v-if="!modelValue.locked && !isEnrolled && !isFull && canAssign" v-if="!modelValue.locked && !isEnrolled && !isFull"
flat flat
color="primary" color="primary"
label="Eintragen" label="Eintragen"
@ -205,8 +205,6 @@ export default defineComponent({
// Current user can assign other users // Current user can assign other users
const canAssignOther = computed(() => hasPermission(PERMISSIONS.ASSIGN_OTHER)); const canAssignOther = computed(() => hasPermission(PERMISSIONS.ASSIGN_OTHER));
const canAssign = computed(() => hasPermission(PERMISSIONS.ASSIGN));
// options shown in the select // options shown in the select
const options = ref([] as FG.Service[]); const options = ref([] as FG.Service[]);
@ -259,7 +257,6 @@ export default defineComponent({
unassign: (s?: FG.Service) => assign(Object.assign({}, s || service.value, { value: -1 })), unassign: (s?: FG.Service) => assign(Object.assign({}, s || service.value, { value: -1 })),
backup: (is_backup: boolean) => assign(Object.assign({}, service.value, { is_backup })), backup: (is_backup: boolean) => assign(Object.assign({}, service.value, { is_backup })),
canAssignOther, canAssignOther,
canAssign,
canInvite, canInvite,
filterUsers, filterUsers,
isBackup, isBackup,

View File

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

View File

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

View File

@ -43,33 +43,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import AgendaView from '../components/overview/AgendaView.vue'; import AgendaView from '../components/overview/AgendaView.vue';
import ListView from '../components/overview/ListView.vue'; import ListView from '../components/overview/ListView.vue';
import { useEventStore } from '../store';
export default defineComponent({ export default defineComponent({
name: 'EventOverview', name: 'EventOverview',
components: { AgendaView, ListView }, components: { AgendaView, ListView },
setup() { 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 quasar = useQuasar();
const tabs = computed(() => [ const tabs = computed(() => [
@ -86,10 +68,7 @@ export default defineComponent({
}, },
}); });
watch(tab, async (val) => { const tab = ref<string>('agendaView');
console.log(val);
await router.replace({ query: { ...route.query, q_tab: val } });
});
return { return {
showDrawer, showDrawer,

View File

@ -1,5 +1,7 @@
<template> <template>
<q-page padding> <q-page padding>
<q-card>
<q-card-section>
<q-table <q-table
v-model:pagination="pagination" v-model:pagination="pagination"
title="Dienstanfragen" title="Dienstanfragen"
@ -30,37 +32,18 @@
</q-icon> </q-icon>
</q-td> </q-td>
</template> </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-table>
</q-card-section>
</q-card>
</q-page> </q-page>
</template> </template>
<script lang="ts"> <script lang="ts">
import { formatStartEnd, useMainStore, useUserStore } from '@flaschengeist/api'; import { formatStartEnd, useMainStore, useUserStore } from '@flaschengeist/api';
import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue'; import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { QTableProps } from 'quasar'; import { QTableProps } from 'quasar';
import { Job } from '../store/models'; import { Job } from '../store/models';
import { useEventStore } from '../store'; import { useEventStore } from '../store';
import { EventNotification, InvitationData, InvitationResponseData } from '../events';
export default defineComponent({ export default defineComponent({
name: 'PageEventRequests', name: 'PageEventRequests',
@ -68,8 +51,6 @@ export default defineComponent({
const store = useEventStore(); const store = useEventStore();
const userStore = useUserStore(); const userStore = useUserStore();
const mainStore = useMainStore(); const mainStore = useMainStore();
const router = useRouter();
const route = useRoute();
interface RowData extends FG.Invitation { interface RowData extends FG.Invitation {
inviter: FG.User; inviter: FG.User;
@ -96,9 +77,6 @@ export default defineComponent({
? store.invitations ? store.invitations
: store.invitations.filter((i) => i.inviter_id !== mainStore.currentUser.userid) : 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); const showSent = ref(false);
async function fillRows(data: FG.Invitation[]) { async function fillRows(data: FG.Invitation[]) {
@ -158,12 +136,7 @@ export default defineComponent({
.finally(() => (loading.value = false)); .finally(() => (loading.value = false));
}; };
onBeforeMount(async () => { onBeforeMount(() => {
if (route.query.sent === 'true') {
showSent.value = true;
}
void Promise.allSettled([ void Promise.allSettled([
userStore.getUsers(), userStore.getUsers(),
store.getInvitations(), store.getInvitations(),
@ -173,13 +146,10 @@ export default defineComponent({
); );
}); });
watch(showSent, async () => { watch(showSent, () => {
onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] }); onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] });
await router.replace({ query: { sent: showSent.value ? 'true' : 'false' } });
}); });
function getType(row: RowData) { function getType(row: RowData) {
var idx = row.transferee === undefined ? 0 : 1; var idx = row.transferee === undefined ? 0 : 1;
if (row.inviter.userid === mainStore.currentUser.userid) idx += 2; if (row.inviter.userid === mainStore.currentUser.userid) idx += 2;
@ -248,57 +218,10 @@ export default defineComponent({
align: 'right', align: 'right',
name: 'actions', name: 'actions',
classes: dimmed, classes: dimmed,
field: (row: RowData) => { field: (row: RowData) => ({
const sender = row.inviter_id === mainStore.currentUser.userid; job: row.job_id,
let actions = []; sender: row.inviter_id === mainStore.currentUser.userid,
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;
},
}, },
]; ];

View File

@ -1,7 +1,6 @@
import { api, isAxiosError } from '@flaschengeist/api'; import { api, isAxiosError } from '@flaschengeist/api';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { EditableEvent } from './models'; import { EditableEvent } from './models';
import { Notify } from 'quasar';
/** /**
* Convert JSON decoded Job to real job (fix Date object) * Convert JSON decoded Job to real job (fix Date object)
@ -147,16 +146,19 @@ export const useEventStore = defineStore({
}, },
async addEvent(event: EditableEvent) { async addEvent(event: EditableEvent) {
console.log('addEvent', event);
if (event?.id === undefined) { if (event?.id === undefined) {
const { data } = await api.post<FG.Event>('/events', event); const { data } = await api.post<FG.Event>('/events', event);
if (data.is_template) this.templates.push(data); if (data.is_template) this.templates.push(data);
fixEvent(data);
return data; return data;
} else { } else {
if (typeof event.type === 'object') event.type = event.type.id; 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); if (data.is_template) this.templates.push(data);
fixEvent(data);
return data; return data;
} }
}, },
@ -203,47 +205,13 @@ export const useEventStore = defineStore({
}, },
async rejectInvitation(invite: FG.Invitation | number) { async rejectInvitation(invite: FG.Invitation | number) {
try { return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
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();
}
}, },
async acceptInvitation(invite: FG.Invitation | number) { async acceptInvitation(invite: FG.Invitation | number) {
try { return api.put(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`, {
await api.put(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`, {
accept: true, 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();
}
}, },
}, },
}); });
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, milliseconds: 0,
}); });
else this.start = new Date(); // <-- make TS happy "no initalizer" 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); 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", "target": "esnext",
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src/", "baseUrl": "./src/",