Compare commits

...

24 Commits

Author SHA1 Message Date
Tim Gröger c767d92442 add notification 2023-05-02 06:40:48 +02:00
Tim Gröger 8cd9182a8b remove invitation when reject or accept 2023-05-02 06:23:36 +02:00
Tim Gröger 46939d4b64 fix typing 2023-05-01 22:47:37 +02:00
Tim Gröger ae275aeabb remove unecessary notification in frontend 2023-05-01 22:46:16 +02:00
Tim Gröger c721f25104 delete unnecessary notifications 2023-05-01 22:06:24 +02:00
Tim Gröger 26235fef49 add actions on events-request-page 2023-05-01 11:00:24 +02:00
Tim Gröger b33d30fe40 black 2023-04-30 13:56:14 +02:00
Tim Gröger 28223d12b6 fix transferjob when accepted 2023-04-30 13:55:57 +02:00
Tim Gröger 0f0e6702e2 prettier 2023-04-30 13:55:17 +02:00
Tim Gröger b4c3cfa365 fix and add more notification 2023-04-30 13:55:00 +02:00
Tim Gröger 941841b1bb change id to get notifications 2023-04-30 09:49:51 +02:00
Tim Gröger 31c6410eba change view of listview 2023-04-28 20:45:47 +02:00
Tim Gröger 144bc1d58e load jobTypes before used 2023-04-28 20:45:30 +02:00
Tim Gröger a88e3a0160 remove card from EventRequests 2023-04-28 20:05:15 +02:00
Tim Gröger 2ddb89f89c add IDE options in gitignore 2023-04-28 19:56:58 +02:00
Tim Gröger 3ba7e5d366 add end time for job, delete empty jobtype 2023-04-28 19:55:12 +02:00
Ferdinand Thiessen e1ad8f0f11 feat(ui): Implemented job invitations / transfer page 2023-04-28 13:52:32 +02:00
Tim Gröger eb0e54714b fix add Jobs backend 2023-04-28 13:51:41 +02:00
Tim Gröger 994f65c38b fix add jobs 2023-04-28 13:51:11 +02:00
Tim Gröger b7741cfa37 fix adding, modify, remove jobs from event 2023-04-26 11:28:16 +02:00
Tim Gröger 9359db3881 fix add jobs 2023-04-24 21:14:19 +02:00
Tim Gröger f54911ed48 add alembic migration; delete unneccessary card layout 2023-04-10 14:55:17 +02:00
Tim Gröger 7923f4889c fix depracation warnings (sqlalchemy 2.0), fix installable plugin 2023-04-09 23:41:10 +02:00
Ferdinand Thiessen 75fa825962 fix(backend): Provide version, fix metadata
continuous-integration/woodpecker the build was successful Details
2021-12-23 03:24:12 +01:00
16 changed files with 749 additions and 168 deletions

6
.gitignore vendored
View File

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

View File

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

View File

