657 lines
16 KiB
C++
Executable File
657 lines
16 KiB
C++
Executable File
/*
|
|
* This file is a part of Winix
|
|
* and is not publicly distributed
|
|
*
|
|
* Copyright (c) 2008-2010, Tomasz Sowa
|
|
* All rights reserved.
|
|
*
|
|
*/
|
|
|
|
#include "ezc.h"
|
|
#include "ticketinfo.h"
|
|
#include "editticket.h"
|
|
#include "createticket.h"
|
|
#include "core/request.h"
|
|
#include "core/misc.h"
|
|
#include "core/plugin.h"
|
|
#include "functions/functions.h"
|
|
#include "templates/templates.h"
|
|
#include "sessiondata.h"
|
|
#include "plugins/thread/pluginmsg.h"
|
|
|
|
|
|
|
|
namespace Ticket
|
|
{
|
|
using namespace TemplatesFunctions;
|
|
using TemplatesFunctions::system;
|
|
|
|
extern TicketInfo ticket_info;
|
|
extern CreateTicket fun_create_ticket;
|
|
extern EditTicket fun_edit_ticket;
|
|
|
|
|
|
|
|
|
|
int ticket_calc_progress_image_number(int percent)
|
|
{
|
|
percent = int( double(percent) / 10.0 + 0.5 ) * 10;
|
|
|
|
if( percent < 0 )
|
|
percent = 0;
|
|
|
|
if( percent > 100 )
|
|
percent = 100;
|
|
|
|
return percent;
|
|
}
|
|
|
|
|
|
|
|
// binary search for conf_item.id in ticket.par_tab (ticket.par_tab must be sorted by 'param')
|
|
// !! zmienic nazwe na find_ticket_param
|
|
bool find_ticket_value(const TicketConf::TicketItem & conf_item, const Ticket & ticket, size_t & ticket_par_index)
|
|
{
|
|
if( ticket.par_tab.empty() )
|
|
return false;
|
|
|
|
size_t o1 = 0;
|
|
size_t o2 = ticket.par_tab.size() - 1;
|
|
|
|
if( ticket.par_tab[o1].param == conf_item.id )
|
|
{
|
|
ticket_par_index = o1;
|
|
return true;
|
|
}
|
|
|
|
if( conf_item.id < ticket.par_tab[o1].param )
|
|
return false;
|
|
|
|
if( ticket.par_tab[o2].param == conf_item.id )
|
|
{
|
|
while( o2 > 0 && ticket.par_tab[o2-1].param == conf_item.id )
|
|
--o2;
|
|
|
|
ticket_par_index = o2;
|
|
return true;
|
|
}
|
|
|
|
if( conf_item.id > ticket.par_tab[o2].param )
|
|
return false;
|
|
|
|
|
|
while( o1 + 1 < o2 )
|
|
{
|
|
ticket_par_index = (o1 + o2) / 2;
|
|
|
|
if( ticket.par_tab[ticket_par_index].param == conf_item.id )
|
|
{
|
|
while( ticket_par_index > 0 && ticket.par_tab[ticket_par_index-1].param == conf_item.id )
|
|
--ticket_par_index;
|
|
|
|
return true;
|
|
}
|
|
|
|
if( ticket.par_tab[ticket_par_index].param < conf_item.id )
|
|
o1 = ticket_par_index;
|
|
else
|
|
o2 = ticket_par_index;
|
|
}
|
|
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
|
|
void ticket_print_value(Info & i, size_t conf_index, const Ticket::TicketParam & ticket_param)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_index];
|
|
|
|
if( conf_item.type == TicketConf::TicketItem::TypeInteger ||
|
|
conf_item.type == TicketConf::TicketItem::TypeProgress )
|
|
{
|
|
i.out << ticket_param.int_value;
|
|
}
|
|
else
|
|
if( conf_item.type == TicketConf::TicketItem::TypeSelect )
|
|
{
|
|
if( ticket_param.int_value < (int)conf_item.select.size() )
|
|
i.out << conf_item.select[ticket_param.int_value].name;
|
|
}
|
|
else
|
|
if( conf_item.type == TicketConf::TicketItem::TypeString ||
|
|
conf_item.type == TicketConf::TicketItem::TypeMultistring )
|
|
{
|
|
i.out << ticket_param.str_value;
|
|
}
|
|
else
|
|
if( conf_item.type == TicketConf::TicketItem::TypeImages ||
|
|
conf_item.type == TicketConf::TicketItem::TypeFiles )
|
|
{
|
|
i.out << ticket_param.str_value;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void ticket_can_create(Info & i)
|
|
{
|
|
i.res = fun_create_ticket.HasAccess();
|
|
}
|
|
|
|
|
|
void ticket_can_edit(Info & i)
|
|
{
|
|
i.res = fun_edit_ticket.HasAccess();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
namespace ns_tickets_tab
|
|
{
|
|
static size_t item_sort_index; // index for: ticket_info.item_sort_tab
|
|
static size_t ticket_index; // index for: ticket_info.ticket_tab
|
|
static size_t par_index; // index for: ticket_info.ticket_tab[ticket_index].par_tab
|
|
static size_t conf_tab_index; // index for: ticket_info.cur_conf->tab
|
|
static bool has_ticket;
|
|
static bool has_value;
|
|
|
|
|
|
// binary search for id in ticket_tab (ticket_tab must be sorted by 'file_id')
|
|
bool find_ticket(const std::vector<Ticket> & ticket_tab, long id, size_t & ticket_index)
|
|
{
|
|
if( ticket_tab.empty() )
|
|
return false;
|
|
|
|
size_t o1 = 0;
|
|
size_t o2 = ticket_tab.size() - 1;
|
|
|
|
if( ticket_tab[o1].file_id == id )
|
|
{
|
|
ticket_index = o1;
|
|
return true;
|
|
}
|
|
|
|
if( id < ticket_tab[o1].file_id )
|
|
return false;
|
|
|
|
if( ticket_tab[o2].file_id == id )
|
|
{
|
|
ticket_index = o2;
|
|
return true;
|
|
}
|
|
|
|
if( id > ticket_tab[o2].file_id )
|
|
return false;
|
|
|
|
|
|
while( o1 + 1 < o2 )
|
|
{
|
|
ticket_index = (o1 + o2) / 2;
|
|
|
|
if( ticket_tab[ticket_index].file_id == id )
|
|
return true;
|
|
|
|
if( ticket_tab[ticket_index].file_id < id )
|
|
o1 = ticket_index;
|
|
else
|
|
o2 = ticket_index;
|
|
}
|
|
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
void tickets_tab(Info & i)
|
|
{
|
|
has_ticket = false;
|
|
item_sort_index = i.iter;
|
|
i.res = item_sort_index < ticket_info.item_sort_tab.size();
|
|
|
|
if( i.res )
|
|
{
|
|
long id = ticket_info.item_sort_tab[item_sort_index]->id;
|
|
has_ticket = find_ticket(ticket_info.ticket_tab, id, ticket_index);
|
|
|
|
/* for(size_t a=0 ; a<ticket_info.ticket_tab.size() ; ++a)
|
|
{
|
|
if( ticket_info.ticket_tab[a].file_id == ticket_info.item_sort_tab[item_sort_index]->id )
|
|
{
|
|
has_ticket = true;
|
|
ticket_index = a;
|
|
}
|
|
}
|
|
*/
|
|
plugin.Call(WINIX_PL_THREAD_SET_SORTTAB_INDEX, item_sort_index);
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_url(Info & i)
|
|
{
|
|
if( item_sort_index < ticket_info.item_sort_tab.size() )
|
|
i.out << ticket_info.item_sort_tab[item_sort_index]->url;
|
|
}
|
|
|
|
|
|
void tickets_tab_subject_empty(Info & i)
|
|
{
|
|
if( item_sort_index < ticket_info.item_sort_tab.size() )
|
|
i.res = ticket_info.item_sort_tab[item_sort_index]->subject.empty();
|
|
}
|
|
|
|
|
|
void tickets_tab_subject(Info & i)
|
|
{
|
|
if( item_sort_index < ticket_info.item_sort_tab.size() )
|
|
i.out << ticket_info.item_sort_tab[item_sort_index]->subject;
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab(Info & i)
|
|
{
|
|
has_value = false;
|
|
|
|
if( has_ticket &&
|
|
item_sort_index < ticket_info.item_sort_tab.size() &&
|
|
ticket_index < ticket_info.ticket_tab.size() )
|
|
{
|
|
conf_tab_index = i.iter;
|
|
i.res = conf_tab_index < ticket_info.cur_conf->tab.size();
|
|
|
|
if( i.res )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
const Ticket & ticket = ticket_info.ticket_tab[ticket_index];
|
|
has_value = find_ticket_value(conf_item, ticket, par_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_has_value(Info & i)
|
|
{
|
|
i.res = has_value;
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_value(Info & i)
|
|
{
|
|
if( has_value &&
|
|
item_sort_index < ticket_info.item_sort_tab.size() &&
|
|
ticket_index < ticket_info.ticket_tab.size() &&
|
|
par_index < ticket_info.ticket_tab[ticket_index].par_tab.size() )
|
|
{
|
|
ticket_print_value(i, conf_tab_index, ticket_info.ticket_tab[ticket_index].par_tab[par_index]);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void tickets_tab_conf_tab_is_integer(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeInteger;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_is_progress(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeProgress;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_is_select(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeSelect;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_is_string(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeString;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_is_multistring(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeMultistring;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_is_images(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeImages;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_is_files(Info & i)
|
|
{
|
|
if( conf_tab_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_tab_index];
|
|
i.res = conf_item.type == TicketConf::TicketItem::TypeFiles;
|
|
}
|
|
}
|
|
|
|
|
|
void tickets_tab_conf_tab_progress_image_number(Info & i)
|
|
{
|
|
if( has_value &&
|
|
ticket_index < ticket_info.ticket_tab.size() &&
|
|
par_index < ticket_info.ticket_tab[ticket_index].par_tab.size() )
|
|
{
|
|
int progress = ticket_info.ticket_tab[ticket_index].par_tab[par_index].int_value;
|
|
i.out << ticket_calc_progress_image_number(progress);
|
|
}
|
|
}
|
|
|
|
|
|
} // namespace ns_tickets_tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace ns_ticket_tab
|
|
{
|
|
static size_t conf_index; // index for: ticket_info.cur_conf->tab
|
|
static size_t select_index; // index for: ticket_info.cur_conf->tab[conf_index].select
|
|
static size_t par_index; // index for: ticket_info.ticket->par_tab
|
|
static size_t files_number;
|
|
static bool has_value;
|
|
|
|
|
|
|
|
void ticket_tab(Info & i)
|
|
{
|
|
conf_index = i.iter;
|
|
i.res = conf_index < ticket_info.cur_conf->tab.size();
|
|
|
|
if( i.res )
|
|
{
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_index];
|
|
has_value = find_ticket_value(conf_item, *ticket_info.ticket, par_index);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void ticket_tab_param_id(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.out << ticket_info.cur_conf->tab[conf_index].id;
|
|
}
|
|
|
|
|
|
void ticket_tab_param_name(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.out << ticket_info.cur_conf->tab[conf_index].name;
|
|
}
|
|
|
|
|
|
void ticket_tab_value(Info & i)
|
|
{
|
|
if( has_value && par_index < ticket_info.ticket->par_tab.size() )
|
|
ticket_print_value(i, conf_index, ticket_info.ticket->par_tab[par_index]);
|
|
}
|
|
|
|
|
|
void ticket_tab_value_int(Info & i)
|
|
{
|
|
if( has_value && par_index < ticket_info.ticket->par_tab.size() )
|
|
i.out << ticket_info.ticket->par_tab[par_index].int_value;
|
|
}
|
|
|
|
|
|
void ticket_tab_value_str(Info & i)
|
|
{
|
|
if( has_value && par_index < ticket_info.ticket->par_tab.size() )
|
|
i.out << ticket_info.ticket->par_tab[par_index].str_value;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_integer(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeInteger;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_progress(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeProgress;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_select(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeSelect;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_string(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeString;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_multistring(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeMultistring;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_images(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeImages;
|
|
}
|
|
|
|
|
|
void ticket_tab_is_files(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
i.res = ticket_info.cur_conf->tab[conf_index].type == TicketConf::TicketItem::TypeFiles;
|
|
}
|
|
|
|
|
|
void ticket_tab_select_tab(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
select_index = i.iter;
|
|
i.res = select_index < ticket_info.cur_conf->tab[conf_index].select.size();
|
|
}
|
|
}
|
|
|
|
|
|
void ticket_tab_select_tab_is_selected(Info & i)
|
|
{
|
|
if( has_value && par_index < ticket_info.ticket->par_tab.size() )
|
|
{
|
|
i.res = select_index == (size_t)ticket_info.ticket->par_tab[par_index].int_value;
|
|
}
|
|
else
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() &&
|
|
select_index < ticket_info.cur_conf->tab[conf_index].select.size() )
|
|
{
|
|
i.res = select_index == ticket_info.cur_conf->tab[conf_index].select_default;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ticket_tab_select_tab_name(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() &&
|
|
select_index < ticket_info.cur_conf->tab[conf_index].select.size() )
|
|
{
|
|
i.out << ticket_info.cur_conf->tab[conf_index].select[select_index].name;
|
|
}
|
|
}
|
|
|
|
|
|
void ticket_tab_select_tab_id(Info & i)
|
|
{
|
|
if( conf_index < ticket_info.cur_conf->tab.size() &&
|
|
select_index < ticket_info.cur_conf->tab[conf_index].select.size() )
|
|
{
|
|
i.out << ticket_info.cur_conf->tab[conf_index].select[select_index].id;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
void ticket_tab_files_tab(Info & i)
|
|
{
|
|
files_number = i.iter;
|
|
|
|
if( has_value && conf_index < ticket_info.cur_conf->tab.size() )
|
|
{
|
|
if( i.iter == 0 )
|
|
{
|
|
// first iteration, we are using the par_index found by previous call to ticket_tab
|
|
i.res = true;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// next iteration, we are looking for the next par_index
|
|
const TicketConf::TicketItem & conf_item = ticket_info.cur_conf->tab[conf_index];
|
|
|
|
for(++par_index ; par_index < ticket_info.ticket->par_tab.size() ; ++par_index)
|
|
{
|
|
if( ticket_info.ticket->par_tab[par_index].param == conf_item.id )
|
|
{
|
|
i.res = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// there are no more such items
|
|
has_value = false;
|
|
i.res = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ticket_tab_files_tab_index(Info & i)
|
|
{
|
|
i.out << files_number;
|
|
}
|
|
|
|
|
|
|
|
} // namespace ns_ticket_tab
|
|
|
|
|
|
|
|
|
|
|
|
void AddEzcFunctions(PluginInfo & info)
|
|
{
|
|
using namespace ns_tickets_tab;
|
|
using namespace ns_ticket_tab;
|
|
|
|
using TemplatesFunctions::EzcFun;
|
|
EzcFun * fun = reinterpret_cast<EzcFun*>(info.p1);
|
|
|
|
fun->Insert("ticket_can_create", ticket_can_create);
|
|
fun->Insert("ticket_can_edit", ticket_can_edit);
|
|
|
|
fun->Insert("tickets_tab", tickets_tab);
|
|
fun->Insert("tickets_tab_url", tickets_tab_url);
|
|
fun->Insert("tickets_tab_subject_empty", tickets_tab_subject_empty);
|
|
fun->Insert("tickets_tab_subject", tickets_tab_subject);
|
|
fun->Insert("tickets_tab_url", tickets_tab_url);
|
|
|
|
fun->Insert("tickets_tab_conf_tab", tickets_tab_conf_tab);
|
|
fun->Insert("tickets_tab_conf_tab_has_value", tickets_tab_conf_tab_has_value);
|
|
fun->Insert("tickets_tab_conf_tab_value", tickets_tab_conf_tab_value);
|
|
|
|
fun->Insert("tickets_tab_conf_tab_is_integer", tickets_tab_conf_tab_is_integer);
|
|
fun->Insert("tickets_tab_conf_tab_is_progress", tickets_tab_conf_tab_is_progress);
|
|
fun->Insert("tickets_tab_conf_tab_is_select", tickets_tab_conf_tab_is_select);
|
|
fun->Insert("tickets_tab_conf_tab_is_string", tickets_tab_conf_tab_is_string);
|
|
fun->Insert("tickets_tab_conf_tab_is_multistring", tickets_tab_conf_tab_is_multistring);
|
|
fun->Insert("tickets_tab_conf_tab_is_images", tickets_tab_conf_tab_is_images);
|
|
fun->Insert("tickets_tab_conf_tab_is_files", tickets_tab_conf_tab_is_files);
|
|
fun->Insert("tickets_tab_conf_tab_progress_image_number", tickets_tab_conf_tab_progress_image_number);
|
|
|
|
|
|
|
|
fun->Insert("ticket_tab", ticket_tab);
|
|
|
|
fun->Insert("ticket_tab_param_id", ticket_tab_param_id);
|
|
fun->Insert("ticket_tab_param_name", ticket_tab_param_name);
|
|
fun->Insert("ticket_tab_value", ticket_tab_value);
|
|
fun->Insert("ticket_tab_value_int", ticket_tab_value_int);
|
|
fun->Insert("ticket_tab_value_str", ticket_tab_value_str);
|
|
|
|
fun->Insert("ticket_tab_is_integer", ticket_tab_is_integer);
|
|
fun->Insert("ticket_tab_is_progress", ticket_tab_is_progress);
|
|
fun->Insert("ticket_tab_is_select", ticket_tab_is_select);
|
|
fun->Insert("ticket_tab_is_string", ticket_tab_is_string);
|
|
fun->Insert("ticket_tab_is_multistring", ticket_tab_is_multistring);
|
|
fun->Insert("ticket_tab_is_images", ticket_tab_is_images);
|
|
fun->Insert("ticket_tab_is_files", ticket_tab_is_files);
|
|
|
|
fun->Insert("ticket_tab_select_tab", ticket_tab_select_tab);
|
|
fun->Insert("ticket_tab_select_tab_is_selected", ticket_tab_select_tab_is_selected);
|
|
fun->Insert("ticket_tab_select_tab_id", ticket_tab_select_tab_id);
|
|
fun->Insert("ticket_tab_select_tab_name", ticket_tab_select_tab_name);
|
|
|
|
fun->Insert("ticket_tab_files_tab", ticket_tab_files_tab);
|
|
fun->Insert("ticket_tab_files_tab_index", ticket_tab_files_tab_index);
|
|
}
|
|
|
|
|
|
|
|
} // namespace Ticket
|
|
|
|
|
|
|