Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Ahmed Abbas <ah.abbas333@gmail.com>
Ahmad Bustany <ahmadbustany06@gmail.com>
Omar Goher <omargoher59@gmail.com>
Rawan Essam <rawanessamwork@gmail.com>
Huda Amr <hudaamr2006@gmail.com>
109 changes: 109 additions & 0 deletions doc/manual/en/26-availability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
\newpage

# Availability

The **Availability** feature lets users indicate when they are unavailable, such as when they are out of the office. This information helps support teams understand when a reply should not be expected and provides additional context when working with tickets.


## Purpose

Availability provides a clear way to record periods when a user or an entire company is unavailable.

This makes it possible for billetsys to represent absence and company-wide closures without relying only on ticket status or manual communication.

## Personal availability

Users can create personal availability periods to indicate when they are not available.

A personal availability entry includes:

* Start date
* End date
* Reason

The start and end dates define the period during which the user is unavailable.

## Creating availability

To create a personal availability period, open the **Availability** page and add a new entry.

Select the start and end dates and optionally provide a reason for the absence.

The reason helps other users understand the context of the unavailable period.

## Updating and deleting availability

Users can update or delete their own availability entries.

Updating an entry changes the dates or reason associated with that period.

Deleting an entry removes the personal availability period from the calendar and from ticket-related availability information.

## Company-wide availability

Company-wide availability is used for periods when an entire company is unavailable.

This can be useful for:

* Company holidays
* Office closures
* Organization-wide breaks
* Other periods when the company should not be expected to respond

Company-wide availability is associated with the company rather than an individual user.

## Managing company availability

Company-wide availability can only be managed by users with the required permissions.

Support and Superuser users can manage company-wide availability for their own company, while users without the required permissions cannot create or modify these entries.

Company isolation is enforced so that company availability cannot be managed across different companies.

## Viewing other users' availability

Availability visibility depends on the user's role.

Regular users and TAMs can see their own availability together with company-wide availability.

Support and Superuser users can also view the availability of any other user.

On the Availability page, Support and Superuser users can search for a user and view that user's availability periods. When viewing another user's availability, the entries are shown read-only.

Users cannot manage another user's availability.

## Availability in tickets

Availability is also used as context when working with tickets.

When a ticket is associated with an unavailable user or an unavailable company, billetsys can display an availability warning to help support teams understand that a response may be delayed.

The warning provides additional context without changing the ticket itself.

## Date rules

Availability uses calendar dates rather than specific times.

The end date must not be before the start date.

A single-day availability period is valid when the start date and end date are the same.

Personal availability periods cannot overlap with another personal availability period for the same user.

## Reason

The reason is optional and can be used to explain why the user or company is unavailable.

The reason is limited to **255 characters**.

## Operational value

Availability gives support teams additional context when deciding how to handle tickets and when a response should be expected.

Together with ticket information, it helps make periods of absence and company-wide closures visible without requiring users to communicate the same information manually.

## Keyboard shortcuts

Like other list and form views in the application, the Availability page supports the application's standard keyboard navigation shortcuts.

See the **Navigation** chapter for more details.
2 changes: 1 addition & 1 deletion doc/manual/en/97-acknowledgement.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Ahmed Abbas <ah.abbas333@gmail.com>
Ahmad Bustany <ahmadbustany06@gmail.com>
Omar Goher <omargoher59@gmail.com>
Rawan Essam <rawanessamwork@gmail.com>

