summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm7
-rw-r--r--FS/FS/Mason.pm3
-rw-r--r--FS/FS/Schema.pm93
-rw-r--r--FS/FS/cdr.pm9
-rw-r--r--FS/FS/cdr/Import.pm164
-rw-r--r--FS/FS/cdr/acmepacket.pm168
-rw-r--r--FS/FS/cdr/ani_networks.pm81
-rw-r--r--FS/FS/contact.pm7
-rw-r--r--FS/FS/cust_contact.pm7
-rw-r--r--FS/FS/cust_main.pm99
-rw-r--r--FS/FS/cust_main/Import_Charges.pm57
-rw-r--r--FS/FS/cust_main/Search.pm48
-rw-r--r--FS/FS/cust_main/import_charges/gcet.pm26
-rw-r--r--FS/FS/cust_main/import_charges/ooma.pm21
-rw-r--r--FS/FS/cust_main/import_charges/simple.pm21
-rw-r--r--FS/FS/cust_main_Mixin.pm38
-rw-r--r--FS/FS/h_svc_realestate.pm31
-rw-r--r--FS/FS/msg_template/email.pm29
-rw-r--r--FS/FS/realestate_location.pm177
-rw-r--r--FS/FS/realestate_unit.pm163
-rw-r--r--FS/FS/svc_Common.pm10
-rw-r--r--FS/FS/svc_realestate.pm172
-rwxr-xr-xFS/bin/freeside-cdr-aninetworks-import222
-rw-r--r--FS/bin/freeside-cdr-freeswitch11
-rw-r--r--FS/t/realestate_location.t5
-rw-r--r--FS/t/realestate_unit.t5
-rw-r--r--FS/t/svc_realestate.t5
-rwxr-xr-xbin/fakesmtpserver.pl53
-rw-r--r--httemplate/browse/realestate_location.html43
-rw-r--r--httemplate/browse/realestate_unit.html70
-rw-r--r--httemplate/docs/part_svc-table.html7
-rw-r--r--httemplate/edit/process/cust_main-contacts.html8
-rw-r--r--httemplate/edit/process/realestate_location.html14
-rw-r--r--httemplate/edit/process/realestate_unit.html13
-rw-r--r--httemplate/edit/realestate_location.html72
-rw-r--r--httemplate/edit/realestate_unit.html48
-rw-r--r--httemplate/elements/contact.html6
-rw-r--r--httemplate/elements/menu.html8
-rw-r--r--httemplate/elements/select-multiple-contact_class.html21
-rw-r--r--httemplate/elements/select-realestate_location.html32
-rw-r--r--httemplate/elements/select-realestate_unit.html59
-rw-r--r--httemplate/elements/tr-checkbox-multiple.html20
-rw-r--r--httemplate/elements/tr-select-multiple-contact_class.html32
-rw-r--r--httemplate/elements/tr-select-realestate_location.html17
-rw-r--r--httemplate/elements/tr-select-realestate_unit.html5
-rw-r--r--httemplate/misc/cust_main-import_charges.cgi8
-rw-r--r--httemplate/misc/email-customers.html113
-rw-r--r--httemplate/pref/pref-process.html12
-rw-r--r--httemplate/pref/pref.html14
-rw-r--r--httemplate/search/contact.html332
-rwxr-xr-xhttemplate/search/cust_main.html8
-rw-r--r--httemplate/search/elements/grid-report.html8
-rw-r--r--httemplate/search/elements/search.html6
-rw-r--r--httemplate/search/future_autobill.html189
-rw-r--r--httemplate/search/report_contact.html23
-rw-r--r--httemplate/search/report_future_autobill.html42
-rw-r--r--httemplate/view/cust_main/contacts_new.html2
-rw-r--r--min_selfservice/css/default.css108
-rw-r--r--min_selfservice/elements/card.php53
-rw-r--r--min_selfservice/elements/check.php88
-rw-r--r--min_selfservice/elements/error.php3
-rw-r--r--min_selfservice/elements/footer.php1
-rw-r--r--min_selfservice/elements/header.php14
-rw-r--r--min_selfservice/elements/menu.php100
-rw-r--r--min_selfservice/elements/menu_footer.php3
-rw-r--r--min_selfservice/elements/session.php6
-rw-r--r--min_selfservice/freeside.class.php74
-rw-r--r--min_selfservice/images/cross.pngbin0 -> 655 bytes
-rw-r--r--min_selfservice/images/dropdown_arrow_grey.gifbin0 -> 49 bytes
-rw-r--r--min_selfservice/images/dropdown_arrow_white.gifbin0 -> 821 bytes
-rw-r--r--min_selfservice/images/error.pngbin0 -> 666 bytes
-rw-r--r--min_selfservice/images/tick.pngbin0 -> 537 bytes
-rw-r--r--min_selfservice/index.php40
-rw-r--r--min_selfservice/js/jquery.js6
-rw-r--r--min_selfservice/js/menu.js17
-rw-r--r--min_selfservice/login.php102
-rw-r--r--min_selfservice/main.php34
-rw-r--r--min_selfservice/payment.php14
-rw-r--r--min_selfservice/payment_ach.php113
-rw-r--r--min_selfservice/payment_cc.php120
-rw-r--r--min_selfservice/payment_finish.php34
-rw-r--r--min_selfservice/payment_paypal.php41
-rw-r--r--min_selfservice/payment_webpay.php41
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&nbsp;invoices';
+ $label{'message_dest'} = 'Send&nbsp;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&nbsp;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&nbsp;(<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&nbsp;name&nbsp;on&nbsp;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&nbsp;billing&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;holder<BR>Social&nbsp;security&nbsp;or&nbsp;tax&nbsp;ID&nbsp;#</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&nbsp;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
new file mode 100644
index 000000000..1514d51a3
--- /dev/null
+++ b/min_selfservice/images/cross.png
Binary files differ
diff --git a/min_selfservice/images/dropdown_arrow_grey.gif b/min_selfservice/images/dropdown_arrow_grey.gif
new file mode 100644
index 000000000..fbf155d68
--- /dev/null
+++ b/min_selfservice/images/dropdown_arrow_grey.gif
Binary files differ
diff --git a/min_selfservice/images/dropdown_arrow_white.gif b/min_selfservice/images/dropdown_arrow_white.gif
new file mode 100644
index 000000000..c24d7846f
--- /dev/null
+++ b/min_selfservice/images/dropdown_arrow_white.gif
Binary files differ
diff --git a/min_selfservice/images/error.png b/min_selfservice/images/error.png
new file mode 100644
index 000000000..628cf2dae
--- /dev/null
+++ b/min_selfservice/images/error.png
Binary files differ
diff --git a/min_selfservice/images/tick.png b/min_selfservice/images/tick.png
new file mode 100644
index 000000000..a9925a06a
--- /dev/null
+++ b/min_selfservice/images/tick.png
Binary files differ
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&nbsp;Due</TD>
+ <TD>
+ <TABLE><TR><TD BGCOLOR="#ffffff">
+ $<? echo sprintf("%.2f", $balance) ?>
+ </TD></TR></TABLE>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD ALIGN="right">Payment&nbsp;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&nbsp;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&nbsp;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&nbsp;#</TD>
+ <TD BGCOLOR="#ffffff"><B><? echo($payment_results['paynum']); ?></B></TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Payment&nbsp;amount</TH>
+ <TD BGCOLOR="#ffffff"><B>$<? printf('%.2f', $payment_results['paid']); ?></B>
+ </TD>
+</TR>
+<TR>
+ <TD ALIGN="right">Processing&nbsp;#</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&nbsp;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&nbsp;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