Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,78 +13,70 @@
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.Instant;
import java.util.List;

/**
* Primary ticket entity for PSA/Ticketing functionality.
* Owns all metadata; Dialog becomes a simplified child.
*
* Tags are stored via unified TagAssignment (shared tag system).
* Attachments are stored in TicketAttachment collection.
*/

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "tickets")
@CompoundIndexes({
@CompoundIndex(name = "status_created", def = "{'status': 1, 'createdAt': -1}"),
@CompoundIndex(name = "assignee_status", def = "{'assignedTo': 1, 'status': 1}"),
@CompoundIndex(name = "organization_status", def = "{'organizationId': 1, 'status': 1}"),
@CompoundIndex(name = "device_status", def = "{'deviceId': 1, 'status': 1}")
@CompoundIndex(name = "tenant_ticket_number_unique",
def = "{'tenantId': 1, 'ticketNumber': 1}", unique = true),
@CompoundIndex(name = "tenant_status_kind_created",
def = "{'tenantId': 1, 'statusKind': 1, 'createdAt': -1}"),
@CompoundIndex(name = "tenant_assignee_status_kind",
def = "{'tenantId': 1, 'assignedTo': 1, 'statusKind': 1}"),
@CompoundIndex(name = "tenant_org_status_kind",
def = "{'tenantId': 1, 'organizationId': 1, 'statusKind': 1}"),
@CompoundIndex(name = "tenant_device_status_kind",
def = "{'tenantId': 1, 'deviceId': 1, 'statusKind': 1}"),
@CompoundIndex(name = "tenant_status_id",
def = "{'tenantId': 1, 'statusId': 1}")
})
public class Ticket {

@Id
private String id;

/**
* Human-readable ticket number (e.g., 1001, 1002).
* Auto-incremented per tenant.
*/
@Indexed(unique = true)
@Indexed
private String tenantId;

private Integer ticketNumber;

private String title;

/**
* Rich text description (HTML from editor).
*/
private String description;

@Indexed
private TicketStatus status;
private String statusId;

@Indexed
private TicketStatusKind statusKind;

private boolean aiDisabled;

@Indexed
private TicketCreationSource creationSource;

private TicketOwner owner;

/**
* Device (Machine) that this ticket is about.
*/
@Indexed
private String deviceId;

private String deviceHostname;

/**
* Organization the device belongs to.
*/
@Indexed
private String organizationId;

private String organizationName;

/**
* Reporter - the end user (future: from Authentic).
* For now, may be null until Authentic integration.
*/
private String reporterId;

private String reporterName;

/**
* Assigned technician (User from users collection).
*/
@Indexed
private String assignedTo;

private String assignedName;

@Indexed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.openframe.data.document.ticket;

/**
* How the ticket was created.
* Auto-populated by system based on creation flow.
*/
public enum TicketCreationSource {
FAE_FORM,
FAE_DIALOG,
ADMIN_DASHBOARD
ADMIN_DASHBOARD,
INBOUND_EMAIL
}
Original file line number Diff line number Diff line change
@@ -1,29 +0,0 @@
package com.openframe.data.document.ticket;

import java.util.Set;

/**
* Status of a ticket (matches Figma flow).
* Separate from DialogStatus for feature flag isolation.
*/
public enum TicketStatus {
ACTIVE,
TECH_REQUIRED,
ON_HOLD,
RESOLVED,
ARCHIVED;

public boolean canTransitionTo(TicketStatus target) {
return getAllowedTransitions().contains(target);
}

public Set<TicketStatus> getAllowedTransitions() {
return switch (this) {
case ACTIVE -> Set.of(TECH_REQUIRED, ON_HOLD, RESOLVED);
case TECH_REQUIRED -> Set.of(ACTIVE, ON_HOLD, RESOLVED);
case ON_HOLD -> Set.of(ACTIVE, TECH_REQUIRED, RESOLVED);
case RESOLVED -> Set.of(ACTIVE, ARCHIVED);
case ARCHIVED -> Set.of(RESOLVED);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.openframe.data.document.ticket;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.Instant;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "ticket_statuses")
@CompoundIndexes({
@CompoundIndex(name = "tenant_name_unique",
def = "{'tenantId': 1, 'name': 1}", unique = true),
@CompoundIndex(name = "tenant_kind_unique",
def = "{'tenantId': 1, 'kind': 1}", unique = true,
partialFilter = "{'kind': {$in: ['AI_ASSISTANCE', 'TECH_REQUIRED', 'RESOLVED', 'ARCHIVED']}}"),
@CompoundIndex(name = "tenant_position",
def = "{'tenantId': 1, 'position': 1}")
})
public class TicketStatusDefinition {

@Id
private String id;

@Indexed
private String tenantId;

private TicketStatusKind kind;

private String name;

private String color;

private Integer position;

@CreatedDate
private Instant createdAt;

@LastModifiedDate
private Instant updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.openframe.data.document.ticket;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.Instant;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "ticket_status_events")
@CompoundIndexes({
@CompoundIndex(name = "tenant_ticket_time",
def = "{'tenantId': 1, 'ticketId': 1, 'occurredAt': -1}")
})
public class TicketStatusEvent {

@Id
private String id;

@Indexed
private String tenantId;

private String ticketId;

private String fromStatusId;

private TicketStatusKind fromKind;

private String toStatusId;

private TicketStatusKind toKind;

private String actorType;

private String actorId;

private String reason;

private Instant occurredAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.openframe.data.document.ticket;

public enum TicketStatusKind {
AI_ASSISTANCE,
TECH_REQUIRED,
RESOLVED,
ARCHIVED,
CUSTOM;

public boolean isSystem() {
return this != CUSTOM;
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package com.openframe.data.document.ticket.filter;

import com.openframe.data.document.ticket.TicketStatus;
import com.openframe.data.document.ticket.TicketStatusKind;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
* Filter object for ticket queries.
* Follows the same pattern as MachineQueryFilter.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TicketQueryFilter {
private List<TicketStatus> statuses;
private List<String> statusIds;
private List<TicketStatusKind> statusKinds;
private List<String> organizationIds;
private List<String> assigneeIds;
private List<String> labelIds;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.openframe.data.repository.ticket;

import com.openframe.data.document.ticket.Ticket;
import com.openframe.data.document.ticket.TicketStatus;
import com.openframe.data.document.ticket.TicketStatusKind;
import com.openframe.data.document.ticket.filter.TicketQueryFilter;
import org.springframework.data.mongodb.core.query.Query;

Expand All @@ -11,27 +11,34 @@

public interface CustomTicketRepository {

default Query buildTicketQuery(TicketQueryFilter filter) {
return buildTicketQuery(filter, null, null, null);
}
Query buildTicketQuery(String tenantId,
TicketQueryFilter filter,
String search,
List<String> restrictToTicketIds,
String ownerMachineId);

Query buildTicketQuery(TicketQueryFilter filter, String search,
List<String> restrictToTicketIds, String ownerMachineId);

List<Ticket> findTicketsWithCursor(Query query, String cursor, int limit,
String sortField, String sortDirection);
List<Ticket> findTicketsWithCursor(Query query,
String cursor,
int limit,
String sortField,
String sortDirection);

long countTickets(Query query);

Map<TicketStatus, Long> countTicketsByStatus();
Map<TicketStatusKind, Long> countTicketsByStatusKind(String tenantId);

Map<String, Long> countTicketsByStatusId(String tenantId);

long getTotalCount();
long getTotalCount(String tenantId);

Optional<Long> getAverageResolutionTimeMs();
Optional<Long> getAverageResolutionTimeMs(String tenantId);

int updateStatusBulk(TicketStatus fromStatus, TicketStatus toStatus);
int reassignTicketsToStatus(String tenantId,
String fromStatusId,
String toStatusId,
TicketStatusKind toKind);

void updateTitle(String ticketId, String title);
void updateTitle(String tenantId, String ticketId, String title);

boolean isSortableField(String field);

Expand Down
Loading
Loading