//Defines //Deciseconds until ticket becomes stale if unanswered. Alerts admins. #define ADMIN_TICKET_TIMEOUT 6000 // 10 minutes //Decisecions before the user is allowed to open another ticket while their existing one is open. #define ADMIN_TICKET_DUPLICATE_COOLDOWN 3000 // 5 minutes //Status defines #define ADMIN_TICKET_OPEN 1 #define ADMIN_TICKET_CLOSED 2 #define ADMIN_TICKET_RESOLVED 3 #define ADMIN_TICKET_STALE 4 SUBSYSTEM_DEF(tickets) name = "Tickets" init_order = INIT_ORDER_TICKETS wait = 300 priority = FIRE_PRIORITY_TICKETS flags = SS_BACKGROUND var/list/allTickets var/ticketCounter = 1 /datum/controller/subsystem/tickets/Initialize() LAZYINITLIST(allTickets) return ..() /datum/controller/subsystem/tickets/fire() var/stales = checkStaleness() if(LAZYLEN(stales)) var/report for(var/num in stales) report += "[num], " message_adminTicket("Tickets [report] have been open for over [ADMIN_TICKET_TIMEOUT / 600] minutes. Changing status to stale.") /datum/controller/subsystem/tickets/stat_entry() ..("Tickets: [LAZYLEN(allTickets)]") /datum/controller/subsystem/tickets/proc/checkStaleness() var/stales = list() for(var/T in allTickets) var/datum/admin_ticket/ticket = T if(!(ticket.ticketState == ADMIN_TICKET_OPEN)) continue if(world.time > ticket.timeUntilStale && (!ticket.lastAdminResponse || !ticket.adminAssigned)) var/id = ticket.makeStale() stales += id return stales //Return the current ticket number ready to be called off. /datum/controller/subsystem/tickets/proc/getTicketCounter() return ticketCounter //Return the ticket counter and increment /datum/controller/subsystem/tickets/proc/getTicketCounterAndInc() . = ticketCounter ticketCounter++ return /datum/controller/subsystem/tickets/proc/resolveAllOpenTickets() // Resolve all open tickets for(var/i in allTickets) var/datum/admin_ticket/T = i resolveTicket(T.ticketNum) //Open a new ticket and populate details then add to the list of open tickets /datum/controller/subsystem/tickets/proc/newTicket(client/C, passedContent, title) if(!C || !passedContent) return //Check if the user has an open ticket already within the cooldown period, if so we don't create a new one and re-set the cooldown period var/datum/admin_ticket/existingTicket = checkForOpenTicket(C) if(existingTicket) existingTicket.setCooldownPeriod() to_chat(C.mob, "Your ticket #[existingTicket.ticketNum] remains open! Visit \"My tickets\" under the Admin Tab to view it.") return if(!title) title = passedContent var/datum/admin_ticket/T = new(title, passedContent) T.clientName = C T.locationSent = C.mob.loc.name T.mobControlled = C.mob //Inform the user that they have opened a ticket to_chat(C, "You have opened admin ticket number #[(SStickets.getTicketCounter() - 1)]! Please be patient and we will help you soon!") //Set ticket state with key N to open /datum/controller/subsystem/tickets/proc/openTicket(N) var/datum/admin_ticket/T = SStickets.allTickets[N] if(T.ticketState != ADMIN_TICKET_OPEN) T.ticketState = ADMIN_TICKET_OPEN return TRUE //Set ticket state with key N to resolved /datum/controller/subsystem/tickets/proc/resolveTicket(N) var/datum/admin_ticket/T = SStickets.allTickets[N] if(T.ticketState != ADMIN_TICKET_RESOLVED) T.ticketState = ADMIN_TICKET_RESOLVED return TRUE //Set ticket state with key N to closed /datum/controller/subsystem/tickets/proc/closeTicket(N) var/datum/admin_ticket/T = SStickets.allTickets[N] if(T.ticketState != ADMIN_TICKET_CLOSED) T.ticketState = ADMIN_TICKET_CLOSED return TRUE //Check if the user already has a ticket open and within the cooldown period. /datum/controller/subsystem/tickets/proc/checkForOpenTicket(client/C) for(var/datum/admin_ticket/T in allTickets) if(T.clientName == C && T.ticketState == ADMIN_TICKET_OPEN && (T.ticketCooldown > world.time)) return T return FALSE //Check if the user has ANY ticket not resolved or closed. /datum/controller/subsystem/tickets/proc/checkForTicket(client/C) var/list/tickets = list() for(var/datum/admin_ticket/T in allTickets) if(T.clientName == C && (T.ticketState == ADMIN_TICKET_OPEN || T.ticketState == ADMIN_TICKET_STALE)) tickets += T if(tickets.len) return tickets return FALSE //return the client of a ticket number /datum/controller/subsystem/tickets/proc/returnClient(N) var/datum/admin_ticket/T = SStickets.allTickets[N] return T.clientName /datum/controller/subsystem/tickets/proc/assignAdminToTicket(client/C, var/N) var/datum/admin_ticket/T = SStickets.allTickets[N] T.assignAdmin(C) return TRUE //Single admin ticket /datum/admin_ticket var/ticketNum // Ticket number var/clientName // Client which opened the ticket var/timeOpened // Time the ticket was opened var/title //The initial message with links var/list/content // content of the admin help var/lastAdminResponse // Last admin who responded var/lastResponseTime // When the admin last responded var/locationSent // Location the player was when they send the ticket var/mobControlled // Mob they were controlling var/ticketState // State of the ticket, open, closed, resolved etc var/timeUntilStale // When the ticket goes stale var/ticketCooldown // Cooldown before allowing the user to open another ticket. var/adminAssigned // Admin who has assigned themselves to this ticket /datum/admin_ticket/New(tit, cont) title = tit content = list() content += cont timeOpened = worldtime2text() timeUntilStale = world.time + ADMIN_TICKET_TIMEOUT setCooldownPeriod() ticketNum = SStickets.getTicketCounterAndInc() ticketState = ADMIN_TICKET_OPEN SStickets.allTickets += src //Set the cooldown period for the ticket. The time when it's created plus the defined cooldown time. /datum/admin_ticket/proc/setCooldownPeriod() ticketCooldown = world.time + ADMIN_TICKET_DUPLICATE_COOLDOWN //Set the last admin who responded as the client passed as an arguement. /datum/admin_ticket/proc/setLastAdminResponse(client/C) lastAdminResponse = C lastResponseTime = worldtime2text() //Return the ticket state as a colour coded text string. /datum/admin_ticket/proc/state2text() switch(ticketState) if(ADMIN_TICKET_OPEN) return "OPEN" if(ADMIN_TICKET_RESOLVED) return "RESOLVED" if(ADMIN_TICKET_CLOSED) return "CLOSED" if(ADMIN_TICKET_STALE) return "STALE" //Assign the client passed to var/adminAsssigned /datum/admin_ticket/proc/assignAdmin(client/C) if(!C) return adminAssigned = C return TRUE /datum/admin_ticket/proc/addResponse(client/C, msg) if(C.holder) setLastAdminResponse(C) msg = "[C]: [msg]" content += msg /datum/admin_ticket/proc/makeStale() ticketState = ADMIN_TICKET_STALE return ticketNum /* UI STUFF */ /datum/controller/subsystem/tickets/proc/returnUI(tab = ADMIN_TICKET_OPEN) set name = "Open Ticket Interface" set category = "Tickets" //dat var/trStyle = "border-top:2px solid; border-bottom:2px solid; padding-top: 5px; padding-bottom: 5px;" var/tdStyleleft = "border-top:2px solid; border-bottom:2px solid; width:150px; text-align:center;" var/tdStyle = "border-top:2px solid; border-bottom:2px solid;" var/datum/admin_ticket/ticket var/dat dat += "" dat += "

