diff options
83 files changed, 3719 insertions, 257 deletions
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index ce887efcd..c00a13f2b 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -237,6 +237,13 @@ sub login { $svc_x = $svc_phone; + } elsif ( $p->{'domain'} eq 'ip_mac' ) { + + my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $p->{'username'} } ); + return { error => 'IP address not found' } + unless $svc_broadband; + $svc_x = $svc_broadband; + } elsif ( $p->{email} && (my $contact = FS::contact->by_selfservice_email($p->{email})) ) diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 7bdb6059e..7f883dec1 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -418,6 +418,9 @@ if ( -e $addl_handler_use_file ) { use FS::part_svc_msgcat; use FS::commission_schedule; use FS::commission_rate; + use FS::realestate_location; + use FS::realestate_unit; + use FS::svc_realestate; use FS::saved_search; use FS::sector_coverage; # Sammath Naur diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index b2df048c4..edecb7f38 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -495,8 +495,44 @@ sub tables_hashref { my $username_len = 64; #usernamemax config file - # name type nullability length default local + # Return a hashref defining the entire application database schema + # Each key of the hashref contains a structure describing a database table + # + # table_name => { + # columns => [...], + # primary_key => 'column', + # unique => [column,column,...], + # index => [[column],[column,column],...], + # foreign_keys => [{...},{...},...], + # } + # + # + # columns => [ + # + # 'column_name', + # + # 'column_type', + # + # 'NULL' or '', # 'NULL' : Allow null values + # # '' : Disallow null values + # + # 'length', # Column size value. eg: + # # 40 : VARCHAR(40) + # # '10,2' : FLOAT(10,2) + # + # 'default', # Default column value for a new record + # # (Unclear if setting this to '' results in a default + # # value of NULL or empty string?) + # + # '', # local ? + # + # name, type, nullability, length, default, local, + # name, type, nullability, length, default, local, + # ... + # + # ], + # name type nullability length default local return { 'agent' => { @@ -1757,7 +1793,8 @@ sub tables_hashref { 'classnum', 'int', 'NULL', '', '', '', 'comment', 'varchar', 'NULL', 255, '', '', 'selfservice_access', 'char', 'NULL', 1, '', '', - 'invoice_dest', 'char', 'NULL', 1, '', '', + 'invoice_dest', 'char', 'NULL', 1, '', '', # Y or NULL + 'message_dest', 'char', 'NULL', 1, '', '', # Y or NULL ], 'primary_key' => 'custcontactnum', 'unique' => [ [ 'custnum', 'contactnum' ], ], @@ -7598,6 +7635,57 @@ sub tables_hashref { 'foreign_keys' => [], }, + 'realestate_unit' => { + 'columns' => [ + 'realestatenum', 'serial', '', '', '', '', + 'realestatelocnum', 'int', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'unit_title', 'varchar', '', $char_d, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'realestatenum', + 'unique' => [ ['unit_title'] ], + 'index' => [ + ['agentnum'], + ['realestatelocnum'], + ['disabled'], + ['unit_title'], + ], + 'foreign_keys' => [ + {columns => ['agentnum'], table => 'agent'}, + {columns => ['realestatelocnum'] => table => 'realestate_location'}, + ], + }, + + realestate_location => { + 'columns' => [ + 'realestatelocnum', 'serial', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'location_title', 'varchar', '', $char_d, '', '', + 'address1', 'varchar', 'NULL', $char_d, '', '', + 'address2', 'varchar', 'NULL', $char_d, '', '', + 'city', 'varchar', 'NULL', $char_d, '', '', + 'state', 'varchar', 'NULL', $char_d, '', '', + 'zip', 'char', 'NULL', 5, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + primary_key => 'realestatelocnum', + 'unique' => [ ['location_title'] ], + 'index' => [ ['agentnum'], ['disabled'] ], + 'foreign_keys' => [ + {columns => ['agentnum'], table => 'agent'}, + ], + }, + + svc_realestate => { + columns => [ + 'svcnum', 'serial', '', '', '', '', + 'realestatenum', 'int', 'NULL', '', '', '', + ], + primary_key => 'svcnum', + index => [], + }, + # name type nullability length default local #'new_table' => { @@ -7624,4 +7712,3 @@ L<DBIx::DBSchema> =cut 1; - diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 9ec0f3288..3de022466 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -1737,6 +1737,14 @@ sub _cdr_date_parse { # Telos 2014-10-10T05:30:33Z ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 ); $options{gmt} = 1; + } elsif ( $date =~ /^(\d+):(\d+):(\d+)\.\d+ \w+ (\w+) (\d+) (\d+)$/ ) { + ($hour, $min, $sec, $mon, $day, $year) = ( $1, $2, $3, $4, $5, $6 ); + $mon = { # Acme Packet: 15:54:56.868 PST DEC 18 2017 + # My best guess of month abbv they may use + JAN => '01', FEB => '02', MAR => '03', APR => '04', + MAY => '05', JUN => '06', JUL => '07', AUG => '08', + SEP => '09', OCT => '10', NOV => '11', DEC => '12' + }->{$mon}; } else { die "unparsable date: $date"; #maybe we shouldn't die... } @@ -1901,4 +1909,3 @@ L<FS::Record>, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index f32a7bd85..f2263c552 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -19,15 +19,19 @@ FS::cdr::Import - CDR importing use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'mysql', #Pg, Sybase, etc. - 'table' => 'TABLE_NAME', - 'primary_key' => 'BILLING_ID', - 'status_table' = > 'STATUS_TABLE_NAME', # if using a table rather than field in main table - 'column_map' => { #freeside => remote_db - 'freeside_column' => 'remote_db_column', - 'freeside_column' => sub { my $row = shift; $row->{remote_db_column}; }, + 'dbd' => 'Pg', #mysql, Sybase, etc. + 'database' => 'DATABASE_NAME', + 'table' => 'TABLE_NAME',, + 'status_table' => 'STATUS_TABLE_NAME', # if using a table rather than field in main table + 'primary_key' => 'BILLING_ID', + 'primary_key_info' => 'BIGINT', # defaults to bigint + 'status_column' => 'STATUS_COLUMN_NAME', # defaults to freesidestatus + 'status_column_info' => 'varchar(32)', # defaults to varchar(32) + 'column_map' => { #freeside => remote_db + 'freeside_column' => 'remote_db_column', + 'freeside_column' => sub { my $row = shift; $row->{remote_db_column}; }, }, - 'batch_name' => 'batch_name', # cdr_batch name -import-date gets appended. + 'batch_name' => 'batch_name', # cdr_batch name -import-date gets appended. ); =head1 DESCRIPTION @@ -46,61 +50,64 @@ sub dbi_import { my %opt; #opt is specified for each install / run of the script getopts('H:U:P:D:T:c:L:S:', \%opt); - my $user = shift(@ARGV) or die $class->cli_usage; - - $opt{D} ||= $args{database}; - - #do we want to add more types? or add as we go? - my %dbi_connect_types = { - 'Sybase' => ':server', - 'Pg' => ':host', - }; - - my $dsn = 'dbi:'. $args{dbd}; - my $dbi_connect_type = $dbi_connect_types{$args{'dbd'}} ? $dbi_connect_types{$args{'dbd'}} : ':host'; - $dsn .= $dbi_connect_type . "=$opt{H}"; - $dsn .= ";database=$opt{D}" if $opt{D}; + my $user = shift(@ARGV) or die $class->cli_usage; + my $database = $opt{D} || $args{database}; + my $table = $opt{T} || $args{table}; + my $pkey = $args{primary_key}; + my $pkey_info = $args{primary_key_info} ? $args{primary_key_info} : 'BIGINT'; + my $status_table = $opt{S} || $args{status_table}; + my $dbd_type = $args{'dbd'} ? $args{'dbd'} : 'Pg'; + my $status_column = $args{status_column} ? $args{status_column} : 'freesidestatus'; + my $status_column_info = $args{status_column_info} ? $args{status_column} : 'VARCHAR(32)'; + + my $queries = get_queries({ + 'dbd' => $dbd_type, + 'host' => $opt{H}, + 'table' => $table, + 'status_column' => $status_column, + 'status_column_info' => $status_column_info, + 'status_table' => $status_table, + 'primary_key' => $pkey, + 'primary_key_info' => $pkey_info, + }); + + my $dsn = 'dbi:'. $dbd_type . $queries->{connect_type}; + $dsn .= ";database=$database" if $database; my $dbi = DBI->connect($dsn, $opt{U}, $opt{P}) or die $DBI::errstr; adminsuidsetup $user; - #my $fsdbh = FS::UID::dbh; - - my $table = $opt{T} || $args{table}; - my $pkey = $args{primary_key}; - my $status_table = $opt{S} || $args{status_table}; - - #just doing this manually with IVR MSSQL databases for now - # # check for existence of freesidestatus - # my $status = $dbi->selectall_arrayref("SHOW COLUMNS FROM $table WHERE Field = 'freesidestatus'"); - # if( ! @$status ) { - # print "Adding freesidestatus column...\n"; - # $dbi->do("ALTER TABLE $table ADD COLUMN freesidestatus varchar(32)") - # or die $dbi->errstr; - # } - # else { - # print "freesidestatus column present\n"; - # } - # or if using a status_table: - # CREATE TABLE FREESIDE_BILLING ( - # BILLING_ID BIGINT, - # FREESIDESTATUS VARCHAR(32) - # ) + ## check for status table if using. if not there create it. + if ($status_table) { + my $status = $dbi->selectall_arrayref( $queries->{check_statustable} ); + if( ! @$status ) { + print "Adding status table $status_table ...\n"; + $dbi->do( $queries->{create_statustable} ) + or die $dbi->errstr; + } + } + ## check for column freeside status if not using status table and create it if not there. + else { + my $status = $dbi->selectall_arrayref( $queries->{check_statuscolumn} ); + if( ! @$status ) { + print "Adding $status_column column...\n"; + $dbi->do( $queries->{create_statuscolumn} ) + or die $dbi->errstr; + } + } #my @cols = values %{ $args{column_map} }; my $sql = "SELECT $table.* FROM $table "; # join(',', @cols). " FROM $table ". - $sql .= 'LEFT JOIN '. $status_table. - " ON ( $table.$pkey = ". $status_table. ".$pkey )" + $sql .= "LEFT JOIN $status_table ON ( $table.$pkey = $status_table.$pkey ) " if $status_table; - $sql .= ' WHERE freesidestatus IS NULL '; + $sql .= "WHERE $status_column IS NULL "; #$sql .= ' LIMIT '. $opt{L} if $opt{L}; my $sth = $dbi->prepare($sql); $sth->execute or die $sth->errstr. " executing $sql"; - #MySQL-specific print "Importing ".$sth->rows." records...\n"; my $cdr_batch = new FS::cdr_batch({ 'cdrbatch' => $args{batch_name} . '-import-'. time2str('%Y/%m/%d-%T',time), @@ -123,6 +130,7 @@ sub dbi_import { } $hash{$field} = '' if $hash{$field} =~ /^\s+$/; #IVR (MSSQL?) bs } + my $cdr = FS::cdr->new(\%hash); $cdr->cdrtypenum($opt{c}) if $opt{c}; @@ -145,12 +153,12 @@ sub dbi_import { if ( $status_table ) { $st_sql = - 'INSERT INTO '. $status_table. " ( $pkey, freesidestatus ) ". + 'INSERT INTO '. $status_table. " ( $pkey, $status_column ) ". " VALUES ( ?, 'done' )"; } else { - $st_sql = "UPDATE $table SET freesidestatus = 'done' WHERE $pkey = ?"; + $st_sql = "UPDATE $table SET $status_column = 'done' WHERE $pkey = ?"; } @@ -174,15 +182,63 @@ sub dbi_import { } sub cli_usage { - #"Usage: \n $0\n\t[ -H hostname ]\n\t-D database\n\t-U user\n\t-P password\n\tfreesideuser\n"; - #"Usage: \n $0\n\t-H hostname\n\t-D database\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\tfreesideuser\n"; "Usage: \n $0\n\t-H hostname\n\t[ -D database ]\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\t[ -L num_cdrs_limit ]\n\t[ -T table ]\n\t[ -S status table ]\n\tfreesideuser\n"; } +sub get_queries { + #my ($dbd, $host, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; + my $info = shift; + + #get host and port information. + my ($host, $port) = split /:/, $info->{host}; + $host ||= 'localhost'; + $port ||= '5000'; # check for pg default 5000 is sybase. + + my %dbi_connect_types = ( + 'Sybase' => ':host='.$host.';port='.$port, + 'Pg' => ':host='.$info->{host}, + ); + + #Check for freeside status table + my %dbi_check_statustable = ( + 'Sybase' => "SELECT * FROM sysobjects WHERE name = '$info->{status_table}'", + 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", + ); + + #Create freeside status table + my %dbi_create_statustable = ( + 'Sybase' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", + 'Pg' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", + ); + + #Check for freeside status column + my %dbi_check_statuscolumn = ( + 'Sybase' => "SELECT syscolumns.name FROM sysobjects + JOIN syscolumns ON sysobjects.id = syscolumns.id + WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = '$info->{status_column}'", + 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{table}' AND column_name = '$info->{status_column}' ", + ); + + #Create freeside status column + my %dbi_create_statuscolumn = ( + 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info} NULL", + 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + ); + + my $queries = { + 'connect_type' => $dbi_connect_types{$info->{dbd}}, + 'check_statustable' => $dbi_check_statustable{$info->{dbd}}, + 'create_statustable' => $dbi_create_statustable{$info->{dbd}}, + 'check_statuscolumn' => $dbi_check_statuscolumn{$info->{dbd}}, + 'create_statuscolumn' => $dbi_create_statuscolumn{$info->{dbd}}, + }; + + return $queries; +} + =head1 BUGS -Not everything has been refactored out of the various bin/cdr-*.import scripts, -let alone other places. +currently works with Pg(Postgresql) and Sybase(Sybase AES) Sparse documentation. diff --git a/FS/FS/cdr/acmepacket.pm b/FS/FS/cdr/acmepacket.pm new file mode 100644 index 000000000..1f00e4086 --- /dev/null +++ b/FS/FS/cdr/acmepacket.pm @@ -0,0 +1,168 @@ +package FS::cdr::acmepacket; + +=head1 NAME + +FS:cdr::acmepacket - CDR import definition based on Acme Packet Net-Net 4000 + +=head1 DESCRIPTION + +The Acme Packet NetNet 4000 S-CX6.4.0 generates an odd cdr log format: + + - Each row in the CSV may be in one of many various formats, some of + them undocumented. + - Columns are inconsistently enclosed with " characters + - Quoted column values may, themselves, contain unescaped quote marks. + This breaks Text::CSV (well technically the FORMAT is broken, not + Text::CSV). + - A single call can generate multiple CDR records. The only records we're + interested in are billable calls: + - These are called "Stop Record CSV Placement" in Acme Packet documentation + - These will always contain a "2" as the first column value + - These rows may be 175 or 269 fields in length. It's unclear if the + undocumented 269 column rows are an intentional Acme Packet format, or + a bug in their switch. The extra columns are inserted at idx 115, + and can safely be disregarded. + +NOTE ON DATE PARSING: + + The Acme Packet manual doesn't describe it's date format in detail. The sample + we were given contains only records from December. Dates are formatted like + so: 15:54:56.868 PST DEC 18 2017 + + I gave my best guess how they will format the month text in the parser + FS::cdr::_cdr_date_parse(). If this format doesn't import records on a + particular month, check there. + +=cut + +use strict; +use warnings; +use vars qw(%info); +use base qw(FS::cdr); +use FS::cdr qw(_cdr_date_parse); +use Text::CSV; + +my $DEBUG = 0; + +my $cbcsv = Text::CSV->new({binary => 1}) + or die "Error loading Text::CSV - ".Text::CSV->error_diag(); + +# Used to map source format into the contrived format created for import_fields +# $cdr_premap[ IMPORT_FIELDS_IDX ] = SOURCE_FORMAT_IDX +my @cdr_premap = ( + 9, # clid + 9, # src + 10, # dst + 22, # channel + 21, # dstchannel + 26, # src_ip_addr + 28, # dst_ip_addr + 13, # startdate + 14, # answerdate + 15, # enddate + 12, # duration + 12, # billsec + 3, # userfield +); + +our %info = ( + name => 'Acme Packet', + weight => 600, + header => 0, + type => 'csv', + + import_fields => [ + # freeside # [idx] acmepacket + 'clid', # 9 Calling-Station-Id + 'src', # 9 Calling-Station-Id + 'dst', # 10 Called-Station-Id + 'channel', # 22 Acme-Session-Ingress-Realm + 'dstchannel', # 23 Acme-Session-Egress-Realm + 'src_ip_addr', # 26 Acme-Flow-In-Src-Adr_FS1_f + 'dst_ip_addr', # 28 Acme-Flow-In-Dst-Addr_FS1_f + 'startdate', # 13 h323-setup-time + 'answerdate', # 14 h323-connect-time + 'enddate', # 15 h323-disconnect-time + 'duration', # 12 Acct-Session-Time + 'billsec', # 12 Acct-Session-Time + 'userfield', # 3 Acct-Session-Id + ], + + row_callback => sub { + my $line = shift; + + # Only process records whose first column contains a 2 + return undef unless $line =~ /^2\,/; + + # Replace unescaped quote characters within quote-enclosed text cols + $line =~ s/(?<!\,)\"(?!\,)/\'/g; + + unless( $cbcsv->parse($line) ) { + warn "Text::CSV Error parsing Acme Packet CDR: ".$cbcsv->error_diag(); + return undef; + } + + my @row = $cbcsv->fields(); + if (@row == 269) { + # Splice out the extra columns + @row = (@row[0..114], @row[209..@row-1]); + } elsif (@row != 175) { + warn "Unknown row format parsing Acme Packet CDR"; + return undef; + } + + my @out = map{ $row[$_] } @cdr_premap; + + if ($DEBUG) { + warn "~~~~~~~~~~pre-processed~~~~~~~~~~~~~~~~ \n"; + warn "$_ $out[$_] \n" for (0..@out-1); + } + + # answerdate, enddate, startdate + for (7,8,9) { + $out[$_] = _cdr_date_parse($out[$_]); + if ($out[$_] =~ /\D/) { + warn "Unparsable date in Acme Packet CDR: ($out[$_])"; + return undef; + } + } + + # clid, dst, src CDR field formatted as one of the following: + # 'WIRELESS CALLER'<sip:12513001300@4.2.2.2:5060;user=phone>;tag=SDepng302 + # <sip:12513001300@4.2.2.2:5060;user=phone>;tag=SDepng302 + # '12513001300'<sip:4.2.2.2:5060;user=phone>;tag=SDepng302 + + # clid + $out[0] = $out[0] =~ /^\'(.+)\'/ ? $1 : ""; + + # src, dst + # Use the 7+ digit number as the src/dst phone number. + # Prefer using the number within <sip> label. If there is not one, + # allow using number from caller-id text field. + for (1,2) { + my $f = $out[$_]; + $out[$_] =~ s/^\'.+\'//g; # strip caller id label portion + if ($out[$_] =~ /(\d{7,})/) { + # Using number from <sip> + $out[$_] = $1; + } elsif ($f =~ /(\d{7,})/) { + # Using number from caller-id text + $out[$_] = $1; + } else { + # No phone number, perhaps an IP only call + $out[$_] = undef; + } + } + + if ($DEBUG) { + warn "~~~~~~~~~~post-processed~~~~~~~~~~~~~~~~ \n"; + warn "$_ $out[$_] \n" for (0..@out-1); + } + + # I haven't seen commas in sample data text fields. Extra caution, + # mangle commas into periods as we pass back to importer + join ',', map{ $_ =~ s/\,/\./g; $_ } @out; + }, +); + +1; diff --git a/FS/FS/cdr/ani_networks.pm b/FS/FS/cdr/ani_networks.pm new file mode 100644 index 000000000..cac30c488 --- /dev/null +++ b/FS/FS/cdr/ani_networks.pm @@ -0,0 +1,81 @@ +package FS::cdr::ani_networks; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info ); +use Time::Local; + +%info = ( + 'name' => 'ANI NETWORKS', + 'weight' => 60, + 'type' => 'fixedlength', + 'fixedlength_format' => [qw( + call_date_time:14:1:14 + bill_to_number:15:15:29 + translate_number:10:30:39 + originating_number:10:40:49 + originating_lata:3:50:52 + originating_city:30:53:82 + originating_state:2:83:84 + originating_country:4:85:88 + terminating_number:15:89:103 + terminating_lata:3:104:106 + terminating_city:30:107:136 + terminating_state:2:137:138 + terminating_citycode:3:139:141 + terminating_country:4:142:145 + call_type:2:146:147 + call_transport:1:148:148 + account_code:12:149:160 + info_digits:2:161:162 + duration:8:163:170 + wholesale_amount:9:171:179 + cic:4:180:183 + originating_lrn:10:184:193 + terminating_lrn:10:194:203 + originating_ocn:4:204:207 + terminating_ocn:4:208:211 + )], + 'import_fields' => [ + + sub { #call_date and time + my($cdr, $data, $conf, $param) = @_; + $data =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ or die "unparsable record_date: $data"; + $cdr->set('calldate', "$2/$3/$1 $4:$5:$6"); + }, + + 'charged_party', #bill to number + '', #translate number + + 'src', #originating number + + '', #originating lata + '', #originating city + '', #originating state + '', #originating country + + 'dst', #terminating number + + '', #terminating lata + '', #terminating city + '', #terminating state + '', #terminating city code + '', #terminating country + + '', #call type + '', #call transport + 'accountcode', #account code + '', #info digits + 'duration', #duration + '', #wholesale amount + '', #cic + 'src_lrn', #originating lrn + 'dst_lrn', #terminating lrn + '', #originating ocn + '', #terminating ocn + + ], + +); + +1;
\ No newline at end of file diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 44c538806..fa047f59d 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -155,7 +155,7 @@ sub insert { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access invoice_dest )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest)) { $link_hash{$_} = $self->get($_); $self->$_(''); } @@ -430,7 +430,7 @@ sub replace { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access invoice_dest )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest )) { $link_hash{$_} = $self->get($_); $self->$_(''); } @@ -955,7 +955,7 @@ sub cgi_contact_fields { my @contact_fields = qw( classnum first last title comment emailaddress selfservice_access - invoice_dest password + invoice_dest message_dest password ); push @contact_fields, 'phonetypenum'. $_->phonetypenum @@ -1028,4 +1028,3 @@ L<FS::Record>, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cust_contact.pm b/FS/FS/cust_contact.pm index 118a9e000..adad46e9e 100644 --- a/FS/FS/cust_contact.pm +++ b/FS/FS/cust_contact.pm @@ -59,6 +59,11 @@ empty or Y 'Y' if the customer should get invoices sent to this address, null if not +=item message_dest + +'Y' if contact should get non-invoice email messages sent to this address, +NULL if not + =back =head1 METHODS @@ -119,6 +124,7 @@ sub check { || $self->ut_textn('comment') || $self->ut_enum('selfservice_access', [ '', 'Y' ]) || $self->ut_flag('invoice_dest') + || $self->ut_flag('message_dest') ; return $error if $error; @@ -148,4 +154,3 @@ L<FS::contact>, L<FS::cust_main>, L<FS::Record> =cut 1; - diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 925eb4e44..7c9868d7a 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3022,48 +3022,104 @@ sub invoicing_list_emailonly_scalar { join(', ', $self->invoicing_list_emailonly); } -=item contact_list [ CLASSNUM, ... ] +=item contact_list [ CLASSNUM, DEST_FLAG... ] -Returns a list of contacts (L<FS::contact> objects) for the customer. If -a list of contact classnums is given, returns only contacts in those -classes. If the pseudo-classnum 'invoice' is given, returns contacts that -are marked as invoice destinations. If '0' is given, also returns contacts -with no class. +Returns a list of contacts (L<FS::contact> objects) for the customer. If no arguments are given, returns all contacts for the customer. +Arguments may contain classnums. When classnums are specified, only +contacts with a matching cust_contact.classnum are returned. When a +classnum of 0 is given, contacts with a null classnum are also included. + +Arguments may also contain the dest flag names 'invoice' or 'message'. +If given, contacts who's invoice_dest and/or message_dest flags are +not set to 'Y' will be excluded. + =cut sub contact_list { my $self = shift; my $search = { table => 'contact', - select => 'contact.*, cust_contact.invoice_dest', + select => join(', ',( + 'contact.*', + 'cust_contact.invoice_dest', + 'cust_contact.message_dest', + )), addl_from => ' JOIN cust_contact USING (contactnum)', extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum, }; - my @orwhere; + # Bugfix notes: + # Calling methods were relying on this method to use invoice_dest to + # block e-mail messages. Depending on parameters, this may or may not + # have actually happened. + # + # The bug could cause this SQL to be used to filter e-mail addresses: + # + # AND ( + # cust_contact.classnums IN (1,2,3) + # OR cust_contact.invoice_dest = 'Y' + # ) + # + # improperly including everybody with the opt-in flag AND everybody + # in the contact classes + # + # Possibility to introduce new bugs: + # If callers of this method called it incorrectly, and didn't notice + # because it seemed to send the e-mails they wanted. + + # WHERE ... + # AND ( + # ( + # cust_contact.classnum IN (1,2,3) + # OR + # cust_contact.classnum IS NULL + # ) + # AND ( + # cust_contact.invoice_dest = 'Y' + # OR + # cust_contact.message_dest = 'Y' + # ) + # ) + + my @and_dest; + my @or_classnum; my @classnums; - foreach (@_) { - if ( $_ eq 'invoice' ) { - push @orwhere, 'cust_contact.invoice_dest = \'Y\''; - } elsif ( $_ eq '0' ) { - push @orwhere, 'cust_contact.classnum is null'; + for (@_) { + if ($_ eq 'invoice' || $_ eq 'message') { + push @and_dest, " cust_contact.${_}_dest = 'Y' "; + } elsif ($_ eq '0') { + push @or_classnum, ' cust_contact.classnum IS NULL '; } elsif ( /^\d+$/ ) { push @classnums, $_; } else { - die "bad classnum argument '$_'"; + croak "bad classnum argument '$_'"; } } - if (@classnums) { - push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')'; - } - if (@orwhere) { - $search->{extra_sql} .= ' AND (' . - join(' OR ', map "( $_ )", @orwhere) . - ')'; + push @or_classnum, 'cust_contact.classnum IN ('.join(',',@classnums).')' + if @classnums; + + if (@or_classnum || @and_dest) { # catch, no arguments given + $search->{extra_sql} .= ' AND ( '; + + if (@or_classnum) { + $search->{extra_sql} .= ' ( '; + $search->{extra_sql} .= join ' OR ', map {" $_ "} @or_classnum; + $search->{extra_sql} .= ' ) '; + $search->{extra_sql} .= ' AND ( ' if @and_dest; + } + + if (@and_dest) { + $search->{extra_sql} .= join ' OR ', map {" $_ "} @and_dest; + $search->{extra_sql} .= ' ) ' if @or_classnum; + } + + $search->{extra_sql} .= ' ) '; + + warn "\$extra_sql: $search->{extra_sql} \n" if $DEBUG; } qsearch($search); @@ -5540,4 +5596,3 @@ L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index 0a12c8752..3d2031d45 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -8,6 +8,48 @@ use FS::UID qw( dbh ); use FS::CurrentUser; use FS::Record qw( qsearchs ); use FS::cust_main; +use FS::Conf; + +my $DEBUG = ''; + +my %import_charges_info; +foreach my $INC ( @INC ) { + warn "globbing $INC/FS/cust_main/import_charges/[a-z]*.pm\n" if $DEBUG; + foreach my $file ( glob("$INC/FS/cust_main/import_charges/[a-z]*.pm") ) { + warn "attempting to load import charges format info from $file\n" if $DEBUG; + $file =~ /\/(\w+)\.pm$/ or do { + warn "unrecognized file in $INC/FS/cust_main/import_charges/: $file\n"; + next; + }; + my $mod = $1; + my $info = eval "use FS::cust_main::import_charges::$mod; ". + "\\%FS::cust_main::import_charges::$mod\::info;"; + if ( $@ ) { + die "error using FS::cust_main::import_charges::$mod (skipping): $@\n" if $@; + next; + } + unless ( keys %$info ) { + warn "no %info hash found in FS::cust_main::import_charges::$mod, skipping\n"; + next; + } + warn "got import charges format info from FS::cust_main::import_charges::$mod: $info\n" if $DEBUG; + if ( exists($info->{'disabled'}) && $info->{'disabled'} ) { + warn "skipping disabled import charges format FS::cust_main::import_charges::$mod" if $DEBUG; + next; + } + $import_charges_info{$mod} = $info; + } +} + +tie my %import_formats, 'Tie::IxHash', + map { $_ => $import_charges_info{$_}->{'name'} } + sort { $import_charges_info{$a}->{'weight'} <=> $import_charges_info{$b}->{'weight'} } + grep { exists($import_charges_info{$_}->{'fields'}) } + keys %import_charges_info; + +sub import_formats { + %import_formats; +} =head1 NAME @@ -65,17 +107,10 @@ sub batch_charge { my @fields; my %charges; - if ( $format eq 'simple' ) { - @fields = qw( custnum agent_custid amount pkg ); - } elsif ( $format eq 'ooma' ) { - @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); - ##should charges to charge be a config option? - %charges = ( - 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', - 'FEDERAL TRS FUND' => '1', - 'FEDERAL UNIVERSAL SERVICE FUND' => '1', - 'STATE SALES TAX' => '1', - ); + + if ( $import_charges_info{$format} ) { + @fields = @{$import_charges_info{$format}->{'fields'}}; + %charges = %{$import_charges_info{$format}->{'charges'}}; } else { die "unknown format $format"; } diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 2ec87cd14..815304bb4 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -1,6 +1,7 @@ package FS::cust_main::Search; use strict; +use Carp qw( croak ); use base qw( Exporter ); use vars qw( @EXPORT_OK $DEBUG $me $conf @fuzzyfields ); use String::Approx qw(amatch); @@ -804,15 +805,51 @@ sub search { unless $params->{'cancelled_pkgs'}; ## - # "with email address(es)" checkbox + # "with email address(es)" checkbox, + # also optionally: with_email_dest and with_contact_type ## - push @where, - 'EXISTS ( SELECT 1 FROM contact_email + if ($params->{with_email}) { + my @email_dest; + my $email_dest_sql; + my $contact_type_sql; + + if ($params->{with_email_dest}) { + croak unless ref $params->{with_email_dest} eq 'ARRAY'; + + @email_dest = @{$params->{with_email_dest}}; + $email_dest_sql = + " AND ( ". + join(' OR ',map(" cust_contact.${_}_dest IS NOT NULL ", @email_dest)). + " ) "; + # Can't use message_dist = 'Y' because single quotes are escaped later + } + if ($params->{with_contact_type}) { + croak unless ref $params->{with_contact_type} eq 'ARRAY'; + + my @contact_type = grep {/^\d+$/ && $_ > 0} @{$params->{with_contact_type}}; + my $has_null_type = 0; + $has_null_type = 1 if grep { $_ eq 0 } @{$params->{with_contact_type}}; + my $hnt_sql; + if ($has_null_type) { + $hnt_sql = ' OR ' if @contact_type; + $hnt_sql .= ' cust_contact.classnum IS NULL '; + } + + $contact_type_sql = + " AND ( ". + join(' OR ', map(" cust_contact.classnum = $_ ", @contact_type)). + $hnt_sql. + " ) "; + } + push @where, + "EXISTS ( SELECT 1 FROM contact_email JOIN cust_contact USING (contactnum) WHERE cust_contact.custnum = cust_main.custnum - )' - if $params->{'with_email'}; + $email_dest_sql + $contact_type_sql + ) "; + } ## # "with postal mail invoices" checkbox @@ -1390,4 +1427,3 @@ L<FS::cust_main>, L<FS::Record> =cut 1; - diff --git a/FS/FS/cust_main/import_charges/gcet.pm b/FS/FS/cust_main/import_charges/gcet.pm new file mode 100644 index 000000000..83f956545 --- /dev/null +++ b/FS/FS/cust_main/import_charges/gcet.pm @@ -0,0 +1,26 @@ +package FS::cust_main::import_charges::gcet; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# gcet fields. +my @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = { + 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', + 'FEDERAL TRS FUND' => '1', + 'FEDERAL UNIVERSAL SERVICE FUND' => '1', + 'STATE SALES TAX' => '1', +}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Gcet', + 'weight' => '30', + 'disabled' => '1', +); + +1;
\ No newline at end of file diff --git a/FS/FS/cust_main/import_charges/ooma.pm b/FS/FS/cust_main/import_charges/ooma.pm new file mode 100644 index 000000000..a43def239 --- /dev/null +++ b/FS/FS/cust_main/import_charges/ooma.pm @@ -0,0 +1,21 @@ +package FS::cust_main::import_charges::ooma; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# ooma fields +my @fields = ('userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = {}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Ooma', + 'weight' => '10', + 'disabled' => '', +); + +1;
\ No newline at end of file diff --git a/FS/FS/cust_main/import_charges/simple.pm b/FS/FS/cust_main/import_charges/simple.pm new file mode 100644 index 000000000..e039328ba --- /dev/null +++ b/FS/FS/cust_main/import_charges/simple.pm @@ -0,0 +1,21 @@ +package FS::cust_main::import_charges::simple; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# simple field format +my @fields = ('custnum', 'agent_custid', 'amount', 'pkg'); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = {}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Simple', + 'weight' => '1', + 'disabled' => '', +); + +1;
\ No newline at end of file diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 8b6569a74..cceaa4bc7 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -348,10 +348,21 @@ sub cust_search_sql { =item email_search_result HASHREF -Emails a notice to the specified customers. Customers without -invoice email destinations will be skipped. +Emails a notice to the specified customer's contact_email addresses. -Parameters: + +If the user has specified "Invoice recipients" on the send e-mail screen, +contact_email rows containing the invoice_dest flag will be included. +This option is default, if neither 'invoice' nor 'message' are present. + +If the user has specified "Message recipients" on the send e-mail screen, +contact_email rows containing the message_dest flag will be included. + +The selection is indicated by the presence of the text 'message' or +'invoice' within the to_contact_classnum argument. + + +Parameters: =over 4 @@ -386,9 +397,19 @@ Text body =item to_contact_classnum -The customer contact class (or classes, as a comma-separated list) to send -the message to. If unspecified, will be sent to any contacts that are marked -as invoice destinations (the equivalent of specifying 'invoice'). +This field contains a comma-separated list. This list may contain: + +- the text "invoice" indicating contacts with invoice_dest flag should + be included +- the text "message" indicating contacts with message_dest flag should + be included +- numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL + +If neither 'invoice' nor 'message' has been specified, this method will +behave as if 'invoice' had been selected =back @@ -483,8 +504,8 @@ sub email_search_result { next; # unlinked object; nothing else we can do } -my %to = {}; -if ($to) { $to{'to'} = $to; } + my %to = (); + if ($to) { $to{'to'} = $to; } my $cust_msg = $msg_template->prepare( 'cust_main' => $cust_main, @@ -736,4 +757,3 @@ L<FS::cust_main>, L<FS::Record> =cut 1; - diff --git a/FS/FS/h_svc_realestate.pm b/FS/FS/h_svc_realestate.pm new file mode 100644 index 000000000..2fdd291d1 --- /dev/null +++ b/FS/FS/h_svc_realestate.pm @@ -0,0 +1,31 @@ +package FS::h_svc_realestate; + +use strict; +use vars qw( @ISA ); +use FS::h_Common; + + +@ISA = qw( FS::h_Common ); + +sub table { 'h_svc_realestate' }; + +=head1 NAME + +FS::h_svc_circuit - Historical realestate service objects + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +An FS::h_svc_realestate object + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::h_Common>, L<FS::svc_realestate>, L<FS::Record>, schema.html from the base +documentation. + +=cut + +1; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 4ae89f056..37c1fab46 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -210,6 +210,22 @@ go away in the future. A L<MIME::Entity> (or arrayref of them) to attach to the message. +=item to_contact_classnum + +Set a string containing a comma-separated list. This list may contain: + +- the text "invoice" indicating contacts with invoice_dest flag should + be included +- the text "message" indicating contacts with message_dest flag should + be included +- numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL + +If neither 'invoice' nor 'message' has been specified, this method will +behave as if 'invoice' had been selected + =cut =back @@ -296,8 +312,16 @@ sub prepare { my $classnum = $opt{'to_contact_classnum'} || ''; my @classes = ref($classnum) ? @$classnum : split(',', $classnum); - # traditional behavior: send to all invoice recipients - @classes = ('invoice') unless @classes; + + # There are two e-mail opt-in flags per contact_email address. + # If neither 'invoice' nor 'message' has been specified, default + # to 'invoice'. + # + # This default supports the legacy behavior of + # send to all invoice recipients + push @classes,'invoice' + unless grep {$_ eq 'invoice' || $_ eq 'message'} @classes; + @to = $cust_main->contact_list_email(@classes); # not guaranteed to produce contacts, but then customers aren't # guaranteed to have email addresses on file. in that case, env_to @@ -625,4 +649,3 @@ L<FS::Record>, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/realestate_location.pm b/FS/FS/realestate_location.pm new file mode 100644 index 000000000..d9cd76a58 --- /dev/null +++ b/FS/FS/realestate_location.pm @@ -0,0 +1,177 @@ +package FS::realestate_location; +use strict; +use warnings; +use Carp qw(croak); + +use base 'FS::Record'; + +use FS::Record qw(qsearchs qsearch); + +=head1 NAME + +FS::realestate_location - Object representing a realestate_location record + +=head1 SYNOPSIS + + use FS::realestate_location; + + $location = new FS::realestate_location \%values; + $location = new FS::realestate_location { + agentnum => 1, + location_title => 'Superdome', + address1 => '1500 Sugar Bowl Dr', + city => 'New Orleans', + state => 'LA', + zip => '70112', + }; + + $error = $location->insert; + $error = $new_loc->replace($location); + $error = $record->check; + + $error = $location->add_unit('Box Seat No. 42'); + @units = $location->units; + +=head1 DESCRIPTION + +An FS::realestate_location object represents a location for one or more +FS::realestate_unit objects. Expected to contain at least one unit, as only +realestate_unit objects are assignable to packages via +L<FS::svc_realestate>. + +FS::realestate_location inherits from FS::Record. + +The following fields are currently supported: + +=over 4 + +=item realestatelocnum + +=item agentnum + +=item location_title + +=item address1 (optional) + +=item address2 (optional) + +=item city (optional) + +=item state (optional) + +=item zip (optional) + +=item disabled + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF (see L<FS::Record>) + +=cut + +sub table {'realestate_location';} + +=item insert (see L<FS::Record>) + +=item delete + + FS::realestate_location records should never be deleted, only disabled + +=cut + +sub delete { + # Once this record has been associated with a customer in any way, it + # should not be deleted. todo perhaps, add a is_deletable function that + # checks if the record has ever actually been used, and allows deletion + # if it hasn't. (entered in error, etc). + croak "FS::realestate_location records should never be deleted"; +} + +=item replace OLD_RECORD (see L<FS::Record>) + +=item check (see L<FS::Record>) + +=item agent + +Returns the associated agent + +=cut + +sub agent { + my $self = shift; + return undef unless $self->agentnum; + return exists $self->{agent} + ? $self->{agent} + : $self->{agent} = qsearchs('agent', {agentnum => $self->agentnum} ); +} + + +=item add_unit UNIT_TITLE + +Create an associated L<FS::realestate_unit> record + +=cut + +sub add_unit { + my ($self, $unit_title) = @_; + croak "add_unit() requires a \$unit_title parameter" unless $unit_title; + + if ( + qsearchs('realestate_unit',{ + realestatelocnum => $self->realestatelocnum, + unit_title => $unit_title, + }) + ) { + return "Unit Title ($unit_title) has already been used for location (". + $self->location_title.")"; + } + + my $unit = FS::realestate_unit->new({ + realestatelocnum => $self->realestatelocnum, + agentnum => $self->agentnum, + unit_title => $unit_title, + }); + my $err = $unit->insert; + die "Error creating FS::realestate_new record: $err" if $err; + + return; +} + + +=item units + +Returns all units associated with this location + +=cut + +sub units { + my $self = shift; + return qsearch( + 'realestate_unit', + {realestatelocnum => $self->realestatelocnum} + ); +} + + +=head1 SUBROUTINES + +=over 4 + +=cut + + + + +=back + +=head1 SEE ALSO + +L<FS::record>, L<FS::realestate_unit>, L<FS::svc_realestate> + +=cut + +1; diff --git a/FS/FS/realestate_unit.pm b/FS/FS/realestate_unit.pm new file mode 100644 index 000000000..d1d1f7fda --- /dev/null +++ b/FS/FS/realestate_unit.pm @@ -0,0 +1,163 @@ +package FS::realestate_unit; +use strict; +use warnings; +use Carp qw(croak); + +use base 'FS::Record'; +use FS::Record qw(qsearch qsearchs); + +=head1 NAME + +FS::realestate_unit - Object representing a realestate_unit record + +=head1 SYNOPSIS + + use FS::realestate_unit; + + $record = new FS:realestate_unit \%values; + $record = new FS::realestate_unit { + realestatelocnum => 42, + agentnum => 1, + unit_title => 'Ste 404', + }; + + $error = $record->insert; + $error = $new_rec->replace($record) + $error = $record->check; + + $location = $record->location; + +=head1 DESCRIPTION + +An FS::realestate_unit object represents an invoicable unit of real estate. +Object may represent a single property, such as a rental house. It may also +represent a group of properties sharing a common address or identifier, such +as a shopping mall, apartment complex, or office building, concert hall. + +A FS::realestate_unit object must be associated with a FS::realestate_location + +FS::realestate_unit inherits from FS::Record. + +The following fields are currently supported: + +=over 4 + +=item realestatenum + +=item realestatelocnum + +=item agentnum + +=item unit_title + +=item disabled + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF (see L<FS::Record>) + +=cut + +sub table {'realestate_unit';} + +=item insert (see L<FS::Record>) + +=item delete + + FS::realestate_unit records should never be deleted, only disabled + +=cut + +sub delete { + # Once this record has been associated with a customer in any way, it + # should not be deleted. todo perhaps, add a is_deletable function that + # checks if the record has ever actually been used, and allows deletion + # if it hasn't. (entered in error, etc). + croak "FS::realestate_unit records should never be deleted"; +} + + +=item replace OLD_RECORD (see L<FS::Record>) + +=item check (see L<FS::Record>) + +=item agent + +Returns the associated agent, if any, for this object + +=cut + +sub agent { + my $self = shift; + return undef unless $self->agentnum; + return qsearchs('agent', {agentnum => $self->agentnum} ); +} + +=item location + + Return the associated FS::realestate_location object + +=cut + +sub location { + my $self = shift; + return $self->{location} if exists $self->{location}; + return $self->{location} = qsearchs( + 'realestate_location', + {realestatelocnum => $self->realestatelocnum} + ); +} + +=back + +=item custnum + +Pull the assigned custnum for this unit, if provisioned + +=cut + +sub custnum { + my $self = shift; + return $self->{custnum} + if $self->{custnum}; + + # select cust_pkg.custnum + # from svc_realestate + # LEFT JOIN cust_svc ON svc_realestate.svcnum = cust_svc.svcnum + # LEFT JOIN cust_pkg ON cust_svc.pkgnum = cust_pkg.pkgnum + # WHERE svc_realestate.realestatenum = $realestatenum + + my $row = qsearchs({ + select => 'cust_pkg.custnum', + table => 'svc_realestate', + addl_from => 'LEFT JOIN cust_svc ON svc_realestate.svcnum = cust_svc.svcnum ' + . 'LEFT JOIN cust_pkg ON cust_svc.pkgnum = cust_pkg.pkgnum ', + extra_sql => 'WHERE svc_realestate.realestatenum = '.$self->realestatenum, + }); + + return + unless $row && $row->custnum; + + return $self->{custnum} = $row->custnum; +} + +=head1 SUBROUTINES + +=over 4 + +=cut + + +=back + +=head1 SEE ALSO + +L<FS::record>, L<FS::realestate_location>, L<FS::svc_realestate> + +=cut + +1; diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index f2456a56f..afd5db64f 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -122,6 +122,15 @@ sub virtual_fields { =item label +Returns a label to identify a record of this service. +Label may be displayed on freeside screens, and within customer bills. + +For example, $obj->label may return: + + - A provisioned phone number for svc_phone + - The mailing list name and e-mail address for svc_mailinglist + - The address of a rental property svc_realestate + svc_Common provides a fallback label subroutine that just returns the svcnum. =cut @@ -1586,4 +1595,3 @@ from the base documentation. =cut 1; - diff --git a/FS/FS/svc_realestate.pm b/FS/FS/svc_realestate.pm new file mode 100644 index 000000000..a7512eef8 --- /dev/null +++ b/FS/FS/svc_realestate.pm @@ -0,0 +1,172 @@ +package FS::svc_realestate; +use base qw(FS::svc_Common); + +use strict; +use warnings; +use vars qw($conf); + +use FS::Record qw(qsearchs qsearch dbh); +use Tie::IxHash; + +$FS::UID::callback{'FS::svc_realestate'} = sub { + $conf = new FS::Conf; +}; + +=head1 NAME + +FS::svc_realestate - Object methods for svc_realestate records + +=head1 SYNOPSIS + + {...} TODO + +=head1 DESCRIPTION + +A FS::svc_realestate object represents a billable real estate trasnaction, +such as renting a home or office. + +FS::svc_realestate inherits from FS::svc_Common. The following fields are +currently supported: + +=over 4 + +=item svcnum - primary key + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Instantiates a new svc_realestate object. + +=cut + +sub table_info { + tie my %fields, 'Tie::IxHash', + svcnum => 'Service', + realestatenum => { + type => 'select-realestate_unit', + label => 'Real estate unit', + }; + + { + name => 'Real estate', + name_plural => 'Real estate services', + longname_plural => 'Real estate services', + display_weight => 100, + cancel_weight => 100, + fields => \%fields, + }; +} + +sub table {'svc_realestate'} + +=item label + +Returns a label formatted as: + <location_title> <unit_title> + +=cut + +sub label { + my $self = shift; + my $unit = $self->realestate_unit; + my $location = $self->realestate_location; + + return $location->location_title.' '.$unit->unit_title + if $unit && $location; + + return $self->svcnum; # shouldn't happen +} + + +=item realestate_unit + +Returns associated L<FS::realestate_unit> + +=cut + +sub realestate_unit { + my $self = shift; + + return $self->get('_realestate_unit') + if $self->get('_realestate_unit'); + + return unless $self->realestatenum; + + my $realestate_unit = qsearchs( + 'realestate_unit', + {realestatenum => $self->realestatenum} + ); + + $self->set('_realestate_unit', $realestate_unit); + $realestate_unit; +} + +=item realestate_location + +Returns associated L<FS::realestate_location> + +=cut + +sub realestate_location { + my $self = shift; + + my $realestate_unit = $self->realestate_unit; + return unless $realestate_unit; + + $realestate_unit->location; +} + +=item cust_svc + +Returns associated L<FS::cust_svc> + +=cut + +sub cust_svc { + qsearchs('cust_svc', { 'svcnum' => $_[0]->svcnum } ); +} + +=item search_sql + +I have an unfounded suspicion this method serves no purpose in this context + +=cut + +# sub search_sql {die "search_sql called on FS::svc_realestate"} + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Delete this record from the database. + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=back 4 + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import new file mode 100755 index 000000000..b5fc226a4 --- /dev/null +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -0,0 +1,222 @@ +#!/usr/bin/perl + +use strict; +use Getopt::Std; +use Date::Format; +use File::Temp 'tempdir'; +use Net::SFTP::Foreign::Compat; +use FS::UID qw(adminsuidsetup datasrc dbh); +use FS::cdr; +use FS::cdr_batch; +use FS::Record qw(qsearch qsearchs); +use Date::Format 'time2str'; +use Date::Parse 'str2time'; + + +### +# parse command line +### + +use vars qw( $opt_d $opt_v $opt_c $opt_s $opt_e $opt_a ); +getopts('dvc:s:e:a'); + +my ($user, $login, $password) = @ARGV; +($user and $login and $password) or die &usage; + +my $dbh = adminsuidsetup $user; +$FS::UID::AutoCommit = 0; + +# index already-downloaded batches +my @previous = qsearch({ + 'table' => 'cdr_batch', + 'hashref' => { 'cdrbatch' => {op=>'like', value=>'ani_networks%'} }, + 'order_by' => 'ORDER BY cdrbatch DESC', +}); +my %exists = map {$_->cdrbatch => 1} @previous; + +my $format = 'ani_networks'; +my $host = 'arkftp.aninetworks.com'; + +### +# get the file list +### + +warn "Retrieving directory listing\n" if $opt_v; + +my $sftp = sftp(); + +## get the current working dir +my $cwd = $sftp->cwd; + +## switch to CDR dir +$sftp->setcwd($cwd . '/CDR') or die "can't chdir to $cwd/CDR\n"; + +my $ls = $sftp->ls('.', wanted => qr/^UYM.*.zip$/i, names_only =>1 ); +my @files = @$ls; + +warn scalar(@files)." CDR files found.\n" if $opt_v; +# apply date range from last downloaded batch. +if ( $opt_a ) { + my $most_recent = $previous[0]; + if ($most_recent) { + if ($most_recent->cdrbatch =~ /^*Daily_(\d+)_/) { + my $date = $1; + warn "limiting to dates >= $date (from most recent batch)\n" if $opt_v; + @files = grep { /^*Daily_(\d+)_/ && $1 >= $date } @files; + } + } +} + +# apply a start date if given +if ( $opt_s ) { + # normalize date format + $opt_s = time2str('%Y%m%d', str2time($opt_s)) if $opt_s =~ /\D/; + warn "limiting to dates > $opt_s\n" if $opt_v; + @files= grep { /^*Daily_(\d+)_/ && $1 >= $opt_s } @files; +} + +# apply a end date if given +if ( $opt_e ) { + # normalize date format + $opt_e = time2str('%Y%m%d', str2time($opt_e)) if $opt_e =~ /\D/; + warn "limiting to dates < $opt_e\n" if $opt_v; + @files= grep { /^*Daily_(\d+)_/ && $1 < $opt_e } @files; +} + +warn scalar(@files) ." files to be downloaded\n" if $opt_v; +foreach my $file (@files) { + + my $tmpdir = tempdir( CLEANUP => $opt_v ); + + warn "downloading $file to $tmpdir\n" if $opt_v; + $sftp = sftp(); + $sftp->get($file, "$tmpdir/$file"); + + ## extract zip file + if(system ("unzip $tmpdir/$file -d $tmpdir") != 0) { + unlink "$tmpdir/$file"; + my $error = "unzip of '$tmpdir/$file' failed\n"; + if ( $opt_s ) { + warn $error; + next; + } else { + die $error; + } + } + + warn "processing $file\n" if $opt_v; + + my $batchname = "$format-$file"; + if ($exists{$batchname}) { + warn "already imported $file\n"; + next; + } + + my $unzipped_file = $file; + $unzipped_file =~ s/.zip/.txt/i; + + warn "going to import file $unzipped_file" if $opt_v; + + my $import_options = { + 'file' => "$tmpdir/$unzipped_file", + 'format' => $format, + 'batch_namevalue' => $batchname, + 'empty_ok' => 1, + }; + $import_options->{'cdrtypenum'} = $opt_c if $opt_c; + + my $error = FS::cdr::batch_import($import_options); + + if ( $error ) { + die "error processing $unzipped_file: $error\n"; + } +} +warn "finished\n" if $opt_v; +$dbh->commit; + +### +# subs +### + +sub usage { + "Usage: \n freeside-cdr-aninetworks-import [ options ] user login password + Options: + -v: be verbose + -d: enable FTP debugging (very noisy) + -c num: apply a cdrtypenum to the imported CDRs + -s date: start date + -e date: end date + -a: automatically choose start date from most recently downloaded batch + +"; +} + +sub sftp { + + #reuse connections + return $sftp if $sftp && $sftp->cwd; + + my %sftp = ( host => $host, + user => $login, + password => $password, + more => [-o => 'StrictHostKeyChecking no'], + ); + + $sftp = Net::SFTP::Foreign->new(%sftp); + $sftp->error and die "SFTP connection failed: ". $sftp->error; + + $sftp; +} + +=head1 NAME + +freeside-cdr-aninetworks-import - Download CDR files from a remote server via SFTP + +=head1 SYNOPSIS + + freeside-cdr-aninetworks-import [ -v ] [ -d ] [ -a ] [ -c cdrtypenum ] + [ -s startdate ] [ -e enddate ] user sftpuser sftppassword + +=head1 DESCRIPTION + +Command line tool to download CDR files from a remote server via SFTP +and then import them into the database. + +-v: be verbose + +-d: enable sftp debugging (very noisy) + +-a: automatically choose start date from most recently downloaded batch + +-c: cdrtypenum to set, defaults to none + +-s: if specified, sets a startdate. startdate starts at 00:00:00 + +-e: if specified, sets a enddate. enddate starts at 00:00:00 so if you wish to include enddate must add one more day. + +user: freeside username + +sftpuser: sftp user for arkftp.aninetworks.com + +sftppassword: password for sftp user + +=head1 EXAMPLES + +freeside-cdr-aninetworks-import -a <freeside user> <sftp login> <sftp password> +will get all cdr files starting from the day of the last day processed. + +freeside-cdr-aninetworks-import -s 20180120 -e 20180121 <freeside user> <sftp login> <sftp password> +will get all cdr files from 01/20/2018 + +freeside-cdr-aninetworks-import -v -s $(date --date="-1 day" +\%Y\%m\%d) -e $(date +\%Y\%m\%d) <freeside user> <sftp login> <sftp password> +will get all cdr files from yesterday + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::cdr> + +=cut + +1;
\ No newline at end of file diff --git a/FS/bin/freeside-cdr-freeswitch b/FS/bin/freeside-cdr-freeswitch index 7f09578d4..3c18ef2a6 100644 --- a/FS/bin/freeside-cdr-freeswitch +++ b/FS/bin/freeside-cdr-freeswitch @@ -5,11 +5,12 @@ use Date::Parse 'str2time'; use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'Pg', - 'database' => 'fusionpbx', - 'table' => 'v_xml_cdr', - 'primary_key' => 'uuid', - 'column_map' => { #freeside => fusionpbx + 'dbd' => 'Pg', + 'database' => 'fusionpbx', + 'table' => 'v_xml_cdr', + 'status_column' => 'freesidestatus', + 'primary_key' => 'uuid', + 'column_map' => { #freeside => fusionpbx #'cdrid' => 'uuid', #Primary key #'' => 'CALL_SESSION_ID', # Call Session Id (unique per call session – GUID) 'uniqueid' => 'uuid', # diff --git a/FS/t/realestate_location.t b/FS/t/realestate_location.t new file mode 100644 index 000000000..ecb1d8be9 --- /dev/null +++ b/FS/t/realestate_location.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::realestate_location; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/realestate_unit.t b/FS/t/realestate_unit.t new file mode 100644 index 000000000..bbecc1a4c --- /dev/null +++ b/FS/t/realestate_unit.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::realestate_unit; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/svc_realestate.t b/FS/t/svc_realestate.t new file mode 100644 index 000000000..4145d8b52 --- /dev/null +++ b/FS/t/svc_realestate.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::svc_realestate; +$loaded=1; +print "ok 1\n"; diff --git a/bin/fakesmtpserver.pl b/bin/fakesmtpserver.pl new file mode 100755 index 000000000..1f2ca3f31 --- /dev/null +++ b/bin/fakesmtpserver.pl @@ -0,0 +1,53 @@ +#!/usr/bin/env perl + +=head1 Fake SMTP Server + +While this script is running, creates an SMTP server at localhost port 25. + +Can only accept one client connection at a time. If necessary, +it could be updated to fork on client connections. + +When an e-mail is delivered, the TO and FROM are printed to STDOUT. +The TO, FROM and MSG are saved to a file in $message_save_dir + +=cut + +use strict; +use warnings; + +use Carp; +use Net::SMTP::Server; +use Net::SMTP::Server::Client; +use Net::SMTP::Server::Relay; + +my $message_save_dir = '/home/freeside/fakesmtpserver'; + +mkdir $message_save_dir, 0777; + +my $server = new Net::SMTP::Server('localhost', 25) || + croak("Unable to handle client connection: $!\n"); + +while(my $conn = $server->accept()) { + my $client = new Net::SMTP::Server::Client($conn) || + croak("Unable to handle client connection: $!\n"); + + $client->process || next; + + open my $fh, '>', $message_save_dir.'/'.time().'.txt' + or die "error: $!"; + + for my $f (qw/TO FROM/) { + + if (ref $client->{$f} eq 'ARRAY') { + print "$f: $_\n" for @{$client->{$f}}; + print $fh "$f: $_\n" for @{$client->{$f}}; + } else { + print "$f: $client->{$f}\n"; + print $fh "$f: $client->{$f}\n"; + } + + } + print $fh "\n\n$client->{MSG}\n"; + print "\n"; + close $fh; +} diff --git a/httemplate/browse/realestate_location.html b/httemplate/browse/realestate_location.html new file mode 100644 index 000000000..be2cd11f8 --- /dev/null +++ b/httemplate/browse/realestate_location.html @@ -0,0 +1,43 @@ +<% include( 'elements/browse.html', + title => emt('Real Estate Locations'), + name => 'real estate locations', + + menubar => [ + 'Edit units' => "${p}browse/realestate_unit.html", + 'Add a new location' => "${p}edit/realestate_location.html", + 'Add a new unit' => "${p}edit/realestate_unit.html", + ], + + query => { table => 'realestate_location' }, + count_query => 'SELECT COUNT(*) FROM realestate_location', + + header => [ 'Location', 'Address', 'Address 2', 'City', 'State', 'Zip' ], + fields => [ + 'location_title', + 'address1', + 'address2', + 'city', + 'state', + 'zip' + ], + links => [ + ["${p}edit/realestate_location.html?", 'realestatelocnum' ], + ], + + agent_virt => 1, + agent_pos => 0, + disableable => 1, +) +%> +<%init> + + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + + + +</%init> diff --git a/httemplate/browse/realestate_unit.html b/httemplate/browse/realestate_unit.html new file mode 100644 index 000000000..399cd2583 --- /dev/null +++ b/httemplate/browse/realestate_unit.html @@ -0,0 +1,70 @@ +<% include( 'elements/browse.html', + title => emt('Real Estate Inventory'), + name => 'real estate inventory', + + menubar => [ + 'Edit locations' => "${p}browse/realestate_location.html", + 'Add a new location' => "${p}edit/realestate_location.html", + 'Add a new unit' => "${p}edit/realestate_unit.html", + ], + + query => { + table => 'realestate_unit', + select => join(', ',qw( + realestate_unit.* + realestate_location.location_title + cust_main.first + cust_main.last + cust_main.company + )), + addl_from => " + LEFT JOIN realestate_location + ON realestate_location.realestatelocnum + = realestate_unit.realestatelocnum + LEFT JOIN svc_realestate + ON realestate_unit.realestatenum = svc_realestate.realestatenum + LEFT JOIN cust_svc + ON svc_realestate.svcnum = cust_svc.svcnum + LEFT JOIN cust_pkg + ON cust_svc.pkgnum = cust_pkg.pkgnum + LEFT JOIN cust_main + ON cust_pkg.custnum = cust_main.custnum + ", + order_by => "ORDER BY location_title, unit_title" + }, + + count_query => 'SELECT COUNT(*) FROM realestate_unit', + + header => [ 'Location', 'Unit', 'Customer' ], + fields => [ + 'location_title', + 'unit_title', + sub { + return '' unless $_[0]->custnum; + return $_[0]->company if $_[0]->company; + return $_[0]->first.' '.$_[0]->last; + }, + ], + links => [ + ["${p}edit/realestate_location.html?", 'realestatelocnum' ], + ["${p}edit/realestate_unit.html?", 'realestatenum' ], + ["${p}view/cust_main.cgi?", 'custnum' ] + ], + + agent_virt => 1, + agent_pos => 0, + disableable => 1, +) +%> +<%init> + + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + + + +</%init> diff --git a/httemplate/docs/part_svc-table.html b/httemplate/docs/part_svc-table.html index 820d0b9cc..56a4d0e8c 100644 --- a/httemplate/docs/part_svc-table.html +++ b/httemplate/docs/part_svc-table.html @@ -39,6 +39,7 @@ <TR> <TH ALIGN="left">Hosting</TH> <TH ALIGN="left">Colocation</TH> + <TH ALIGN="left">Real Estate</TH> </TR> <TD VALIGN="top"> <UL STYLE="margin:0"> @@ -54,6 +55,11 @@ <LI><B>svc_port</B>: Customer router/switch port </UL> </TD> + <TD VALIGN="top"> + <UL STYLE="margin:0"> + <LI><B>svc_realestate</B>: Real estate properties + </UL> + </TD> </TR> <TABLE> <!-- <LI>svc_charge - One-time charges (Partially unimplemented) @@ -62,4 +68,3 @@ </BODY> </HTML> - diff --git a/httemplate/edit/process/cust_main-contacts.html b/httemplate/edit/process/cust_main-contacts.html index 2a7185b5c..5b8319f5a 100644 --- a/httemplate/edit/process/cust_main-contacts.html +++ b/httemplate/edit/process/cust_main-contacts.html @@ -1,3 +1,11 @@ +<%doc> + + This form works indirectly with the tables contact_email and + contact_phone. The columns are updated against an FS::contact + object. The insert/update methods of FS::contact will make the + necessary inserts/updates to contact_email and contact_phone. + +</%doc> <% include('elements/process.html', 'table' => 'cust_main', 'error_redirect' => popurl(3). 'edit/cust_main-contacts.html?', diff --git a/httemplate/edit/process/realestate_location.html b/httemplate/edit/process/realestate_location.html new file mode 100644 index 000000000..ab5cf230f --- /dev/null +++ b/httemplate/edit/process/realestate_location.html @@ -0,0 +1,14 @@ +<% include( 'elements/process.html', + table => 'realestate_location', + viewall_dir => 'browse', + ) +%> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/process/realestate_unit.html b/httemplate/edit/process/realestate_unit.html new file mode 100644 index 000000000..ba9b5dc92 --- /dev/null +++ b/httemplate/edit/process/realestate_unit.html @@ -0,0 +1,13 @@ +<& elements/process.html, + 'table' => 'realestate_unit', + 'viewall_dir' => 'browse', +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/realestate_location.html b/httemplate/edit/realestate_location.html new file mode 100644 index 000000000..34344e98f --- /dev/null +++ b/httemplate/edit/realestate_location.html @@ -0,0 +1,72 @@ +<% include( 'elements/edit.html', + 'name_singular' => 'Real Estate Location', + 'table' => 'realestate_location', + + 'labels' => { + realestatelocnum => 'Location', + location_title => "Location", + address1 => "Address", + address2 => "Address", + city => "City", + state => "State", + zip => "Zip-Code", + disabled => "Disabled", + }, + 'fields' => [ + { field => 'realestatelocnum', type => 'hidden' }, + + { field => 'location_title', + type=>'text', + size => 40, + maxlength => 80, + }, + { field => 'address1', + type=>'text', + size => 40, + maxlength => 80, + }, + { field => 'address2', + type=>'text', + size => 40, + maxlength => 80, + }, + { field => 'city', + type=>'text', + size => 40, + maxlength => 80, + }, + { field => 'state', + type=>'text', + size => 40, + maxlength => 80, + }, + { field => 'zip', + type=>'text', + size => 5, + maxlength => 5, + }, + + { field => 'agentnum', + type => 'select-agent', + value => 1, + }, + { field => 'disabled', + type=>'checkbox', + value=>'Y' + }, + ], + + 'viewall_dir' => 'browse', + 'agent_virt' => 1, +) +%> + +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/realestate_unit.html b/httemplate/edit/realestate_unit.html new file mode 100644 index 000000000..a7ca72dd9 --- /dev/null +++ b/httemplate/edit/realestate_unit.html @@ -0,0 +1,48 @@ +<% include( 'elements/edit.html', + 'name_singular' => 'Real Estate Unit', + 'table' => 'realestate_unit', + + 'labels' => { + realestatenum => 'Ref No', + unit_title => 'Unit Title', + agentnum => 'Agent', + realestatelocnum => 'Location', + }, + 'fields' => [ + { field => 'realestatenum', type => 'hidden' }, + + { field => 'unit_title', + type=>'text', + size => 40, + }, + { field => 'realestatelocnum', + type => 'select-realestate_location', + + # possible todo: + # I'd like to have this field disabled for editing on existing records, + # and only show the full selectbox for new records. + + }, + { field => 'agentnum', + type => 'select-agent', + }, + { field => 'disabled', + type=>'checkbox', + value=>'Y' + }, + ], + + 'viewall_dir' => 'browse', + 'agent_virt' => 1, +) +%> + +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + +</%init> diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html index faee7ead4..43e520155 100644 --- a/httemplate/elements/contact.html +++ b/httemplate/elements/contact.html @@ -42,7 +42,8 @@ % $value = join(', ', map $_->emailaddress, $contact->contact_email); % } elsif ( $field eq 'selfservice_access' % or $field eq 'comment' -% or $field eq 'invoice_dest' ) { +% or $field eq 'invoice_dest' +% or $field eq 'message_dest' ) { % $value = $X_contact->get($field); % } else { % $value = $contact->get($field); @@ -78,7 +79,7 @@ return false } </SCRIPT> -% } elsif ( $field eq 'invoice_dest' ) { +% } elsif ( $field eq 'invoice_dest' || $field eq 'message_dest' ) { % my $curr_value = $cgi->param($name . '_' . $field); % $curr_value = $value if !defined($curr_value); <& select.html, @@ -168,6 +169,7 @@ tie my %label, 'Tie::IxHash', unless ($opt{'for_prospect'}) { $label{'invoice_dest'} = 'Send invoices'; + $label{'message_dest'} = 'Send messages'; $label{'selfservice_access'} = 'Self-service'; } diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 3b3d244db..eb065b668 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -418,6 +418,8 @@ if( $curuser->access_right('Financial reports') ) { $report_financial{'Customer Accounting Summary'} = [ $fsurl.'search/report_customer_accounting_summary.html', 'Customer accounting summary report' ]; + $report_financial{'Upcoming Auto-Bill Transactions'} = [ $fsurl.'search/report_future_autobill.html', 'Upcoming auto-bill transactions' ]; + } elsif($curuser->access_right('Receivables report')) { $report_financial{'A/R Aging'} = [ $fsurl.'search/report_receivables.html', 'Accounts Receivable Aging report' ]; @@ -835,6 +837,11 @@ $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_cla || $curuser->access_right('Edit global inventory') || $curuser->access_right('Configuration'); +$config_misc{'Real estate inventory'} = [ $fsurl.'browse/realestate_unit.html', 'Setup real estate inventory' ] + if $curuser->access_right('Edit realestate inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + $config_misc{'Upload targets'} = [ $fsurl.'browse/upload_target.html', 'Billing and payment upload destinations' ] if $curuser->access_right('Configuration'); @@ -1039,4 +1046,3 @@ sub submenu { } </%init> - diff --git a/httemplate/elements/select-multiple-contact_class.html b/httemplate/elements/select-multiple-contact_class.html new file mode 100644 index 000000000..81a71cc25 --- /dev/null +++ b/httemplate/elements/select-multiple-contact_class.html @@ -0,0 +1,21 @@ +<%doc> + +Display a multi-select box containing all Email Types listed in +the contact_class table. + +NOTE: + Don't confuse "Contact Type" (contact_email.classnum) with + "Customer Class" (cust_main.classnum) + +</%doc> +<% include( '/elements/select-table.html', + table => 'contact_class', + hashref => { disabled => '' }, + name_col => 'classname', + field => 'classnum', + pre_options => [ 0 => '(No Type)' ], + multiple => 1, + all_selected => 1, + @_, + ) +%> diff --git a/httemplate/elements/select-realestate_location.html b/httemplate/elements/select-realestate_location.html new file mode 100644 index 000000000..001ed3e89 --- /dev/null +++ b/httemplate/elements/select-realestate_location.html @@ -0,0 +1,32 @@ +<%doc> + + Displays a selectbox populated with values from realestate_location. + key: realestate_location.realestatenum + value: realestate_location.location_title + +</%doc> + +<% include( '/elements/select-table.html', + %opt, + table => 'realestate_location', + name_col => 'location_title', + hashref => { 'disabled' => '' }, + value => $select_value, + disable_empty => 1, + ) +%> + +<%init> + +# +# possible todo: +# I'd like to change the behavior of this select based on if +# a new item is being created, or an existing item being edited + +my %opt = @_; +my $select_value = $opt{'curr_value'} || $opt{'value'}; + +# use Data::Dumper qw(Dumper); +# print Dumper(\%opt); + +</%init> diff --git a/httemplate/elements/select-realestate_unit.html b/httemplate/elements/select-realestate_unit.html new file mode 100644 index 000000000..e189d5d99 --- /dev/null +++ b/httemplate/elements/select-realestate_unit.html @@ -0,0 +1,59 @@ +<%doc> + +Display a pair of select boxes for provisioning a realestate_unit +- Real Estate Location +- Real Estate Unit + +NOTE: + Records are always suppresed if + - realestate_location.disabled is set + - realestate_unit is provisioned to a customer [not working] + + If it becomes necessary, an option may be added to the template + to show disabled/provisioned records, but is not yet implemented + +</%doc> +<& select-tiered.html, + 'tiers' => [ + { + + field => 'realestate_location', + table => 'realestate_location', + extra_sql => "WHERE realestate_location.disabled IS NULL " + . " OR realestate_location.disabled = '' ", + name_col => 'location_title', + empty_label => '(all)', + }, + { + field => 'realestatenum', + table => 'realestate_unit', + name_col => 'unit_title', + value_col => 'realestatenum', + link_col => 'realestatelocnum', + + # TODO: Filter units assigned to customers + # SQL below breaks the selectbox... why? + + # Also, can we assume if realestatenum doesn't appear in svc_realestate + # that the realestate_unit is unprovisioned to a customer? What indicator + # should be used to determine when a realestae_unit is not provisioned? + + # addl_from => " + # LEFT JOIN svc_realestate + # ON svc_realestate.realestatenum = realestate_unit.realestatenum + # ", + + #extra_sql => "WHERE svc_realestate.svcnum IS NULL ", + + disable_empty => 1, + debug => 1, + }, + ], + %opt, + 'prefix' => $opt{'prefix'}. $opt{'field'}. '_', #after %opt so it overrides +&> +<%init> + +my %opt = @_; + +</%init> diff --git a/httemplate/elements/tr-checkbox-multiple.html b/httemplate/elements/tr-checkbox-multiple.html index 4d754b007..baf18f916 100644 --- a/httemplate/elements/tr-checkbox-multiple.html +++ b/httemplate/elements/tr-checkbox-multiple.html @@ -1,3 +1,23 @@ +<%doc> + +Display a <tr> containing multiple checkboxes + +USAGE: + +<& /elements/tr-checkbox-multipe.html, + label => emt('Label'), + field => 'field_name', + options => ['opt1', 'opt2'], + labels => { + opt1 => 'Option 1', + opt2 => 'Option 2', + }, + value => { + opt2 => '1', # opt2 defaults as checked + } +&> + +</%doc> <% include('tr-td-label.html', @_ ) %> <TD <% $style %>> diff --git a/httemplate/elements/tr-select-multiple-contact_class.html b/httemplate/elements/tr-select-multiple-contact_class.html new file mode 100644 index 000000000..5de129324 --- /dev/null +++ b/httemplate/elements/tr-select-multiple-contact_class.html @@ -0,0 +1,32 @@ +<%doc> + + Displays Contact Types as a multi-select box. + + If no non-disabled Contact Types have been defined in contact_class table, + renders a hidden input field with a blank value. + +</%doc> + +% if ($has_types) { +<TR> + <TD ALIGN="right"><% $opt{'label'} || emt('Contact Type') %></TD> + <TD> + <% include( '/elements/select-multiple-contact_class.html', %opt ) %> + </TD> +</TR> +% } else { +<INPUT TYPE="hidden" NAME="<% $opt{field} %>" VALUE=""> +% } + +<%init> + +my %opt = @_; +$opt{field} ||= $opt{element_name} ||= 'classnum'; + +my $has_types =()= qsearch({ + table => 'contact_class', + hashref => { disabled => '' }, + extra_sql => ' LIMIT 1 ', +}); + +</%init> diff --git a/httemplate/elements/tr-select-realestate_location.html b/httemplate/elements/tr-select-realestate_location.html new file mode 100644 index 000000000..1367886ed --- /dev/null +++ b/httemplate/elements/tr-select-realestate_location.html @@ -0,0 +1,17 @@ +<TR> + <TH ALIGN="right"><% $opt{'label'} || 'Real Estate Location' %></TD> + <TD> + <% include( '/elements/select-realestate_location.html', + 'curr_value' => $curr_value, + %opt + ) + %> + </TD> +</TR> + +<%init> + +my %opt = @_; +my $curr_value = $opt{'curr_value'} || $opt{'value'}; + +</%init> diff --git a/httemplate/elements/tr-select-realestate_unit.html b/httemplate/elements/tr-select-realestate_unit.html new file mode 100644 index 000000000..b1a4296d4 --- /dev/null +++ b/httemplate/elements/tr-select-realestate_unit.html @@ -0,0 +1,5 @@ +<& tr-td-label.html, @_ &> +<td> +<& select-realestate_unit.html, @_ &> +</td> +</tr> diff --git a/httemplate/misc/cust_main-import_charges.cgi b/httemplate/misc/cust_main-import_charges.cgi index 4eacce13a..215cc4c9d 100644 --- a/httemplate/misc/cust_main-import_charges.cgi +++ b/httemplate/misc/cust_main-import_charges.cgi @@ -28,9 +28,9 @@ Import a CSV file containing customer charges. <TH ALIGN="right">Format</TH> <TD> <SELECT NAME="format"> - <OPTION VALUE="simple">Simple - <OPTION VALUE="ooma">Ooma -<!-- <OPTION VALUE="extended" SELECTED>Extended --> +% foreach my $format ( keys %formats ) { + <OPTION VALUE="<% $format %>"><% $formats{$format} %></OPTION> +% } </SELECT> </TD> </TR> @@ -94,6 +94,8 @@ Field information: die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Import'); + tie my %formats, 'Tie::IxHash', FS::cust_main::Import_Charges->import_formats; + my $custbatch = time2str('webimport-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time); </%init> diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index fe637abe1..f52c6b36a 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -171,16 +171,18 @@ Template: <TD>Send to contacts:</TD> <TD> <div id="contactclassesdiv"> - <& /elements/checkboxes.html, - 'style' => 'display: inline; vertical-align: top', - 'disable_links' => 1, - 'names_list' => \@contact_checkboxes, - 'element_name_prefix' => 'contact_class_', - 'checked_callback' => sub { - my($cgi, $name) = @_; - $name eq 'invoice' #others default to unchecked - }, - &> + <& /elements/checkboxes.html, + 'style' => 'display: inline; vertical-align: top', + 'disable_links' => 1, + 'names_list' => \@optin_checkboxes, + 'element_name_prefix' => 'contact_class_', + 'checked_callback' => sub { + # Called for each checkbox + # Return true to default as checked, false as unchecked + my($cgi, $name) = @_; + exists $dest_ischecked{$name}; + }, + &> </div> % if ($send_to_domain) { <div> @@ -197,6 +199,27 @@ Template: </div> % } </TD> +% if (@active_classes) { +</tr> +<tr> +<TD>Contact Type:</TD> +<TD> + <div id="contactclassesdiv"> + <& /elements/checkboxes.html, + 'style' => 'display: inline; vertical-align: top', + 'disable_links' => 1, + 'names_list' => \@classnum_checkboxes, + 'element_name_prefix' => 'contact_class_', + 'checked_callback' => sub { + # Called for each checkbox + # Return true to default as checked, false as unchecked + my($cgi, $name) = @_; + exists $classnum_ischecked{$name}; + }, + &> + </div> +</TD> +% } </TR> </TABLE> <BR> @@ -340,6 +363,21 @@ if ( !$cgi->param('preview') ) { } else { + my @checked_email_dest; + my @checked_contact_type; + for ($cgi->param) { + if (/^contact_class_(.+)$/) { + my $f = $1; + if ($f eq 'invoice' || $f eq 'message') { + push @checked_email_dest, $f; + } elsif ( $f =~ /^\d+$/ ) { + push @checked_contact_type, $f; + } + } + } + $search{with_email_dest} = \@checked_email_dest if @checked_email_dest; + $search{with_contact_type} = \@checked_contact_type if @checked_contact_type; + my $sql_query = "FS::$table"->search(\%search); my $count_query = delete($sql_query->{'count_query'}); my $count_sth = dbh->prepare($count_query) @@ -389,6 +427,8 @@ if ( !$cgi->param('preview') ) { $sql_query->{'select'} = "$table.*"; $sql_query->{'order_by'} = ''; my $object = qsearchs($sql_query); + # Could use better error handling here... + die "No customers match the search criteria" unless ref $object; $cust = $object->cust_main; my %msgopts = ( 'cust_main' => $cust, @@ -422,24 +462,59 @@ if ( !$cgi->param('preview') ) { push @contact_classnum, $1; if ( $1 eq 'invoice' ) { push @contact_classname, 'Invoice recipients'; + } elsif ( $1 eq 'message' ) { + push @contact_classname, 'Message recipients'; } else { my $contact_class = FS::contact_class->by_key($1); - push @contact_classname, encode_entities($contact_class->classname); + push @contact_classname, encode_entities( + $contact_class ? $contact_class->classname : '(none)' + ); } } } } } -my @contact_checkboxes = ( - [ 'invoice' => { label => 'Invoice recipients' } ] -); +# Build data structures for "Opt In" and "Contact Type" checkboxes +# +# By default, message recipients will be selected, this is a message. +# By default, all Contact Types will be selected, but this may be +# overridden by passing 'classnums' get/post values. If no contact +# types have been defined, the option will not be presented. + +my @active_classes = qsearch(contact_class => {disabled => ''} ); + +my %classnum_ischecked; +my %dest_ischecked; -foreach my $class (qsearch('contact_class', { disabled => '' })) { - push @contact_checkboxes, [ - $class->classnum, - { label => $class->classname } - ]; +$CGI::LIST_CONTEXT_WARN = 0; +if ( my @in_classnums = $cgi->param('classnums') ) { + # Set checked boxes from form input + for my $v (@in_classnums) { + + if ( $v =~ /^\d+$/ ) { + $classnum_ischecked{$v} = 1 + } elsif ( $v =~ /^(invoice|message)$/ ) { + $dest_ischecked{$v} = 1; + } + + } +} else { + # Checked boxes default values + $classnum_ischecked{$_->classnum} = 1 for @active_classes; + $classnum_ischecked{0} = 1; } +# At least one destination is required +$dest_ischecked{message} = 1 unless %dest_ischecked; + +my @optin_checkboxes = ( + [ 'message' => { label => 'Message recipients' } ], + [ 'invoice' => { label => 'Invoice recipients' } ], +); +my @classnum_checkboxes = ( + [ '0' => { label => '(None)' }], + map { [ $_->classnum => {label => $_->classname} ] } @active_classes, +); + </%init> diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html index 75e57958f..1b18d2ec8 100644 --- a/httemplate/pref/pref-process.html +++ b/httemplate/pref/pref-process.html @@ -1,9 +1,15 @@ % if ( $error ) { % $cgi->param('error', $error); -<% $cgi->redirect(popurl(1). "pref.html?". $cgi->query_string ) %> + <% $cgi->redirect(popurl(1). "pref.html?". $cgi->query_string ) %> % } else { -<% include('/elements/header.html', mt('Preferences updated')) %> -<% include('/elements/footer.html') %> + <% $cgi->redirect( -uri => popurl(1). "pref.html", + -cookie => CGI::Cookie->new( + -name => 'freeside_status', + -value => mt('Preferences updated'), + -expires => '+5m', + ), + ) + %> % } <%init> diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index abd1ea57f..56fde6d44 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -1,5 +1,7 @@ <& /elements/header.html, mt('Preferences for [_1]', $FS::CurrentUser::CurrentUser->username) &> +% my $js_form_validate = { 'pref_form' => { 'name' => 'pref_form' } }; + <FORM METHOD="POST" NAME="pref_form" ACTION="pref-process.html"> <& /elements/error.html &> @@ -143,10 +145,14 @@ </TD> </TR> +% my $validate_field_cve = 'customer_view_emails'; +% $js_form_validate->{pref_form}->{validate_fields}{$validate_field_cve} = 'digits: true'; +% $js_form_validate->{pref_form}->{error_message}{$validate_field_cve} = 'Please only enter numbers here.'; + <TR> <TH ALIGN="right"><% emt("How many recent outbound emails to show in customer view") %></TH> <TD ALIGN="left" COLSPAN=2> - <INPUT TYPE="text" NAME="customer_view_emails" VALUE="<% $curuser->option('customer_view_emails') %>"></TD> + <INPUT TYPE="text" ID="<% $validate_field_cve %>" NAME="<% $validate_field_cve %>" VALUE="<% $curuser->option('customer_view_emails') %>"></TD> </TD> </TR> @@ -260,7 +266,11 @@ <INPUT TYPE="submit" VALUE="<% emt("Update preferences") %>"> -<&/elements/footer.html &> +% my %footerdata = ( +% 'formvalidation' => $js_form_validate, +% ); +<% include("/elements/footer.html", %footerdata) %> + <%init> my $curuser = $FS::CurrentUser::CurrentUser; diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index 5f02fef2f..aaa591cf4 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -1,126 +1,258 @@ <& elements/search.html, - title => 'Contacts', + title => emt('Contacts'), name_singular => 'contact', - query => { select => join(', ', @select), - table => 'contact', - addl_from => $addl_from, - hashref => \%hash, - extra_sql => $extra_sql, - }, - count_query => "SELECT COUNT(*) FROM contact $addl_from $extra_sql", #XXX - header => \@header, - fields => \@fields, - links => \@links, + query => { + select => join(', ', @select), + table => $link, + addl_from => $addl_from, + hashref => {}, + extra_sql => "WHERE $extra_sql", + order_by => "ORDER BY contact_last,contact_first,contact_email_emailaddress" + }, + count_query => " + SELECT COUNT(*) + FROM $link + $addl_from + WHERE $extra_sql + ", + header => \@header, + fields => \@fields, + links => \@links, + html_init => $send_email_link, + agent_virt => 1, + agent_pos => 11, &> <%init> die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('List contacts'); -my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away -push @select, map "contact.$_", qw( first last title ); -my %hash = (); -my $addl_from = ''; +my $DEBUG = 0; -my $email_sub = sub { - my $contact = shift; - #can't because contactnum is in the wrong field #my @contact_email = $contact->contact_email; - my @contact_email = qsearch('contact_email', { 'contactnum' => $contact->contact_contactnum } ); - join(', ', map $_->emailaddress, @contact_email); -}; +# Catch classnum values from multi-select box +# A classnum of 0 indicates to include rows where classnum IS NULL +$CGI::LIST_CONTEXT_WARN = 0; +my @classnum = grep{ /^\d+$/ && $_ > 0 } $cgi->param('classnum'); +my $classnum_null = grep{ $_ eq 0 } $cgi->param('classnum'); -my $work_phone_sub = sub { - my $contact = shift; - my $phone_type = qsearchs('phone_type', { 'typename' => 'Work' }); - #can't because contactnum is in the wrong field - my @contact_workphone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); - join(', ', map $_->phonenum, @contact_workphone); -}; +# Catch destination values from dest multi-checkbox, default to message +# irrelevant to prospect contacts +my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest'); +@dest = ('message') unless @dest; -my $mobile_phone_sub = sub { - my $contact = shift; - my $phone_type = qsearchs('phone_type', { 'typename' => 'Mobile' }); - #can't because contactnum is in the wrong field - my @contact_mobilephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); - join(', ', map $_->phonenum, @contact_mobilephone); -}; +# Cache the contact_class table +my %classname = + map {$_->classnum => $_->classname} + qsearch(contact_class => {disabled => ''}); -my $home_phone_sub = sub { - my $contact = shift; - my $phone_type = qsearchs('phone_type', { 'typename' => 'Home' }); - #can't because contactnum is in the wrong field - my @contact_homephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); - join(', ', map $_->phonenum, @contact_homephone); -}; +# This data structure is used to generate the sql query parameters +my %colmap = ( + # These are included regardless of which tables we're viewing + common => { + cols => { + contact => [qw/first last title contactnum/], + contact_email => [qw/emailaddress/], + }, + joinsql => "", + }, -my $link; #for closure in this sub, we'll define it later -my $contact_classname_sub = sub { - my $contact = shift; - my %hash = ( 'contactnum' => $contact->contact_contactnum ); - my $X_contact; - if ( $link eq 'cust_main' ) { - $X_contact = qsearchs('cust_contact', { %hash, 'custnum' => $contact->custnum } ); - } elsif ( $link eq 'prospect_main' ) { - $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } ); - } else { - die 'guru meditation #5555'; - } - $X_contact->contact_classname; -}; + # These are included if we're viewing customer records + cust_main => { + cols => { + cust_main => [qw/first last company/], + cust_contact => [qw/ + custnum classnum invoice_dest message_dest selfservice_access comment + /], + }, + joinsql => " + LEFT JOIN cust_contact + ON (cust_main.custnum = cust_contact.custnum) + LEFT JOIN contact + on (cust_contact.contactnum = contact.contactnum) + LEFT JOIN contact_email + ON (cust_contact.contactnum = contact_email.contactnum) + ", + }, -my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type' ); -my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub ); -my @links = ( '', '', '', '', '', '', '', '', ); + # These are included if we're viewing prospect records + prospect_main => { + cols => { + prospect_main => [qw/company/], + prospect_contact => [qw/prospectnum classnum comment/], + }, + joinsql => " + LEFT JOIN prospect_contact + ON (prospect_main.prospectnum = prospect_contact.prospectnum) + LEFT JOIN contact + on (prospect_contact.contactnum = contact.contactnum) + LEFT JOIN contact_email + ON (prospect_contact.contactnum = contact_email.contactnum) + ", + }, +); -my $company_link = ''; +my @select; +my $addl_from; +my $extra_sql; +my $hashref; +my $link = $cgi->param('link'); # cust_main or prospect_main -if ( $cgi->param('selfservice_access') eq 'Y' ) { - $hash{'selfservice_access'} = 'Y'; -} +push @select,'agentnum'; + +# this shouldn't happen without funny-busines +die "Invalid \$link type ($link)" + unless $link eq 'cust_main' || $link eq 'prospect_main'; -my $extra_sql = ''; -$link = $cgi->param('link'); -if ( $link ) { - - my $as = ') AS prospect_or_customer'; - - if ( $link eq 'cust_main' ) { - push @header, 'Customer'; - push @select, - "COALESCE( cust_main.company, cust_main.first||' '||cust_main.last $as", - map "cust_contact.$_", qw( custnum classnum comment selfservice_access ); - $addl_from = - ' LEFT JOIN cust_contact USING ( contactnum ) '. - ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )'; - $extra_sql = ' cust_contact.custnum IS NOT NULL '; - $company_link = [ $p.'view/cust_main.cgi?', 'custnum' ]; - } elsif ( $link eq 'prospect_main' ) { - push @header, 'Prospect'; - push @select, - "COALESCE( prospect_main.company, contact.first||' '||contact.last $as", - map "prospect_contact.$_", qw( prospectnum classnum comment ); - $addl_from = - ' LEFT JOIN prospect_contact USING ( contactnum ) '. - ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )'; - $extra_sql = ' prospect_contact.prospectnum IS NOT NULL '; - $company_link = [ $p.'view/prospect_main.html?', 'prospectnum' ]; - } else { - die "don't know how to report on contacts linked to specified table"; +# Build @select and $addl_from +for my $key ('common', $link) { + $addl_from .= $colmap{$key}->{joinsql}; + my $cols = $colmap{$key}->{cols}; + for my $tbl (keys %{$cols}) { + push @select, map{ "$tbl.$_ AS ${tbl}_$_" } @{$cols->{$tbl}}; } +} - #because right now its harder to show it for both kinds of contacts - push @fields, 'prospect_or_customer'; - push @links, $company_link; +# Filter for Contact Type +if (@classnum || $classnum_null) { + my @stm; + my $tbl = $link eq 'cust_main' ? 'cust_contact' : 'prospect_contact'; + push @stm, "${tbl}.classnum IN (".join(',',@classnum).')' if @classnum; + push @stm, "${tbl}.classnum IS NULL" if $classnum_null; + $extra_sql .= " (" . join(' OR ',@stm) . ') '; +} +# Filter for destination +if (@dest && $link eq 'cust_main') { + my @stm; + push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest; + $extra_sql .= "\nAND (".join(' OR ',@stm).') '; } -push @header, 'Self-service'; -push @fields, 'selfservice_access'; +if ($DEBUG) { + print "<pre>\n"; + print "select \n"; + print join ",\n",@select; + print "\n"; + print "from $link \n"; + print "$addl_from\n"; + print "WHERE \n $extra_sql\n"; + print "</pre>\n"; +} + +# Prepare to display phone numbers +# adds 3 additional queries per table record :-( +my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/); +my %phoneid = (qw/Work 1 Home 2 Mobile 3 Fax 4/); +my $get_phone_sub = sub { + my $type = shift; + return sub { + my $rec = shift; + my @p = qsearch('contact_phone', { + contactnum => $rec->contact_contactnum, + phonetypenum => $phoneid{$type} + }); + @p ? (join ', ',map{$_->phonenum} @p) : undef; + }; +}; -push @header, 'Comment'; -push @fields, 'comment'; +# Cache contact types +my %classname = + map {$_->classnum => $_->classname} + qsearch(contact_class => {disabled => ''}); -$extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql - if $extra_sql; +# And now for something completly different: +my @report = ( + { label => 'First', field => sub { shift->contact_first }}, + { label => 'Last', field => sub { shift->contact_last }}, + { label => 'Title', field => sub { shift->contact_title }}, + { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }}, + { label => 'Work Phone', field => $get_phone_sub->('Work') }, + { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') }, + { label => 'Home Phone', field => $get_phone_sub->('Home') }, + { label => 'Type', + field => sub { + my $rec = shift; + if ($rec->cust_contact_custnum) { + return $rec->cust_contact_classnum + ? $classname{$rec->cust_contact_classnum} + : undef; + } else { + return $rec->prospect_contact_classnum + ? $classname{$rec->prospect_contact_classnum} + : undef; + } + }}, + { label => 'Send Invoices', + field => sub { + my $rec = shift; + return 'N/A' if $rec->prospect_contact_prospectnum; + $rec->cust_contact_invoice_dest ? 'Y' : 'N'; + }}, + { label => 'Send Messages', + field => sub { + my $rec = shift; + return 'N/A' if $rec->prospect_contact_prospectnum; + $rec->cust_contact_message_dest ? 'Y' : 'N'; + }}, + { label => 'Customer', + link => sub { + my $rec = shift; + $rec->cust_main_custnum + ? ["${p}view/cust_main.cgi?", 'cust_main_custnum' ] + : ["${p}view/prospect_main.html?", 'prospect_main_prospectnum' ]; + }, + field => sub { + my $rec = shift; + if ($rec->prospect_contact_prospectnum) { + return $rec->contact_company + || $rec->contact_last.' '.$rec->contact_first; + } + $rec->cust_main_company || $rec->cust_main_last.' '.$rec->cust_main_first; + }}, + { label => 'Self-service', + field => sub { + my $rec = shift; + return 'N/A' if $rec->prospect_contact_prospectnum; + $rec->cust_contact_selfservice_access ? 'Y' : 'N'; + }}, + { label => 'Comment', + field => sub { + my $rec = shift; + $rec->prospect_contact_prospectnum + ? $rec->prospect_contact_comment + : $rec->cust_contact_comment; + }}, +); + +my (@header, @fields, @links); +for my $col (@report) { + push @header, emt($col->{label}); + push @fields, $col->{field}; + push @links, ($col->{link} || ""); +} + +my $classnum_url_part; +if (@classnum) { + $classnum_url_part = join '', map{ "&classnums=$_" } @classnum, @dest; + $classnum_url_part .= '&classnums=0' if $classnum_null; +} + +# E-mail pipeline, from email-customers.html through to email queue job, +# doesn't support cust_prospect table +my $send_email_link = undef; +if ($link eq 'cust_main') { + $send_email_link = + "<a href=\"${fsurl}misc/email-customers.html?". + 'table=cust_main'. + '&agentnum='.$cgi->param('agentnum'). + '&POST=on'. + '&all_pkg_classnums=0'. + '&all_tags=0'. + '&any_pkg_status=0'. + '&refnum=1'. + '&with_email=on'. + $classnum_url_part. + "\">Email a notice to these customers</a>"; +} </%init> diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 30162506f..0a43a82dd 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -140,8 +140,14 @@ my $menubar = []; if ( $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices') ) { + # URI::query_from does not support hashref + # results in: ...&contacts=HASH(0x55e16cb81da8)&... + my %query_hash = %search_hash; + delete $query_hash{contacts} + if exists $query_hash{contacts} && ref $query_hash{contacts}; + my $uri = new URI; - $uri->query_form( \%search_hash ); + $uri->query_form( \%query_hash ); my $query = $uri->query; push @$menubar, emt('Email a notice to these customers') => diff --git a/httemplate/search/elements/grid-report.html b/httemplate/search/elements/grid-report.html index 98e81785f..b1e543012 100644 --- a/httemplate/search/elements/grid-report.html +++ b/httemplate/search/elements/grid-report.html @@ -161,7 +161,7 @@ as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR> .shaded { background-color: #c8c8c8; } .totalshaded { background-color: #bfc094; } </style> -<table class="report" width="100%" cellspacing=0> +<table class="<% $table_class %>" width="<% $table_width %>" cellspacing=0> % foreach my $rowinfo (@rows) { <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>> % my $thisrow = shift @cells; @@ -172,7 +172,11 @@ as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR> % $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1; % $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1; % $style .= ' class="' . $cell->{class} . '"' if $cell->{class}; +% if ($cell->{bypass_filter}) { + <<%$td%><%$style%>><% $cell->{value} %></<%$td%>> +% } else { <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>> +% } % } </tr> % } @@ -186,4 +190,6 @@ $title @cells $head => '' $foot => '' +$table_width => "100%" +$table_class => "report" </%args> diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index 5762afbe5..18b4c0ec5 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -119,7 +119,11 @@ Example: #(query needs to be a qsearch hashref and # header & fields need to be defined) - #handling agent virtualization + # Agent Virtualization parameters: + # In this context, only available if your selected table has agentnum. + # You must also include agentnum as a SELECT column in your SQL query, + # or experience non-obvious problems + # 'agent_virt' => 1, # set true if this search should be # agent-virtualized 'agent_null' => 1, # set true to view global records always diff --git a/httemplate/search/future_autobill.html b/httemplate/search/future_autobill.html new file mode 100644 index 000000000..711a25f82 --- /dev/null +++ b/httemplate/search/future_autobill.html @@ -0,0 +1,189 @@ +<%doc> + +Report listing upcoming auto-bill transactions + +Spec requested the ability to run this report with a longer date range, +and see which charges will process on which day. Checkbox multiple_billing_dates +enables this functionality. + +Performance: +This is a dynamically generated report. The time this report takes to run +will depends on the number of customers. Installations with a high number +of auto-bill customers may find themselves unable to run this report +because of browser timeout. Report could be implemented as a queued job if +necessary, to solve the performance problem. + +</%doc> +<& elements/grid-report.html, + title => 'Upcoming auto-bill transactions', + rows => \@rows, + cells => \@cells, + table_width => "", + table_class => 'gridreport', + head => ' + <style type="text/css"> + table.gridreport { margin: .5em; border: solid 1px #aaa; } + th.gridreport { background-color: #ccc; } + tr.gridreport:nth-child(even) { background-color: #eee; } + tr.gridreport:nth-child(odd) { background-color: #fff; } + td.gridreport { margin: 0 .2em; padding: 0 .4em; } + </style> + ', +&> + +<%init> + +use FS::UID qw( dbh myconnect ); + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + + my $target_dt; + my @target_dates; + + # Work with all date/time operations @ 12 noon + my %noon = ( + hour => 12, + minute => 0, + second => 0 + ); + + my $now_dt = DateTime->now; + $now_dt = DateTime->new( + month => $now_dt->month, + day => $now_dt->day, + year => $now_dt->year, + %noon, + ); + + # Get target date from form + if ($cgi->param('target_date')) { + my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date'); + $target_dt = DateTime->new( + month => $mm, + day => $dd, + year => $yy, + %noon, + ) if $mm && $dd & $yy; + + # Catch a date from the past: time only travels in one direction + $target_dt = undef if $target_dt->epoch < $now_dt->epoch; + } + + # without a target date, default to tomorrow + unless ($target_dt) { + $target_dt = DateTime->from_epoch( epoch => time() + 86400) ; + $target_dt = DateTime->new( + month => $target_dt->month, + day => $target_dt->day, + year => $target_dt->year, + %noon + ); + } + + # If multiple_billing_dates checkbox selected, create a range of dates + # from today until the given report date. Otherwise, use target date only. + if ($cgi->param('multiple_billing_dates')) { + my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch); + until ($walking_dt->epoch > $target_dt->epoch) { + push @target_dates, $walking_dt->epoch; + $walking_dt->add(days => 1); + } + } else { + push @target_dates, $target_dt->epoch; + } + + # List all customers with an auto-bill method + # + # my %cust_payby = map {$_->custnum => $_} qsearch({ + # table => 'cust_payby', + # hashref => { + # weight => { op => '>', value => '0' }, + # paydate => { op => '>', value => $target_dt->ymd }, + # }, + # order_by => " ORDER BY weight DESC ", + # }); + + # List all customers with an auto-bill method that's not expired + my %cust_payby = map {$_->custnum => $_} qsearch({ + table => 'cust_payby', + hashref => { + weight => { op => '>', value => '0' }, + }, + order_by => " ORDER BY weight DESC ", + extra_sql => " AND ( payby = 'CHEK' OR ( paydate > '".$target_dt->ymd."')) ", + }); + + my %abreport; + my @rows; + + local $@; + local $SIG{__DIE__}; + my $temp_dbh = myconnect(); + eval { # Creating sandbox dbh where all connections are to be rolled back + local $FS::UID::dbh = $temp_dbh; + local $FS::UID::AutoCommit = 0; + + # Generate report data into @rows + for my $custnum (keys %cust_payby) { + my $cust_main = qsearchs('cust_main', {custnum => $custnum}); + + # walk forward through billing dates + for my $query_epoch (@target_dates) { + my $return_bill = []; + + eval { # Don't let an error on one customer crash the report + my $error = $cust_main->bill( + time => $query_epoch, + return_bill => $return_bill, + no_usage_reset => 1, + ); + die "$error (simulating future billing)" if $error; + }; + warn ("$@: (future_autobill custnum:$custnum)"); + + if (@{$return_bill}) { + my $inv = $return_bill->[0]; + push @rows,{ + name => $cust_main->name, + _date => $inv->_date, + cells => [ + { class => 'gridreport', value => $custnum }, + { class => 'gridreport', + value => '<a href="/view/cust_main.cgi?"'.$custnum.'">'.$cust_main->name.'</a>', + bypass_filter => 1, + }, + { class => 'gridreport', value => $inv->charged, format => 'money' }, + { class => 'gridreport', value => DateTime->from_epoch(epoch=>$inv->_date)->ymd }, + { class => 'gridreport', value => ($cust_payby{$custnum}->payby || $cust_payby{$custnum}->paytype) }, + { class => 'gridreport', value => $cust_payby{$custnum}->paymask }, + ] + }; + } + + } + $temp_dbh->rollback; + } # /foreach $custnum + + }; # /eval + warn("$@") if $@; + + # Sort output by date, and format for output to grid-report.html + my @cells = [ + # header row + { class => 'gridreport', value => '#', header => 1 }, + { class => 'gridreport', value => 'Name', header => 1 }, + { class => 'gridreport', value => 'Amount', header => 1 }, + { class => 'gridreport', value => 'Date', header => 1 }, + { class => 'gridreport', value => 'Type', header => 1 }, + { class => 'gridreport', value => 'Account', header => 1 }, + ]; + push @cells, + map { $_->{cells} } + sort { $a->{_date} <=> $b->{_date} || $a->{name} cmp $b->{name} } + @rows; + + # grid-report.html requires a parallel @rows parameter to accompany @cells + @rows = map { {class => 'gridreport'} } 1..scalar(@cells); + +</%init> diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html index 3583bb428..048fefb7a 100644 --- a/httemplate/search/report_contact.html +++ b/httemplate/search/report_contact.html @@ -10,17 +10,34 @@ 'disable_empty' => 0, &> +% # Selecting contacts and prospects at the same time has been sacrificed +% # for agent virtualization <& /elements/tr-select.html, - 'label' => 'Contact source', #??? not "type" - contacts have a type + 'label' => 'Contact source:', 'field' => 'link', - 'options' => [ 'prospect_main', 'cust_main', '' ], + 'options' => [ 'prospect_main', 'cust_main' ], 'labels' => { 'prospect_main' => 'Prospect contacts', 'cust_main' => 'Customer contacts', - '' => 'All contacts', }, 'curr_value' => scalar( $cgi->param('link') ), &> + <& /elements/tr-checkbox-multiple.html, + label => emt('Destinations').':', + field => 'dest', + options => [ 'message', 'invoice' ], + labels => { + invoice => 'Invoice recipients', + message => 'Message recipients', + }, + value => { message => 1 }, + &> + + <& /elements/tr-select-multiple-contact_class.html, + label => emt('Contact Type').':', + field => 'classnum', + &> + </FORM> </TABLE> diff --git a/httemplate/search/report_future_autobill.html b/httemplate/search/report_future_autobill.html new file mode 100644 index 000000000..1a0c9f48a --- /dev/null +++ b/httemplate/search/report_future_autobill.html @@ -0,0 +1,42 @@ +<%doc> + +Display date selector for the future_autobill.html report + +</%doc> +<% include('/elements/header.html', 'Future Auto-Bill Transactions' ) %> + + +<FORM ACTION="future_autobill.html" METHOD="GET"> +<TABLE> +<& /elements/tr-input-date-field.html, + { + name => 'target_date', + value => $target_date, + label => emt('Target billing date').': ', + required => 1 + } +&> + +<& /elements/tr-checkbox.html, + 'label' => emt('Multiple billing dates (slow)').': ', + 'field' => 'multiple_billing_dates', + 'value' => '1', +&> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>"> + +</FORM> + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $target_date = DateTime->from_epoch(epoch=>(time()+86400))->mdy('/'); + +</%init> diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html index 94488670d..fe412cc00 100644 --- a/httemplate/view/cust_main/contacts_new.html +++ b/httemplate/view/cust_main/contacts_new.html @@ -10,6 +10,7 @@ <%$th%>Contact</TH> <%$th%>Email</TH> <%$th%>Send invoices</TH> + <%$th%>Send messages</TH> <%$th%>Self-service</TH> % foreach my $phone_type (@phone_type) { <%$th%><% $phone_type->typename |h %></TH> @@ -32,6 +33,7 @@ % my @contact_email = $contact->contact_email; <%$td%><% join(', ', map $_->emailaddress, @contact_email) %></TD> <%$td%><% $cust_contact->invoice_dest eq 'Y' ? 'Yes' : 'No' %></TD> + <%$td%><% $cust_contact->message_dest eq 'Y' ? 'Yes' : 'No' %></TD> <%$td%> % if ( $cust_contact->selfservice_access ) { Enabled diff --git a/min_selfservice/css/default.css b/min_selfservice/css/default.css new file mode 100644 index 000000000..74f3565cb --- /dev/null +++ b/min_selfservice/css/default.css @@ -0,0 +1,108 @@ +body { + background-color:#e8e8e8; + //font-family:Arial, Verdana, Helvetica, sans-serif; + //font-size:12px; + //color:#0D0700; +} + +body, li, ol, p, table, td, th, tr, a, ul, blockquote, div { + //font-family:Arial, Verdana, Helvetica, sans-serif; + //font-size:12px; + color:#0D0700; +} + +a { + //color:#00527f; + text-decoration:none; +} + +a:hover { + text-decoration:underline; +} +td.page{ + border-style:solid; + border-width:2px; + border-color:#cccccc; + background-color:#f8f8f8; + padding:10px; +} + +#menu_ul { + padding: 0; + //width: 840px; + margin: 0 auto; +} + +#menu_ul li { + float: left; + list-style: none; + position: relative; + border-right: 4px solid #e8e8e8; +} + +#menu_ul a { + display: block; + padding: 6px 8px; + color: #525151; + font-size: 13px; + font-weight: bold; + white-space: nowrap; + background: #cccccc; + -moz-border-radius-topleft:8px; + -moz-border-radius-topright:8px; + -webkit-border-radius-topleft:8px; + -webkit-border-radius-topright:8px; + border-top-left-radius:8px; + border-top-right-radius:8px; +} + +#menu_ul a:hover { + text-decoration:none; +} + +#menu_ul ul { + margin:0; + padding:0; + display:none; + position: absolute; + top: 100%; + left: -1px; + background: #ae2099; + border: 1px solid #ffffff; +} + +#menu_ul ul li { + float: none; + border-style: none; +} + +#menu_ul ul a { + padding: 4px 10px; + color: #ffffff; + font-size: 12px; + font-weight: normal; + background: transparent; +} + +#menu_ul ul a:hover { + background: #7e0079; + -moz-border-radius-topleft:0px; + -moz-border-radius-topright:0px; + -webkit-border-radius-topleft:0px; + -webkit-border-radius-topright:0px; + border-top-left-radius:0px; + border-top-right-radius:0px; +} + +#menu_ul a.current_menu, #menu_ul a.hover { + color: #ffffff; + background: #7e0079; +} + +#menu_ul img { + vertical-align:middle; + width: 7px; + height: 4px; + border-style: none; + margin-left: 10px; +}
\ No newline at end of file diff --git a/min_selfservice/elements/card.php b/min_selfservice/elements/card.php new file mode 100644 index 000000000..4d502c223 --- /dev/null +++ b/min_selfservice/elements/card.php @@ -0,0 +1,53 @@ +<TR> + <TD ALIGN="right">Card number</TD> + <TD COLSPAN=6> + <TABLE> + <TR> + <TD> + <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<? echo $payinfo ?>"> </TD> + <TD>Exp.</TD> + <TD> + <SELECT NAME="month"> + <? $months = array( '01', '02', '03' ,'04', '05', '06', '07', '08', '09', '10', '11', '12' ); + foreach ( $months AS $m ) { + ?> + <OPTION <? if ($m == $month) { echo 'SELECTED'; } ?>><? echo $m; ?> + <? } ?> + </SELECT> + </TD> + <TD> / </TD> + <TD> + <SELECT NAME="year"> + <? $years = array( '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025', '2026' ); + foreach ( $years as $y ) { + ?> + <OPTION <? if ($y == $year ) { echo 'SELECTED'; } ?>><? echo $y; ?> + <? } ?> + </SELECT> + </TD> + </TR> + </TABLE> + </TD> +</TR> +<? if ( $withcvv ) { ?> + <TR> + <TD ALIGN="right">CVV2 (<A HREF="javascript:myopen('cvv2.html','cvv2','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=480,height=288')">help</A>)</TD> + <TD><INPUT TYPE="text" NAME="paycvv" VALUE="" SIZE=4 MAXLENGTH=4></TD> + </TR> +<? } ?> +<TR> + <TD ALIGN="right">Exact name on card</TD> + <TD COLSPAN=6><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<? echo $payname; ?>"></TD> +</TR> + +<? $lf = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/location.html', + 'args' => [ + 'no_asterisks' , 1, + #'address1_label' , 'Card billing address', + 'address1_label' , 'Card billing address', + ], + )); + echo $lf['output']; +?> diff --git a/min_selfservice/elements/check.php b/min_selfservice/elements/check.php new file mode 100644 index 000000000..fd0cd6d91 --- /dev/null +++ b/min_selfservice/elements/check.php @@ -0,0 +1,88 @@ +<? if ($ach_read_only) { $bgShade = 'BGCOLOR="#ffffff"'; } ?> +<TR> + <TD ALIGN="right">Account type</TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($paytype); ?> + <INPUT TYPE="hidden" NAME="paytype" VALUE="<? echo $paytype; ?>"> + <? } else { ?> + <SELECT NAME="paytype"> + <? foreach ( $paytypes AS $pt ) { ?> + <OPTION <? if ($pt == $paytype ) { echo 'SELECTED'; } ?> VALUE="<? echo $pt; ?>"><? echo $pt; ?> + <? } ?> + </SELECT> + <? } ?> + </TD> +</TR><TR> + <TD ALIGN="right">Account number</TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($payinfo1); ?> + <INPUT TYPE="hidden" NAME="payinfo1" VALUE="<? echo $payinfo1; ?>"> + <? } else { ?> + <INPUT TYPE="text" NAME="payinfo1" SIZE=10 MAXLENGTH=20 VALUE="<? echo $payinfo1; ?>"> + <? } ?> + </TD> +</TR><TR> + <TD ALIGN="right">ABA/Routing number</TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($payinfo2); ?> + <INPUT TYPE="hidden" NAME="payinfo2" VALUE="<? echo $payinfo2; ?>"> + <? } else { ?> + <INPUT TYPE="text" NAME="payinfo2" SIZE=10 MAXLENGTH=9 VALUE="<? echo $payinfo2; ?>"></TD> + <? } ?> +</TR><TR> + <TD ALIGN="right">Bank name</TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($payname); ?> + <INPUT TYPE="hidden" NAME="payname" VALUE="<? echo $payname; ?>"></TD> + <? } else { ?> + <INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<? echo $payname; ?>"></TD> + <? } ?> +</TR><TR> + + <? if ($show_paystate) { ?> + <TR> + <TD ALIGN="right">Bank state</TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($paystate); ?> + <INPUT TYPE="hidden" NAME="paystate" VALUE="<? echo $paystate; ?>"></TD> + <? } else { ?> + <SELECT NAME="paystate"> + <? foreach ( $states AS $s ) { ?> + <OPTION <? if ($s == $paystate ) { echo 'SELECTED'; } ?>><? echo $s; ?> + <? } ?> + </SELECT></TD> + <? } ?> + </TR> + <? } ?> + + <? if ($show_ss) { ?> + <TR> + <TD ALIGN="right">Account holder<BR>Social security or tax ID #</TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($ss); ?> + <INPUT TYPE="hidden" NAME="ss" VALUE="<? echo $ss; ?>"></TD> + <? } else { ?> + <INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="ss" VALUE="<? echo $ss; ?>"></TD> + <? } ?> + </TR> + <? } ?> + + <? if ($show_stateid) { ?> + <TR> + <TD ALIGN="right">Account holder<BR><? echo $stateid_label; ?></TD> + <TD <? echo $bgShade; ?>> + <? if ($ach_read_only) { echo htmlspecialchars($stateid); ?> + <INPUT TYPE="hidden" NAME="stateid" VALUE="<? echo $stateid; ?>"></TD> + <TD <? echo $bgShade; ?>> <? echo $stateid_state; ?> + <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<? echo $stateid_state; ?>"></TD> + <? } else { ?> + <INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="stateid" VALUE="<? echo $stateid; ?>"></TD> + <TD ALIGN="right"><? echo $stateid_state_label; ?></TD> + <TD><SELECT NAME="stateid_state"> + <? foreach ( $states AS $s ) { ?> + <OPTION <? if ($s == $stateid_state ) { echo 'SELECTED'; } ?>><? echo $s; ?> + <? } ?> + </SELECT></TD> + <? } ?> + </TR> + <? } ?> diff --git a/min_selfservice/elements/error.php b/min_selfservice/elements/error.php new file mode 100644 index 000000000..c8d8a179e --- /dev/null +++ b/min_selfservice/elements/error.php @@ -0,0 +1,3 @@ +<? if ($error) { ?> + <FONT SIZE="+1" COLOR="#ff0000"><? echo htmlspecialchars($error); echo '<BR><BR>'; ?></FONT> +<? } ?> diff --git a/min_selfservice/elements/footer.php b/min_selfservice/elements/footer.php new file mode 100644 index 000000000..fb662be73 --- /dev/null +++ b/min_selfservice/elements/footer.php @@ -0,0 +1 @@ +</BODY></HTML> diff --git a/min_selfservice/elements/header.php b/min_selfservice/elements/header.php new file mode 100644 index 000000000..633996515 --- /dev/null +++ b/min_selfservice/elements/header.php @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<HTML> + <HEAD> + <TITLE> + <? echo $title; ?> + </TITLE> + <link href="css/default.css" rel="stylesheet" type="text/css"/> + <script type="text/javascript" src="js/jquery.js"></script> + <script type="text/javascript" src="js/menu.js"></script> + </HEAD> + <BODY> + <FONT SIZE=5><? echo $title; ?></FONT> + <BR><BR> + diff --git a/min_selfservice/elements/menu.php b/min_selfservice/elements/menu.php new file mode 100644 index 000000000..5f600d5c0 --- /dev/null +++ b/min_selfservice/elements/menu.php @@ -0,0 +1,100 @@ +<? + +require_once('session.php'); + +$skin_info = $freeside->skin_info( array( + 'session_id' => $_COOKIE['session_id'], +) ); + +extract($skin_info); + +?> +<style type="text/css"> +#menu_ul ul li { + display: inline; + width: 100%; +} +</style> + +<ul id="menu_ul"> + +<? + + $menu_array = array( + 'payment.php Payments', + 'payment_cc.php Credit Card Payment', + 'payment_ach.php Electronic check payment', + 'payment_paypal.php PayPal payment', + 'payment_webpay.php Webpay payment', + ); + $submenu = array(); + + foreach ($menu_array AS $menu_item) { + if ( preg_match('/^\s*$/', $menu_item) ) { + print_menu($submenu, $current_menu, $menu_disable); + $submenu = array(); + } else { + $submenu[] = $menu_item; + } + } + print_menu($submenu, $current_menu, $menu_disable); + + function print_menu($submenu_array, $current_menu, $menu_disable) { + if ( count($submenu_array) == 0 ) { return; } + + $links = array(); + $labels = array(); + foreach ($submenu_array AS $submenu_item) { + $pieces = preg_split('/\s+/', $submenu_item, 2, PREG_SPLIT_NO_EMPTY); + $links[] = $pieces[0]; + $labels[] = $pieces[1]; + } + + print_link($links[0], $labels[0], $current_menu, $links); + + if ( count($links) > 1 ) { + if ( in_array( $current_menu, $links ) ) { + echo '<img src="images/dropdown_arrow_white.gif">'; + } else { + echo '<img src="images/dropdown_arrow_white.gif" style="display:none;">'; + echo '<img src="images/dropdown_arrow_grey.gif">'; + } + } + + array_shift($links); + array_shift($labels); + + echo '</a>'; + + if ( count($links) > 0 ) { + echo '<ul>'; + foreach ($links AS $link) { + $label = array_shift($labels); + if ( in_array($label, $menu_disable) == 0) { + print_link($link, $label, $current_menu, array($link) ); + echo '</a></li>'; + } + } + echo '</ul>'; + } + + echo '</li>'; + + } + + function print_link($link, $label, $current_menu, $search_array) { + echo '<li><a href="'. $link. '"'; + if ( in_array( $current_menu, $search_array ) ) { + echo ' class="current_menu"'; + } + echo '>'. _($label); + } + +?> + +</ul> + +<div style="clear:both;"></div> +<table cellpadding="0" cellspacing="0" border="0" style="min-width:666px"> +<tr> +<td class="page">
\ No newline at end of file diff --git a/min_selfservice/elements/menu_footer.php b/min_selfservice/elements/menu_footer.php new file mode 100644 index 000000000..8beeeaf02 --- /dev/null +++ b/min_selfservice/elements/menu_footer.php @@ -0,0 +1,3 @@ +</td> +</tr> +</table> diff --git a/min_selfservice/elements/session.php b/min_selfservice/elements/session.php new file mode 100644 index 000000000..a6b8b4af0 --- /dev/null +++ b/min_selfservice/elements/session.php @@ -0,0 +1,6 @@ +<? + +require_once('freeside.class.php'); +$freeside = new FreesideSelfService(); + +?> diff --git a/min_selfservice/freeside.class.php b/min_selfservice/freeside.class.php new file mode 100644 index 000000000..ee77ce016 --- /dev/null +++ b/min_selfservice/freeside.class.php @@ -0,0 +1,74 @@ +<?php + +#pre-php 5.4 compatible version? +function flatten($hash) { + if ( !is_array($hash) ) return $hash; + $flat = array(); + + array_walk($hash, function($value, $key, &$to) { + array_push($to, $key, $value); + }, $flat); + + if ( PHP_VERSION_ID >= 50400 ) { + + #php 5.4+ (deb 7+) + foreach ($hash as $key => $value) { + $flat[] = $key; + $flat[] = $value; + } + + } + + return($flat); +} + +#php 5.4+? +#function flatten($hash) { +# if ( !is_array($hash) ) return $hash; +# +# $flat = array(); +# +# foreach ($hash as $key => $value) { +# $flat[] = $key; +# $flat[] = $value; +# } +# +# return($flat); +#} + +class FreesideSelfService { + + //Change this to match the location of your selfservice xmlrpc.cgi or daemon + #var $URL = 'https://localhost/selfservice/xmlrpc.cgi'; + #var $URL = 'http://localhost/selfservice/xmlrpc.cgi'; + var $URL = 'http://localhost:8080/'; + + function FreesideSelfService() { + $this; + } + + public function __call($name, $arguments) { + + error_log("[FreesideSelfService] $name called, sending to ". $this->URL); + + $request = xmlrpc_encode_request("FS.ClientAPI_XMLRPC.$name", flatten($arguments[0])); + $context = stream_context_create( array( 'http' => array( + 'method' => "POST", + 'header' => "Content-Type: text/xml", + 'content' => $request + ))); + $file = file_get_contents($this->URL, false, $context); + $response = xmlrpc_decode($file); + // uncomment to trace everything + //error_log(print_r($response, true)); + if (xmlrpc_is_fault($response)) { + trigger_error("[FreesideSelfService] XML-RPC communication error: $response[faultString] ($response[faultCode])"); + } else { + //error_log("[FreesideSelfService] $response"); + return $response; + } + } + +} + +?> diff --git a/min_selfservice/images/cross.png b/min_selfservice/images/cross.png Binary files differnew file mode 100644 index 000000000..1514d51a3 --- /dev/null +++ b/min_selfservice/images/cross.png diff --git a/min_selfservice/images/dropdown_arrow_grey.gif b/min_selfservice/images/dropdown_arrow_grey.gif Binary files differnew file mode 100644 index 000000000..fbf155d68 --- /dev/null +++ b/min_selfservice/images/dropdown_arrow_grey.gif diff --git a/min_selfservice/images/dropdown_arrow_white.gif b/min_selfservice/images/dropdown_arrow_white.gif Binary files differnew file mode 100644 index 000000000..c24d7846f --- /dev/null +++ b/min_selfservice/images/dropdown_arrow_white.gif diff --git a/min_selfservice/images/error.png b/min_selfservice/images/error.png Binary files differnew file mode 100644 index 000000000..628cf2dae --- /dev/null +++ b/min_selfservice/images/error.png diff --git a/min_selfservice/images/tick.png b/min_selfservice/images/tick.png Binary files differnew file mode 100644 index 000000000..a9925a06a --- /dev/null +++ b/min_selfservice/images/tick.png diff --git a/min_selfservice/index.php b/min_selfservice/index.php new file mode 100644 index 000000000..c7e20c503 --- /dev/null +++ b/min_selfservice/index.php @@ -0,0 +1,40 @@ +<? + $error = $_GET['error']; + if ( $error ) { + $username = $_GET['username']; + $domain = $_GET['domain']; + $title ='Login Error'; + include('elements/header.php'); + include('elements/error.php'); +?> + <TABLE BORDER=0 CELLSPACING=2 CELLPADDING=0> + <TR> + <TD> + Sorry we were unable to locate your account with ip <? echo $username; ?> . + </TD> + </TR> + </TABLE> +<? + include('elements/footer.php'); + } + else { include('login.php'); } +?> + +<? #include('login.php'); ?> + + +<? +#require('freeside.class.php'); +#$freeside = new FreesideSelfService(); +# +#$login_info = $freeside->login_info(); +# +#extract($login_info); +# +#$error = $_GET['error']; +#if ( $error ) { +# $username = $_GET['username']; +# $domain = $_GET['domain']; +#} + +?>
\ No newline at end of file diff --git a/min_selfservice/js/jquery.js b/min_selfservice/js/jquery.js new file mode 100644 index 000000000..e407e7699 --- /dev/null +++ b/min_selfservice/js/jquery.js @@ -0,0 +1,6 @@ +/*! jQuery v1.10.1 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery-1.10.1.min.map +*/ +(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.1",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=lt(),k=lt(),E=lt(),S=!1,A=function(){return 0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=bt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+xt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return At(e.replace(z,"$1"),t,n,i)}function st(e){return K.test(e+"")}function lt(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function ut(e){return e[b]=!0,e}function ct(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function pt(e,t,n){e=e.split("|");var r,i=e.length,a=n?null:t;while(i--)(r=o.attrHandle[e[i]])&&r!==t||(o.attrHandle[e[i]]=a)}function ft(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:e[t]===!0?t.toLowerCase():null}function dt(e,t){return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function ht(e){return"input"===e.nodeName.toLowerCase()?e.defaultValue:t}function gt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function mt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function yt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function vt(e){return ut(function(t){return t=+t,ut(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.parentWindow;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.frameElement&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ct(function(e){return e.innerHTML="<a href='#'></a>",pt("type|href|height|width",dt,"#"===e.firstChild.getAttribute("href")),pt(B,ft,null==e.getAttribute("disabled")),e.className="i",!e.getAttribute("className")}),r.input=ct(function(e){return e.innerHTML="<input>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}),pt("value",ht,r.attributes&&r.input),r.getElementsByTagName=ct(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ct(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ct(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=st(n.querySelectorAll))&&(ct(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ct(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=st(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ct(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=st(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},r.sortDetached=ct(function(e){return 1&e.compareDocumentPosition(n.createElement("div"))}),A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return gt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?gt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:ut,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=bt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?ut(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ut(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?ut(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ut(function(e){return function(t){return at(e,t).length>0}}),contains:ut(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:ut(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:vt(function(){return[0]}),last:vt(function(e,t){return[t-1]}),eq:vt(function(e,t,n){return[0>n?n+t:n]}),even:vt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:vt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:vt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:vt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=mt(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=yt(n);function bt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function xt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function wt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function Tt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Ct(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function Nt(e,t,n,r,i,o){return r&&!r[b]&&(r=Nt(r)),i&&!i[b]&&(i=Nt(i,o)),ut(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||St(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:Ct(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=Ct(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=Ct(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function kt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=wt(function(e){return e===t},s,!0),p=wt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[wt(Tt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return Nt(l>1&&Tt(f),l>1&&xt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&kt(e.slice(l,r)),i>r&&kt(e=e.slice(r)),i>r&&xt(e))}f.push(n)}return Tt(f)}function Et(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=Ct(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?ut(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=bt(e)),n=t.length;while(n--)o=kt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Et(i,r))}return o};function St(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function At(e,t,n,i){var a,s,u,c,p,f=bt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&xt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}o.pseudos.nth=o.pseudos.eq;function jt(){}jt.prototype=o.filters=o.pseudos,o.setFilters=new jt,r.sortStable=b.split("").sort(A).join("")===b,p(),[0,0].sort(A),r.detectDuplicates=S,x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!l||i&&!u||(n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null) +}),n=s=l=u=r=o=null,t}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=x(this),l=t,u=e.match(T)||[];while(o=u[a++])l=r?l:!s.hasClass(o),s[l?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); +u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window); diff --git a/min_selfservice/js/menu.js b/min_selfservice/js/menu.js new file mode 100644 index 000000000..30e481621 --- /dev/null +++ b/min_selfservice/js/menu.js @@ -0,0 +1,17 @@ +$(document).ready(function() { + $('#menu_ul > li').hover(function(){ + $('a:first', this).addClass('hover'); + $('ul:first', this).show(); + if ($('.current_menu:first', this).length == 0) { + $('img[src*="dropdown_arrow_white"]', this).show(); + $('img[src*="dropdown_arrow_grey"]', this).hide(); + } + }, function(){ + $('ul:first', this).hide(); + $('a:first', this).removeClass('hover'); + if ($('.current_menu:first', this).length == 0) { + $('img[src*="dropdown_arrow_white"]', this).hide(); + $('img[src*="dropdown_arrow_grey"]', this).show(); + } + }); +}); diff --git a/min_selfservice/login.php b/min_selfservice/login.php new file mode 100644 index 000000000..91e19cd7f --- /dev/null +++ b/min_selfservice/login.php @@ -0,0 +1,102 @@ +<? + +require('freeside.class.php'); +$freeside = new FreesideSelfService(); + +$ip = $_SERVER['REMOTE_ADDR']; +# need a routine here to get mac address from radius account table based on ip address. Every else should be good to go. +$mac_addr = '1234567890FF'; + +$response = $freeside->login( array( + 'username' => $mac_addr, + 'domain' => 'ip_mac', +) ); + +#error_log("[login] received response from freeside: $response"); + +$error = $response['error']; + +if ( $error ) { + + header('Location:index.php?username='. urlencode($mac). + '&domain='. urlencode($domain). + '&email='. urlencode($email). + '&error='. urlencode($error) + ); + die(); + +} + +// sucessful login + +$session_id = $response['session_id']; + +error_log("[login] logged into freeside with session_id=$session_id, setting cookie"); + +// now what? for now, always redirect to the main page (or the select a +// customer diversion). +// eventually, other options? + +setcookie('session_id', $session_id); + +if ( $response['custnum'] || $response['svcnum'] ) { + + header("Location:main.php"); + die(); + //1; + +} elseif ( $response['customers'] ) { +var_dump($response['customers']); +?> + + <? $title ='Select customer'; include('elements/header.php'); ?> + <? include('elements/error.php'); ?> + + <FORM NAME="SelectCustomerForm" ACTION="process_select_cust.php" METHOD=POST> + <INPUT TYPE="hidden" NAME="action" VALUE="switch_cust"> + + <TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0> + + <TR> + <TH ALIGN="right">Customer </TH> + <TD> + <SELECT NAME="custnum" ID="custnum" onChange="custnum_changed()"> + <OPTION VALUE="">Select a customer + <? foreach ( $response['customers'] AS $custnum => $customer ) { ?> + <OPTION VALUE="<? echo $custnum ?>"><? echo htmlspecialchars( $customer ) ?> + <? } ?> + </SELECT> + </TD> + </TR> + + <TR> + <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" ID="submit" VALUE="Select customer" DISABLED></TD> + </TR> + + </TABLE> + </FORM> + + <SCRIPT TYPE="text/javascript"> + + function custnum_changed () { + var form = document.SelectCustomerForm; + if ( form.custnum.selectedIndex > 0 ) { + form.submit.disabled = false; + } else { + form.submit.disabled = true; + } + } + + </SCRIPT> + + <? include('elements/footer.php'); ?> + +<? + +// } else { +// +// die 'login successful, but unrecognized info (no custnum, svcnum or customers)'; + +} + +?>
\ No newline at end of file diff --git a/min_selfservice/main.php b/min_selfservice/main.php new file mode 100644 index 000000000..9c58f3f87 --- /dev/null +++ b/min_selfservice/main.php @@ -0,0 +1,34 @@ +<? $title ='Make A Payment'; include('elements/header.php'); ?> +<? $current_menu = 'payment.php'; include('elements/menu.php'); ?> + +<? +$customer_info = $freeside->customer_info_short( array( + 'session_id' => $_COOKIE['session_id'], +) ); + + +if ( isset($customer_info['error']) && $customer_info['error'] ) { + $error = $customer_info['error']; + header('Location:index.php?error='. urlencode($error)); + die(); +} + +extract($customer_info); + +?> + +<? include('elements/error.php'); ?> + +<P>Hello <? echo htmlspecialchars($name); ?></P> + +<P>Your current balance is <B>$<? echo $balance ?></B> how would you like to make a payment today?</P> + +<div STYLE="margin-left: 25px;"> +<a href="payment_cc.php">Credit card payment</A><BR><BR> +<a href="payment_ach.php">Electronic check payment</A><BR><BR> +<a href="payment_paypal.php">PayPal payment</A><BR><BR> +<a href="payment_webpay.php">Webpay payment</A><BR><BR> +</div> + +<? include('elements/menu_footer.php'); ?> +<? include('elements/footer.php'); ?>
\ No newline at end of file diff --git a/min_selfservice/payment.php b/min_selfservice/payment.php new file mode 100644 index 000000000..f93d08eeb --- /dev/null +++ b/min_selfservice/payment.php @@ -0,0 +1,14 @@ +<? $title ='Make A Payment'; include('elements/header.php'); ?> +<? $current_menu = 'payment.php'; include('elements/menu.php'); ?> + +<? include('elements/error.php'); ?> + +<FONT SIZE="+1"> +<a href="payment_cc.php">Credit card payment</A><BR><BR> +<a href="payment_ach.php">Electronic check payment</A><BR><BR> +<a href="payment_paypal.php">PayPal payment</A><BR><BR> +<a href="payment_webpay.php">Webpay payment</A><BR><BR> +</FONT> + +<? include('elements/menu_footer.php'); ?> +<? include('elements/footer.php'); ?>
\ No newline at end of file diff --git a/min_selfservice/payment_ach.php b/min_selfservice/payment_ach.php new file mode 100644 index 000000000..04e39c537 --- /dev/null +++ b/min_selfservice/payment_ach.php @@ -0,0 +1,113 @@ +<? $title ='Electronic Check Payment'; include('elements/header.php'); ?> +<? $current_menu = 'payment_ach.php'; include('elements/menu.php'); ?> +<? + +if ( isset($_POST['amount']) && $_POST['amount'] ) { + + $payment_results = $freeside->process_payment(array( + 'session_id' => $_COOKIE['session_id'], + 'payby' => 'CHEK', + 'amount' => $_POST['amount'], + 'payinfo1' => $_POST['payinfo1'], + 'payinfo2' => $_POST['payinfo2'], + 'month' => 12, + 'year' => 2037, + 'payname' => $_POST['payname'], + 'paytype' => $_POST['paytype'], + 'paystate' => $_POST['paystate'], + 'ss' => $_POST['ss'], + 'stateid' => $_POST['stateid'], + 'stateid_state' => $_POST['stateid_state'], + 'save' => $_POST['save'], + 'auto' => $_POST['auto'], + 'paybatch' => $_POST['paybatch'], + //'discount_term' => $discount_term, + )); + + if ( $payment_results['error'] ) { + $payment_error = $payment_results['error']; + } else { + $receipt_html = $payment_results['receipt_html']; + } + +} + +if ( $receipt_html ) { ?> + + Your payment was processed successfully. Thank you.<BR><BR> + <? echo $receipt_html; ?> + +<? } else { + + $payment_info = $freeside->payment_info( array( + 'session_id' => $_COOKIE['session_id'], + 'payment_payby' => 'CHEK', + ) ); + + if ( isset($payment_info['error']) && $payment_info['error'] ) { + $error = $payment_info['error']; + header('Location:index.php?error='. urlencode($error)); + die(); + } + + extract($payment_info); + + $error = $payment_error; + +?> + + <? include('elements/error.php'); ?> + + <FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_ach.php" onSubmit="document.OneTrueForm.process.disabled=true"> + + <TABLE> + <TR> + <TD ALIGN="right">Amount Due</TD> + <TD> + <TABLE><TR><TD BGCOLOR="#ffffff"> + $<? echo sprintf("%.2f", $balance) ?> + </TD></TR></TABLE> + </TD> + </TR> + + <TR> + <TD ALIGN="right">Payment amount</TD> + <TD> + <TABLE><TR><TD BGCOLOR="#ffffff"> + $<INPUT TYPE="text" NAME="amount" SIZE=8 VALUE="<? echo sprintf("%.2f", $balance) ?>"> + </TD></TR></TABLE> + </TD> + </TR> + <? // include('elements/discount_term.php') ?> + + <? include('elements/check.php') ?> + + <? if ($ach_read_only) { ?> + <? if ( $payby == 'CARD' ) { ?> + <INPUT TYPE="hidden" NAME="auto" VALUE="1"> + <? } ?> + </TD></TR> + <? } else { ?> + <TR> + <TD COLSPAN=2> + <INPUT TYPE="checkbox" <? if ( ! $save_unchecked ) { echo 'CHECKED'; } ?> NAME="save" VALUE="1"> + Remember this information + </TD> + </TR><TR> + <TD COLSPAN=2> + <INPUT TYPE="checkbox" <? if ( $payby == 'CARD' ) { echo ' CHECKED'; } ?> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }"> + Charge future payments to this account automatically + </TD> + </TR> + <? } ?> + + </TABLE> + <BR> + <INPUT TYPE="hidden" NAME="paybatch" VALUE="<? echo $paybatch; ?>"> + <INPUT TYPE="submit" NAME="process" VALUE="Process payment"> <!-- onClick="this.disabled=true"> --> + </FORM> + +<? } ?> + +<? include('elements/menu_footer.php'); ?> +<? include('elements/footer.php'); ?>
\ No newline at end of file diff --git a/min_selfservice/payment_cc.php b/min_selfservice/payment_cc.php new file mode 100644 index 000000000..f47e83c2c --- /dev/null +++ b/min_selfservice/payment_cc.php @@ -0,0 +1,120 @@ +<? $title ='Credit Card Payment'; include('elements/header.php'); ?> +<? $current_menu = 'payment_cc.php'; include('elements/menu.php'); ?> +<? + +if ( isset($_POST['amount']) && $_POST['amount'] ) { + + $payment_results = $freeside->process_payment(array( + 'session_id' => $_COOKIE['session_id'], + 'payby' => 'CARD', + 'amount' => $_POST['amount'], + 'payinfo' => $_POST['payinfo'], + 'paycvv' => $_POST['paycvv'], + 'month' => $_POST['month'], + 'year' => $_POST['year'], + 'payname' => $_POST['payname'], + 'address1' => $_POST['address1'], + 'address2' => $_POST['address2'], + 'city' => $_POST['city'], + 'state' => $_POST['state'], + 'zip' => $_POST['zip'], + 'country' => $_POST['country'], + 'save' => $_POST['save'], + 'auto' => $_POST['auto'], + 'paybatch' => $_POST['paybatch'], + //'discount_term' => $discount_term, + )); + + if ( $payment_results['error'] ) { + $payment_error = $payment_results['error']; + } else { + $receipt_html = $payment_results['receipt_html']; + } + +} + +if ( $receipt_html ) { ?> + + Your payment was processed successfully. Thank you.<BR><BR> + <? echo $receipt_html; ?> + +<? } else { + + $payment_info = $freeside->payment_info( array( + 'session_id' => $_COOKIE['session_id'], + 'payment_payby' => 'CARD', + ) ); + + if ( isset($payment_info['error']) && $payment_info['error'] ) { + $error = $payment_info['error']; + header('Location:index.php?error='. urlencode($error)); + die(); + } + + extract($payment_info); + + $error = $payment_error; + + $tr_amount_fee = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount', $balance ], + )); + //$tr_amount_fee = $tr_amount_fee->{'error'} || $tr_amount_fee->{'output'}; + $tr_amount_fee = $tr_amount_fee['output']; + + ?> + + <? include('elements/error.php'); ?> + + <FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_cc.php" onSubmit="document.OneTrueForm.process.disabled=true"> + + <TABLE> + <TR> + <TD ALIGN="right">Amount Due</TD> + <TD COLSPAN=7> + <TABLE><TR><TD> + $<? echo sprintf("%.2f", $balance) ?> + </TD></TR></TABLE> + </TD> + </TR> + + <? echo $tr_amount_fee; ?> + + <? //include('elements/discount_term.php') ?> + + <TR> + <TD ALIGN="right">Card type</TD> + <TD COLSPAN=7> + <SELECT NAME="card_type"><OPTION></OPTION> + <? foreach ( $card_types AS $ct ) { ?> + <OPTION <? if ( $card_type == $ct ) { echo 'SELECTED'; } ?> + VALUE="<? echo $ct; ?>"><? echo $ct; ?> + <? } ?> + </SELECT> + </TD> + </TR> + + <? include('elements/card.php'); ?> + + <TR> + <TD COLSPAN=8> + <INPUT TYPE="checkbox" <? if ( ! $save_unchecked ) { echo 'CHECKED'; } ?> NAME="save" VALUE="1"> + Remember this card and billing address + </TD> + </TR><TR> + <TD COLSPAN=8> + <INPUT TYPE="checkbox" <? if ( $payby == 'CARD' ) { echo ' CHECKED'; } ?> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }"> + Charge future payments to this card automatically + </TD> + </TR> + </TABLE> + <BR> + <INPUT TYPE="hidden" NAME="paybatch" VALUE="<? echo $paybatch ?>"> + <INPUT TYPE="submit" NAME="process" VALUE="Process payment"> <!-- onClick="this.disabled=true"> --> + </FORM> + +<? } ?> + +<? include('elements/menu_footer.php'); ?> +<? include('elements/footer.php'); ?>
\ No newline at end of file diff --git a/min_selfservice/payment_finish.php b/min_selfservice/payment_finish.php new file mode 100644 index 000000000..04fdfa6f3 --- /dev/null +++ b/min_selfservice/payment_finish.php @@ -0,0 +1,34 @@ +<? $title ='Payment Confirmation'; include('elements/header.php'); ?> +<? $current_menu = ''; include('elements/menu.php'); ?> +<? + $params = $_GET; + $params['session_id'] = $_COOKIE['session_id']; + + //print_r($params); + $payment_results = $freeside->finish_thirdparty($params); + + if ( isset($payment_results['error']) ) { + $error = $payment_results['error']; + include('elements/error.php'); + } else { +?> +<TABLE> + <TR> + <TH COLSPAN=2><FONT SIZE=+1><B>Your payment details</B></FONT></TH> + </TR> + <TR> +<TR> + <TD ALIGN="right">Payment #</TD> + <TD BGCOLOR="#ffffff"><B><? echo($payment_results['paynum']); ?></B></TD> +</TR> +<TR> + <TD ALIGN="right">Payment amount</TH> + <TD BGCOLOR="#ffffff"><B>$<? printf('%.2f', $payment_results['paid']); ?></B> + </TD> +</TR> +<TR> + <TD ALIGN="right">Processing #</TD> + <TD BGCOLOR="#ffffff"><B><? echo($payment_results['order_number']); ?></B> + </TD> +</TR> +<? } ?>
\ No newline at end of file diff --git a/min_selfservice/payment_paypal.php b/min_selfservice/payment_paypal.php new file mode 100644 index 000000000..7a70f9852 --- /dev/null +++ b/min_selfservice/payment_paypal.php @@ -0,0 +1,41 @@ +<? $title ='PayPal Payment'; include('elements/header.php'); ?> +<? $current_menu = 'payment_paypal.php'; include('elements/menu.php'); ?> +<? +if ( isset($_POST['amount']) && $_POST['amount'] ) { + + $payment_results = $freeside->start_thirdparty(array( + 'session_id' => $_COOKIE['session_id'], + 'method' => 'PAYPAL', + 'amount' => $_POST['amount'], + )); + + include('elements/post_thirdparty.php'); + +} else { + + $payment_info = $freeside->payment_info( array( + 'session_id' => $_COOKIE['session_id'], + ) ); + + $tr_amount_fee = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount', $payment_info['balance'] ], + )); + $tr_amount_fee = $tr_amount_fee['output']; + + include('elements/error.php'); ?> +<FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_paypal.php"> + <TABLE> + <TR> + <TD ALIGN="right">Amount Due</TD> + <TD>$<? echo sprintf('%.2f', $payment_info['balance']); ?></TD> + </TR> + <? echo $tr_amount_fee; ?> + </TABLE> + <BR> + <INPUT TYPE="submit" NAME="process" VALUE="Start payment"> +</FORM> +<? } ?> +<? include('elements/menu_footer.php'); ?> +<? include('elements/footer.php'); ?>
\ No newline at end of file diff --git a/min_selfservice/payment_webpay.php b/min_selfservice/payment_webpay.php new file mode 100644 index 000000000..e4343fcb4 --- /dev/null +++ b/min_selfservice/payment_webpay.php @@ -0,0 +1,41 @@ +<? $title ='Webpay Payment'; include('elements/header.php'); ?> +<? $current_menu = 'payment_webpay.php'; include('elements/menu.php'); ?> +<? +if ( isset($_POST['amount']) && $_POST['amount'] ) { + + $payment_results = $freeside->start_thirdparty(array( + 'session_id' => $_COOKIE['session_id'], + 'method' => 'CC', + 'amount' => $_POST['amount'], + )); + + include('elements/post_thirdparty.php'); + +} else { + + $payment_info = $freeside->payment_info( array( + 'session_id' => $_COOKIE['session_id'], + ) ); + + $tr_amount_fee = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount', $payment_info['balance'] ], + )); + $tr_amount_fee = $tr_amount_fee['output']; + + include('elements/error.php'); ?> +<FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_webpay.php"> + <TABLE> + <TR> + <TD ALIGN="right">Amount Due</TD> + <TD>$<? echo sprintf('%.2f', $payment_info['balance']); ?></TD> + </TR> + <? echo $tr_amount_fee; ?> + </TABLE> + <BR> + <INPUT TYPE="submit" NAME="process" VALUE="Start payment"> +</FORM> +<? } ?> +<? include('elements/menu_footer.php'); ?> +<? include('elements/footer.php'); ?>
\ No newline at end of file |