@ -1,7 +1,8 @@
from datetime import datetime, timedelta, timezone
from enum import IntEnum
from typing import Optional, Tuple
from flaschengeist.models import UtcDateTime
from typing import Optional, Tuple, Union
from flaschengeist.controller import userController
from flaschengeist.models import Notification, UtcDateTime
from flaschengeist.models.user import User
from werkzeug.exceptions import BadRequest, Conflict, NotFound
@ -16,6 +17,7 @@ from flaschengeist.plugins.scheduler import scheduled
from . import EventPlugin
from .models import EventType, Event, Invitation, Job, JobType, Service
# STUB
def _(x):
return x
@ -26,8 +28,11 @@ class NotifyType(IntEnum):
INVITE = 0x01
TRANSFER = 0x02
# Invitation responsed 0x10..0x1F
INVITATION_ACCEPTED = 0x10
INVITATION_REJECTED = 0x11
INVITATION_ACCEPTED = 0x11
INVITATION_REJECTED = 0x12
# Information responses 0x20..0x2F
INFO_ACCEPTED = 0x21
INFO_REJECTED = 0x22
@before_delete_user
@ -43,8 +48,8 @@ def clear_services(user):
db.session.commit()
def update():
db.session.commit()
# def update():
# db.session.commit()
def get_event_types():
@ -234,10 +239,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 = Job.query.filter(Job.id == job_id)
query = db.select(Job).where(Job.id == job_id)
if event_id is not None:
query = query.filter(Job.event_id_ == event_id)
job = query.one_or_none()
query = query.where(Job.event_id_ == event_id)
job = db.session.execute(query).scalar_one_or_none()
if job is None:
raise NotFound
return job
@ -294,7 +299,7 @@ def delete_job(job: Job):
db.session.commit()
def assign_job(job: Job, user, value, is_backup=False):
def assign_job(job: Job, user, value, is_backup=False, notify=False):
assert value > 0
service = Service.query.get((job.id, user.id_))
if service:
@ -302,10 +307,17 @@ def assign_job(job: Job, user, value, is_backup=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_))
@ -319,17 +331,24 @@ def unassign_job(job: Job = None, user=None, service=None, notify=False):
db.session.delete(service)
db.session.commit()
if notify:
EventPlugin.plugin.notify(user, "Your assignmet was cancelled", {"event_id": event_id})
EventPlugin.getPlugin().notify(
user, f"Your assignmet was cancelled\n{_date}", {"type": NotifyType.INFO_REJECTED, "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.plugin.notify(invitee, _("Job invitation"), {"type": NotifyType.INVITE, "invitation": inv.id})
EventPlugin.getPlugin().notify(
invitee, _(f"Job invitation\n{_date}"), {"type": NotifyType.INVITE, "invitation": inv.id}
)
else:
EventPlugin.plugin.notify(invitee, _("Job transfer"), {"type": NotifyType.TRANSFER, "invitation": inv.id})
EventPlugin.getPlugin().notify(
invitee, _(f"Job transfer\n{_date}"), {"type": NotifyType.TRANSFER, "invitation": inv.id}
)
return inv
@ -346,9 +365,21 @@ 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):
@ -361,7 +392,7 @@ def respond_invitation(invite: Invitation, accepted=True):
raise Conflict
if not accepted:
EventPlugin.plugin.notify(
EventPlugin.getPlugin().notify(
inviter,
_("Invitation rejected"),
{
@ -375,12 +406,12 @@ def respond_invitation(invite: Invitation, accepted=True):
if invite.transferee_id is None:
assign_job(job, invite.invitee_, 1)
else:
service = filter(lambda s: s.userid == invite.transferee_id, job.services)
service = tuple(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.plugin.notify(
EventPlugin.getPlugin().notify(
inviter,
_("Invitation accepted"),
{
@ -401,7 +432,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.plugin.notify(
EventPlugin.getPlugin().notify(
service.user_,
"Your backup assignment was cancelled.",
{"event_id": service.job_.event_id_},
@ -411,7 +442,7 @@ def assign_backups():
else:
service.is_backup = False
logger.debug(f"Service not full, assigning backup. {service.serialize()}")
EventPlugin.plugin.notify(
EventPlugin.getPlugin().notify(
service.user_,
"Your backup assignment was accepted.",
{"event_id": service.job_.event_id_},

View File

@ -0,0 +1,105 @@
"""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
from typing import Optional, Union, List
from sqlalchemy import UniqueConstraint
@ -17,6 +17,7 @@ _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)
@ -34,6 +35,7 @@ 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)
@ -57,6 +59,7 @@ 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)
@ -65,7 +68,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)
@ -85,6 +88,7 @@ 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)
@ -93,7 +97,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",
@ -109,6 +113,7 @@ 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
from flask import request, jsonify, Blueprint
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
from flaschengeist.models.session import Session
@ -7,10 +7,14 @@ 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:
@ -20,19 +24,20 @@ def dict_get(self, key, default=None, type=None):
if type is not None:
try:
rv = type(rv)
except ValueError:
except (ValueError, TypeError):
rv = default
return rv
@EventPlugin.blueprint.route("/events/templates", methods=["GET"])
@blueprint.route("/events/templates", methods=["GET"])
@login_required()
def get_templates(current_session):
return jsonify(event_controller.get_templates())
@EventPlugin.blueprint.route("/events/event-types", methods=["GET"])
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["GET"])
@blueprint.route("/events/event-types", methods=["GET"])
@blueprint.route("/events/event-types/<int:identifier>", methods=["GET"])
@login_required()
def get_event_types(current_session, identifier=None):
"""Get EventType(s)
@ -54,7 +59,7 @@ def get_event_types(current_session, identifier=None):
return jsonify(result)
@EventPlugin.blueprint.route("/events/event-types", methods=["POST"])
@blueprint.route("/events/event-types", methods=["POST"])
@login_required(permission=permissions.EVENT_TYPE)
def new_event_type(current_session):
"""Create a new EventType
@ -76,7 +81,7 @@ def new_event_type(current_session):
return jsonify(event_type)
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
@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
@ -102,7 +107,7 @@ def modify_event_type(identifier, current_session):
return "", NO_CONTENT
@EventPlugin.blueprint.route("/events/job-types", methods=["GET"])
@blueprint.route("/events/job-types", methods=["GET"])
@login_required()
def get_job_types(current_session):
"""Get all JobTypes
@ -119,7 +124,7 @@ def get_job_types(current_session):
return jsonify(types)
@EventPlugin.blueprint.route("/events/job-types", methods=["POST"])
@blueprint.route("/events/job-types", methods=["POST"])
@login_required(permission=permissions.JOB_TYPE)
def new_job_type(current_session):
"""Create a new JobType
@ -141,7 +146,7 @@ def new_job_type(current_session):
return jsonify(jt)
@EventPlugin.blueprint.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
@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
@ -167,7 +172,7 @@ def modify_job_type(type_id, current_session):
return "", NO_CONTENT
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["GET"])
@blueprint.route("/events/<int:event_id>", methods=["GET"])
@login_required()
def get_event(event_id, current_session):
"""Get event by id
@ -188,7 +193,7 @@ def get_event(event_id, current_session):
return jsonify(event)
@EventPlugin.blueprint.route("/events", methods=["GET"])
@blueprint.route("/events", methods=["GET"])
@login_required()
def get_events(current_session):
count, result = event_controller.get_events(
@ -204,21 +209,39 @@ 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")
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),
)
except StopIteration:
raise BadRequest("Job not in event")
@EventPlugin.blueprint.route("/events", methods=["POST"])
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"])
@login_required(permission=permissions.CREATE)
def create_event(current_session):
"""Create an new event
@ -259,7 +282,7 @@ def create_event(current_session):
raise BadRequest("Invalid parameter")
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["PUT"])
@blueprint.route("/events/<int:event_id>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
def modify_event(event_id, current_session):
"""Modify an event
@ -277,6 +300,7 @@ 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)
@ -284,12 +308,15 @@ 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)
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["DELETE"])
@blueprint.route("/events/<int:event_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_event(event_id, current_session):
"""Delete an event
@ -307,7 +334,7 @@ def delete_event(event_id, current_session):
return "", NO_CONTENT
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs", methods=["POST"])
@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
@ -328,7 +355,7 @@ def add_job(event_id, current_session):
return jsonify(event)
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
@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
@ -348,7 +375,7 @@ def delete_job(event_id, job_id, current_session):
return no_content()
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
@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
@ -390,20 +417,20 @@ def update_job(event_id, job_id, current_session: Session):
return jsonify(job)
@EventPlugin.blueprint.route("/events/jobs", methods=["GET"])
@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})
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>", methods=["GET"])
@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))
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/assign", methods=["POST"])
@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
@ -431,7 +458,9 @@ 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))
event_controller.assign_job(
job, user, value, data.get("is_backup", False), notify=user != current_session.user_
)
else:
event_controller.unassign_job(job, user, notify=user != current_session.user_)
except (TypeError, KeyError, ValueError):
@ -439,7 +468,7 @@ def assign_job(job_id, current_session: Session):
return jsonify(job)
@EventPlugin.blueprint.route("/events/jobs/<int:job_id>/lock", methods=["POST"])
@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
@ -466,7 +495,7 @@ def lock_job(job_id, current_session: Session):
return no_content()
@EventPlugin.blueprint.route("/events/invitations", methods=["POST"])
@blueprint.route("/events/invitations", methods=["POST"])
@login_required()
def invite(current_session: Session):
"""Invite an user to a job or transfer job
@ -504,13 +533,13 @@ def invite(current_session: Session):
raise BadRequest
@EventPlugin.blueprint.route("/events/invitations", methods=["GET"])
@blueprint.route("/events/invitations", methods=["GET"])
@login_required()
def get_invitations(current_session: Session):
return jsonify(event_controller.get_invitations(current_session.user_))
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["GET"])
@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)
@ -519,7 +548,7 @@ def get_invitation(invitation_id: int, current_session: Session):
return jsonify(inv)
@EventPlugin.blueprint.route("/events/invitations/<int:invitation_id>", methods=["DELETE", "PUT"])
@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

@ -82,13 +82,14 @@
</div>
</div>
<template v-for="(job, index) in event.jobs" :key="index">
<!--:ref="active === index ? 'activeJob' : undefined"-->
<edit-job-slot
:ref="active === index ? 'activeJob' : undefined"
ref="activeJob"
v-model="event.jobs[index]"
:active="index === active"
class="q-mb-md"
@activate="activate(index)"
@remove-job="removeJob(index)"
@activate="activate(index)"
/>
</template>
</q-card-section>
@ -111,9 +112,9 @@ import { notEmpty } from '@flaschengeist/api';
import { IsoDateInput } from '@flaschengeist/api/components';
import { useEventStore } from '../../store';
import { emptyEvent, emptyJob, EditableEvent } from '../../store/models';
import { emptyEvent, Job, EditableEvent } from '../../store/models';
import { date, ModifyDateOptions } from 'quasar';
import { date, DateOptions } from 'quasar';
import { computed, defineComponent, PropType, ref, onBeforeMount, watch } from 'vue';
import EditJobSlot from './EditJobSlot.vue';
@ -142,7 +143,7 @@ export default defineComponent({
const store = useEventStore();
const active = ref(0);
const activeJob = ref<{ validate: () => Promise<boolean> }>();
const activeJob = ref<{ validate: () => Promise<boolean> }[]>([]);
const templates = computed(() => store.templates);
const template = ref<FG.Event>();
const event = ref<EditableEvent>(props.modelValue || emptyEvent());
@ -164,11 +165,12 @@ export default defineComponent({
);
function addJob() {
if (!activeJob.value) event.value.jobs.push(emptyJob());
else
void activeJob.value.validate().then((success) => {
if (!activeJob.value[active.value]) {
event.value.jobs.push(new Job());
} else
void activeJob.value[active.value].validate().then((success) => {
if (success) {
event.value.jobs.push(emptyJob());
event.value.jobs.push(new Job());
active.value = event.value.jobs.length - 1;
}
});
@ -203,14 +205,11 @@ export default defineComponent({
async function save(template = false) {
event.value.is_template = template;
try {
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: ModifyDateOptions = {};
const options: DateOptions = {};
switch (recurrenceRule.value.frequency) {
case 'daily':
options['days'] = 1 * recurrenceRule.value.interval;
@ -263,7 +262,7 @@ export default defineComponent({
!d || event.value.start <= d || 'Das Veranstaltungsende muss vor dem Beginn liegen';
function activate(idx: number) {
void activeJob.value?.validate().then((s) => {
void activeJob.value[active.value]?.validate().then((s) => {
if (s) active.value = idx;
});
}

View File

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

View File

@ -28,31 +28,21 @@
</q-card>
</q-dialog>
<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>
<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>
</div>
</template>

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 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 class="q-pa-md"> -->
<!-- <q-card style="height: 70vh; max-width: 1800px" class="q-pa-md"> -->
<div ref="scrollDiv" class="scroll" style="height: 100%">
<q-infinite-scroll :offset="250" @load="load">
<q-list>
<q-item id="bbb">
<q-btn label="Ältere Veranstaltungen laden" @click="load(-1)" />
</q-item>
<template v-for="(events, index) in agendas" :key="index">
<q-separator />
<q-item-label header>{{ asDate(index) }}</q-item-label>
<q-item v-for="(event, idx) in events" :key="idx">
<event-slot :model-value="event" />
</q-item>
</template>
</q-list>
<template #loading>
<div class="row justify-center q-my-md">
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</div>
<!-- </q-card> -->
<!-- </div> -->
</template>
<script lang="ts">
@ -157,6 +157,15 @@ 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,
@ -167,6 +176,7 @@ export default defineComponent({
editDone,
load,
remove,
asDate,
};
},
});

View File

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

View File

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

View File

@ -1,8 +1,312 @@
<template>
<q-page padding>
<q-card>
<q-card-section class="row"> </q-card-section>
<q-card-section> </q-card-section>
</q-card>
<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-page>
</template>
<script lang="ts">
import { formatStartEnd, useMainStore, useUserStore } from '@flaschengeist/api';
import { computed, defineComponent, ref, onBeforeMount, watch } from 'vue';
import { QTableProps } from 'quasar';
import { Job } from '../store/models';
import { useEventStore } from '../store';
import { EventNotification, InvitationData, InvitationResponseData } from '../events';
export default defineComponent({
name: 'PageEventRequests',
setup() {
const store = useEventStore();
const userStore = useUserStore();
const mainStore = useMainStore();
interface RowData extends FG.Invitation {
inviter: FG.User;
invitee: FG.User;
transferee?: FG.User;
job: Job;
}
// Generated data used for the table
const rows = ref([] as RowData[]);
// Loading state for data generation (getting Job information)
const loading = ref(false);
// Which "page" of invitations to show (we load all, but do not fetch all jobs)
const pagination = ref({
sortBy: 'desc',
descending: false,
page: 1,
rowsPerPage: 3,
rowsNumber: 4,
});
// Real invitations
const invitations = computed(() =>
showSent.value
? store.invitations
: store.invitations.filter((i) => i.inviter_id !== mainStore.currentUser.userid)
);
const all_notifications = computed<EventNotification[]>(() => {
return mainStore.notifications.filter((n) => n.plugin === 'events') as EventNotification[];
});
const showSent = ref(false);
async function fillRows(data: FG.Invitation[]) {
const res = [] as RowData[];
for (let i = 0; i < data.length; ++i) {
res.push(
Object.assign({}, data[i], {
inviter: <FG.User>await userStore.getUser(data[i].inviter_id),
invitee: <FG.User>await userStore.getUser(data[i].invitee_id),
transferee: data[i].transferee_id
? await userStore.getUser(<string>data[i].transferee_id)
: undefined,
job: new Job(await store.getJob(data[i].job_id)),
})
);
}
rows.value = res;
}
type onRequestType = QTableProps['onRequest'];
const onRequest: onRequestType = (requestProp) => {
const { page, rowsPerPage, sortBy, descending } = requestProp.pagination;
loading.value = true;
// Number of total invitations
pagination.value.rowsNumber = invitations.value.length;
// calculate starting row of data
const startRow = (page - 1) * rowsPerPage;
// get all rows if "All" (0) is selected
const fetchCount =
rowsPerPage === 0
? pagination.value.rowsNumber
: Math.min(pagination.value.rowsNumber - startRow, rowsPerPage);
// copy array, as sort is in-place
function sorting<T = any>(key: string | keyof T, descending = true) {
return (a: T, b: T) => {
const v1 = a[key as keyof T];
if (v1 === undefined) return descending ? -1 : 1;
const v2 = b[key as keyof T];
if (v2 === undefined) return descending ? 1 : -1;
return (v1 < v2 ? -1 : 1) * (descending ? -1 : 1);
};
}
// Set table data
fillRows(
[...invitations.value]
.sort(sorting(sortBy, descending))
.slice(startRow, startRow + fetchCount)
)
.then(() => {
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
})
.finally(() => (loading.value = false));
};
onBeforeMount(() => {
void Promise.allSettled([
userStore.getUsers(),
store.getInvitations(),
store.getJobTypes(),
]).then(() =>
onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] })
);
});
watch(showSent, () => {
onRequest({ pagination: pagination.value, filter: () => [], getCellValue: () => [] });
});
function getType(row: RowData) {
var idx = row.transferee === undefined ? 0 : 1;
if (row.inviter.userid === mainStore.currentUser.userid) idx += 2;
return idx;
}
const dimmed = (row: RowData) => (getType(row) >= types.length / 2 ? 'dimmed' : undefined);
const columns = [
{
label: 'Type',
name: 'type',
align: 'left',
field: getType,
sortable: true,
classes: dimmed,
},
{
label: 'Dienstart',
align: 'left',
name: 'job_type',
sortable: true,
classes: dimmed,
field: (row: RowData) =>
store.jobTypes.find((t) => t.id == row.job.type)?.name || 'Unbekannt',
},
{
label: 'Wann',
align: 'center',
sortable: true,
name: 'job_start',
classes: dimmed,
field: (row: RowData) => formatStartEnd(row.job.start, row.job.end) + ' Uhr',
},
{
label: 'Von / Mit',
name: 'inviter',
align: 'center',
classes: dimmed,
field: (row: RowData) => {
const sender =
row.transferee_id && row.transferee_id !== row.inviter_id
? row.inviter.display_name
: undefined;
if (row.invitee_id === mainStore.currentUser.userid) {
return {
with: row.transferee ? row.transferee.display_name : row.inviter.display_name,
sender,
};
}
if (row.transferee_id === mainStore.currentUser.userid) {
return {
with: row.invitee.display_name,
sender,
};
}
return {
with: !row.transferee
? row.invitee.display_name
: `${row.transferee.display_name} <-> ${row.invitee.display_name}`,
};
},
},
{
label: 'Aktionen',
align: 'right',
name: 'actions',
classes: dimmed,
field: (row: RowData) => {
const sender = row.inviter_id === mainStore.currentUser.userid;
let actions = [];
const reject = {
icon: 'mdi-delete',
tooltip: 'Einladung löschen',
color: 'negative',
onClick: () => {
void store.rejectInvitation(row.id).then(() => {
onRequest({
pagination: pagination.value,
filter: () => [],
getCellValue: () => [],
});
const notification = all_notifications.value.find(
(n) => (<InvitationData>n.data).invitation === row.id
);
if (notification !== undefined) {
void mainStore.removeNotification(notification.id);
}
});
},
};
const accept = {
icon: 'mdi-check',
tooltip: 'Einladung annehmen',
color: 'primary',
onClick: () => {
void store.acceptInvitation(row.id).then(() => {
onRequest({
pagination: pagination.value,
filter: () => [],
getCellValue: () => [],
});
const notification = all_notifications.value.find(
(n) => (<InvitationData>n.data).invitation === row.id
);
if (notification !== undefined) {
void mainStore.removeNotification(notification.id);
}
});
},
};
if (sender) {
actions.push(reject);
} else if (row.invitee_id === mainStore.currentUser.userid) {
actions.push(accept);
actions.push({ ...reject, icon: 'mdi-close' });
}
return actions;
},
},
];
const types = [
{ icon: 'mdi-calendar', tooltip: 'Einladung' },
{ icon: 'mdi-calendar-sync', tooltip: 'Tauschanfrage' },
{ icon: 'mdi-calendar-outline', tooltip: 'Einladung (von dir)' },
{ icon: 'mdi-calendar-sync-outline', tooltip: 'Tauschanfrage (von dir)' },
];
return {
columns,
loading,
onRequest,
pagination,
rows,
showSent,
types,
};
},
});
</script>

View File

@ -1,6 +1,7 @@
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)
@ -146,17 +147,16 @@ 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);
return data;
} else {
if (typeof event.type === 'object') event.type = event.type.id;
const { data } = await api.put<FG.Event>(
`/events/${event.id}`,
Object.assign(event, { jobs: undefined })
// Object.assign(event, { jobs: undefined })
event
);
if (data.is_template) this.templates.push(data);
return data;
@ -205,13 +205,47 @@ export const useEventStore = defineStore({
},
async rejectInvitation(invite: FG.Invitation | number) {
return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
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();
}
},
async acceptInvitation(invite: FG.Invitation | number) {
return api.put(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`, {
accept: true,
});
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();
}
},
},
});
function notify_failure() {
Notify.create({
message: 'Es ist ein Fehler aufgetreten.',
color: 'negative',
group: false,
timeout: 10000,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}
function notify_success(msg: string) {
Notify.create({
message: msg,
color: 'positive',
group: false,
timeout: 5000,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}

View File

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

View File

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