Admin Tickets

" dat +="Refresh
Open TicketsResolved TicketsClosed Tickets" if(tab == ADMIN_TICKET_OPEN) dat += "

Open Tickets

" dat += "" dat +="" if(tab == ADMIN_TICKET_OPEN) for(var/T in allTickets) ticket = T if(ticket.ticketState == ADMIN_TICKET_OPEN || ticket.ticketState == ADMIN_TICKET_STALE) dat += "" else continue else if(tab == ADMIN_TICKET_RESOLVED) dat += "

Resolved Tickets

" for(var/T in allTickets) ticket = T if(ticket.ticketState == ADMIN_TICKET_RESOLVED) dat += "" else continue else if(tab == ADMIN_TICKET_CLOSED) dat += "

Closed Tickets

" for(var/T in allTickets) ticket = T if(ticket.ticketState == ADMIN_TICKET_CLOSED) dat += "" else continue dat += "
ControlTicket
ResolveDetails
#[ticket.ticketNum] ([ticket.timeOpened]) [ticket.ticketState == ADMIN_TICKET_STALE ? "STALE" : ""]
[ticket.title]
ResolveDetails
#[ticket.ticketNum] ([ticket.timeOpened])
[ticket.title]
ResolveDetails
#[ticket.ticketNum] ([ticket.timeOpened])
[ticket.title]
" return dat /datum/controller/subsystem/tickets/proc/showUI(mob/user, tab) var/dat = null dat = returnUI(tab) var/datum/browser/popup = new(user, "admintickets", "Admin Tickets", 1400, 600) popup.set_content(dat) popup.open() /datum/controller/subsystem/tickets/proc/showDetailUI(mob/user, ticketID) var/datum/admin_ticket/T = SStickets.allTickets[ticketID] var/status = "[T.state2text()]" var/dat = "