Huda Amr <hudaamr2006@gmail.com>
```

## Committers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Eclipse Public License - v 2.0
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*/

package ai.mnemosyne_systems.model;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;

import java.time.LocalDate;

@Entity
@Table(name = "user_availability")
public class UserAvailability extends PanacheEntityBase {

public static final String SCOPE_PERSONAL = "PERSONAL";
public static final String SCOPE_COMPANY = "COMPANY";

@Id
@SequenceGenerator(name = "user_availability_seq", sequenceName = "user_availability_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_availability_seq")
public Long id;

@ManyToOne
@JoinColumn(name = "user_id")
public User user;

@ManyToOne
@JoinColumn(name = "company_id")
public Company company;

@Column(name = "start_date", nullable = false)
public LocalDate startDate;

@Column(name = "end_date", nullable = false)
public LocalDate endDate;

@Column(nullable = false)
public String scope;

@Column
public String reason;
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,22 @@ private List<NavLink> navigation(User user) {
}
if (AuthHelper.isSupport(user)) {
return List.of(new NavLink("Tickets", "/support/tickets"), new NavLink("Articles", "/articles"),
new NavLink("Users", "/support/users"));
new NavLink("Users", "/support/users"), new NavLink("Availability", "/availability"));
}
if (user != null && User.TYPE_USER.equalsIgnoreCase(user.type)) {
return List.of(new NavLink("Tickets", "/user/tickets"), new NavLink("Articles", "/articles"),
new NavLink("External", "/user/externals"), new NavLink("Profile", "/profile"));
new NavLink("External", "/user/externals"), new NavLink("Availability", "/availability"),
new NavLink("Profile", "/profile"));
}
if (AuthHelper.isSuperuser(user)) {
return List.of(new NavLink("Tickets", "/superuser/tickets"), new NavLink("Users", "/superuser/users"),
new NavLink("Reports", "/reports"), new NavLink("Articles", "/articles"),
new NavLink("Profile", "/profile"));
new NavLink("Availability", "/availability"), new NavLink("Profile", "/profile"));
}
if (AuthHelper.isTam(user)) {
return List.of(new NavLink("Tickets", "/user/tickets"), new NavLink("Reports", "/reports"),
new NavLink("Articles", "/articles"), new NavLink("Users", "/tam/users"),
new NavLink("Profile", "/profile"));
new NavLink("Availability", "/availability"), new NavLink("Profile", "/profile"));
}
return List.of(new NavLink("Tickets", "/user/tickets"), new NavLink("Articles", "/articles"),
new NavLink("Profile", "/profile"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import ai.mnemosyne_systems.service.DirectoryService;
import ai.mnemosyne_systems.service.EventService;
import ai.mnemosyne_systems.service.TicketBootstrapService;
import ai.mnemosyne_systems.service.UserAvailabilityService;
import ai.mnemosyne_systems.util.AuthHelper;
import ai.mnemosyne_systems.util.AuthHelper;
import ai.mnemosyne_systems.util.CurrentUser;
import jakarta.annotation.security.RolesAllowed;
Expand Down Expand Up @@ -64,6 +66,9 @@ public class SuperuserTicketApiResource {
@Inject
DirectoryService directoryService;

@Inject
UserAvailabilityService userAvailabilityService;

@GET
@Transactional
public SupportTicketApiResource.SupportTicketListResponse list(
Expand Down Expand Up @@ -205,6 +210,7 @@ public UserTicketApiResource.RoleTicketDetailResponse detail(@PathParam("id") Lo
java.util.Map<Long, Ticket> ticketCache = crossReferenceService
.preloadReferencedTickets(messages.stream().map(m -> m.body).toList());
List<Event> rawEvents = eventService.getAllChangesToEntity(ticket.id);
List<String> availabilityWarnings = userAvailabilityService.getWarningsForTicket(ticket);
List<SupportTicketApiResource.EventEntry> eventEntries = rawEvents.stream().map(this::toEventEntry).toList();

return new UserTicketApiResource.RoleTicketDetailResponse(ticket.id, ticket.name, ticket.displayTitle(),
Expand All @@ -231,7 +237,7 @@ public UserTicketApiResource.RoleTicketDetailResponse detail(@PathParam("id") Lo
"/superuser/tickets/" + ticket.id + "/messages", "/tickets/export/" + ticket.id,
List.of("Open", "Assigned", "In Progress", "Resolved", "Closed"), false, false, false, true, true,
externalUsers.stream().map(this::toUserReference).toList(),
userUsers.stream().map(this::toUserReference).toList(), eventEntries);
userUsers.stream().map(this::toUserReference).toList(), eventEntries, availabilityWarnings);
}

@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import ai.mnemosyne_systems.model.event.Event;
import ai.mnemosyne_systems.service.EventService;
import ai.mnemosyne_systems.service.TicketBootstrapService;
import ai.mnemosyne_systems.service.UserAvailabilityService;
import ai.mnemosyne_systems.util.CurrentUser;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
Expand Down Expand Up @@ -60,6 +61,9 @@ public class SupportTicketApiResource {
@Inject
TicketBootstrapService ticketBootstrapService;

@Inject
UserAvailabilityService userAvailabilityService;

@GET
@Transactional
public SupportTicketListResponse list(@QueryParam("view") @DefaultValue("assigned") String view,
Expand Down Expand Up @@ -188,6 +192,7 @@ public SupportTicketDetailResponse detail(@PathParam("id") Long id) {
List<MessageEntry> messageEntries = messages.stream().map(m -> toMessageEntry(m, ticketCache)).toList();
List<Event> rawEvents = eventService.getAllChangesToEntity(ticket.id);
List<EventEntry> eventEntries = rawEvents.stream().map(this::toEventEntry).toList();
List<String> availabilityWarnings = userAvailabilityService.getWarningsForTicket(ticket);
return new SupportTicketDetailResponse(ticket.id, ticket.name, ticket.displayTitle(), displayStatus,
counts.assignedCount(), counts.openCount(), ticket.company == null ? null : ticket.company.id,
ticket.company == null ? null : ticket.company.name,
Expand All @@ -210,7 +215,7 @@ public SupportTicketDetailResponse detail(@PathParam("id") Long id) {
SupportTicketViewSupport.isEntitlementExpired(ticket), "/support/tickets/" + ticket.id,
"/support/tickets/" + ticket.id + "/messages", "/tickets/export/" + ticket.id,
List.of("Open", "Assigned", "In Progress", "Waiting for External Feedback", "Resolved", "Closed"),
externalUsers.stream().map(this::toUserReference).toList(), eventEntries);
externalUsers.stream().map(this::toUserReference).toList(), eventEntries, availabilityWarnings);
}

@GET
Expand Down Expand Up @@ -447,6 +452,6 @@ public record SupportTicketDetailResponse(Long id, String name, String title, St
List<VersionOption> versions, List<CategoryOption> categories, List<UserReference> supportUsers,
List<UserReference> tamUsers, List<MessageEntry> messages, boolean ticketEntitlementExpired,
String actionPath, String messageActionPath, String exportPath, List<String> statusOptions,
List<UserReference> externalUsers, List<EventEntry> events) {
List<UserReference> externalUsers, List<EventEntry> events, List<String> availabilityWarnings) {
}
}
Loading
Loading