Admin Tickets

" dat +="Show AllRefresh" dat += "

Ticket #[T.ticketNum]

" dat += "

[T.clientName] / [T.mobControlled] opened this ticket at [T.timeOpened] at location [T.locationSent]

" dat += "

Ticket Status: [status]" dat += "" dat += "" if(T.content.len > 1) for(var/i = 2, i <= T.content.len, i++) dat += "" dat += "
[T.title]
[T.content[i]]


" dat += "Re-OpenResolve

" if(!T.adminAssigned) dat += "No admin assigned to this ticket - Take Ticket
" else dat += "[T.adminAssigned] is assigned to this Ticket. - Take Ticket
" if(T.lastAdminResponse) dat += "Last Admin Response: [T.lastAdminResponse] at [T.lastResponseTime]" else dat +="No Admin Response" dat += "

" dat += "Close Ticket" var/datum/browser/popup = new(user, "adminticketsdetail", "Admin Ticket #[T.ticketNum]", 1000, 600) popup.set_content(dat) popup.open() /datum/controller/subsystem/tickets/proc/userDetailUI(mob/user) //dat var/tickets = checkForTicket(user.client) var/dat dat += "

Your open tickets

" dat += "" for(var/datum/admin_ticket/T in tickets) dat += "" for(var/i = 1, i <= T.content.len, i++) dat += "" dat += "

Ticket #[T.ticketNum]

[T.content[i]]
" var/datum/browser/popup = new(user, "userticketsdetail", "Tickets", 1000, 600) popup.set_content(dat) popup.open() //Sends a message to the target safely. If the target left the server it won't throw a runtime. Also accepts lists of text /datum/controller/subsystem/tickets/proc/to_chat_safe(target, text) if(!target) return FALSE if(istype(text, /list)) for(var/T in text) to_chat(target, T) else to_chat(target, text) return TRUE /datum/controller/subsystem/tickets/Topic(href, href_list) if(href_list["refresh"]) showUI(usr) return if(href_list["refreshdetail"]) var/indexNum = text2num(href_list["refreshdetail"]) showDetailUI(usr, indexNum) return if(href_list["showopen"]) showUI(usr, ADMIN_TICKET_OPEN) return if(href_list["showresolved"]) showUI(usr, ADMIN_TICKET_RESOLVED) return if(href_list["showclosed"]) showUI(usr, ADMIN_TICKET_CLOSED) return if(href_list["details"]) var/indexNum = text2num(href_list["details"]) showDetailUI(usr, indexNum) return if(href_list["resolve"]) var/indexNum = text2num(href_list["resolve"]) if(SStickets.resolveTicket(indexNum)) message_adminTicket("[usr.client] / ([usr]) resolved admin ticket number [indexNum]") to_chat_safe(returnClient(indexNum), "Your admin ticket has now been resolved.") showUI(usr) if(href_list["detailresolve"]) var/indexNum = text2num(href_list["detailresolve"]) if(SStickets.resolveTicket(indexNum)) message_adminTicket("[usr.client] / ([usr]) resolved admin ticket number [indexNum]") to_chat_safe(returnClient(indexNum), "Your admin ticket has now been resolved.") showDetailUI(usr, indexNum) if(href_list["detailclose"]) var/indexNum = text2num(href_list["detailclose"]) if(alert("Are you sure? This will send a negative message.",,"Yes","No") != "Yes") return if(SStickets.closeTicket(indexNum)) message_adminTicket("[usr.client] / ([usr]) closed admin ticket number [indexNum]") to_chat_safe(returnClient(indexNum), list( "- AdminHelp Rejected! -", "Please try to be calm, clear, and descriptive in admin helps, do not assume the admin has seen any related events, and clearly state the names of anybody you are reporting. If you asked a question, please ensure it was clear what you were asking.", "Your ticket has now been closed." )) showDetailUI(usr, indexNum) if(href_list["detailreopen"]) var/indexNum = text2num(href_list["detailreopen"]) if(SStickets.openTicket(indexNum)) message_adminTicket("[usr.client] / ([usr]) re-opened admin ticket number [indexNum]") showDetailUI(usr, indexNum) if(href_list["assignadmin"]) var/indexNum = text2num(href_list["assignadmin"]) if(SStickets.assignAdminToTicket(usr.client, indexNum)) message_adminTicket("[usr.client] / ([usr]) has taken ticket number [indexNum]") to_chat_safe(returnClient(indexNum), "Your ticket is being handled by [usr.client].") showDetailUI(usr, indexNum)