6502848606a317660c8fbdf09edae83207d62c1e
[freeside.git] / httemplate / elements / customer-table.html
1 <%doc>
2
3 Example:
4
5   include( '/elements/customer-table.html',
6
7              ###
8              # required
9              ###
10
11              #listrefs...
12              'header'        => [ '#', 'Item' ],
13              'fields'        => [
14                                   'column',
15                                   sub { my ($row,$param) = @_;
16                                         $param->{"column$row"};
17                                       },
18                                 ],
19
20              ###
21              # optional
22              ###
23
24              'name_singular' => 'customer', #label
25              'custnum_update_callback' => 'name_of_js_callback' #passed a rownum
26
27              #listrefs
28              'types'         => ['immutable', ''], # immutable or ''/text
29              'align'         => [ 'c', 'l', 'r', '' ],
30              'size'          => [],                # sizes ignored for immutable
31              'color'         => [],
32              'footer'        => ['string', '_TOTAL'], # strings or the special
33                                                       #value _TOTAL
34              'footer_align'  => [ 'c', 'l', 'r', '' ],
35
36              'param'         => { column0 => 1 },  # preset column of row 0 to 1
37
38          )
39
40 Some incomplete notes for javascript programmers:
41
42 On page load, existing rows are initialized by passing values to addRow
43 based on existing cgi values.  An empty row (marked with the 'emptyrow' 
44 attribute) is created by invoking addRow without values.  After that, 
45 to keep the non-empty row count (totalrows) accurate, use newEmptyRow to 
46 create the next row.  There should only be one empty row at a time.
47
48 Global vars:
49 total_el  - element for displaying total number of rows
50 totalrows - total number of non-empty rows
51 rownum - really more of a "next row" value, used by addRow
52 allrows - array of tr elements, one for each row
53
54 Don't confuse the global rownum with the element attribute rownum
55 that is set as a reference point on some of the elements generated
56 by this script.  They have different values.
57
58 Some of the functions:
59 updateTotalRow() - updates total_el based on value of totalrows
60 addDeleteButton(searchrow) - adds delete button to searchrow
61 newEmptyRow() - replaces old empty row
62 deleteRow() - removes the row specified by this.rownum
63 addRow(values) - adds a new row (marked as empty if values aren't specified)
64
65 This mason element is currently only used by misc/batch-cust_pay.html, 
66 and probably should be cleaned up more before being used by anything else.
67
68 </%doc>
69
70 <SCRIPT TYPE="text/javascript">
71
72   var num_open_invoices = new Array;
73
74   function clearhint_invnum() {
75
76     if ( this.value == 'Not found' || this.value == 'Multiple' ) {
77       this.value = '';
78       this.style.color = '#000000';
79     }
80
81   }
82
83   function clearhint_custnum() {
84
85     if ( this.value == 'Not found' || this.value == 'Multiple' ) {
86       this.value = '';
87       this.style.color = '#000000';
88     }
89
90   }
91
92   function clearhint_customer() {
93
94     this.style.color = '#000000';
95
96     if ( this.value == '(last name or company)' || this.value == 'Not found' )
97       this.value = '';
98
99   }
100
101   function update_customer(searchrow, customerArray) {
102       
103       var display_custnum_obj = document.getElementById('display_custnum'+searchrow);
104       var custnum_obj = document.getElementById('custnum'+searchrow);
105       var customer = document.getElementById('customer'+searchrow);
106       var customer_select = document.getElementById('cust_select'+searchrow);
107
108       var postPaymentBtn = document.getElementById('btnsubmit');
109       postPaymentBtn.disabled = false;
110
111       display_custnum_obj.disabled = false;
112       display_custnum_obj.style.backgroundColor = '#ffffff';
113       customer.disabled = false;
114       customer.style.backgroundColor = '#ffffff';
115
116       if ( customerArray.length == 0 ) {
117
118           custnum_obj.value = '';
119           display_custnum_obj.value = 'Not found';
120           customer.value = 'Not found';
121           display_custnum_obj.style.color = '#ff0000';
122           customer.style.color = '#ff0000';
123
124           customer.style.display = '';
125           customer_select.style.display = 'none';
126           return false;
127
128       } else if ( customerArray.length >= 6 ) {
129
130           custnum_obj.value = customerArray[0];
131           display_custnum_obj.value = customerArray[6];
132           display_custnum_obj.style.color = '#000000';
133           customer.value = customerArray[1];
134
135           update_balance_text(searchrow, customerArray[2]);
136           update_status_text( searchrow, customerArray[3]);
137           update_status_color(searchrow, '#'+customerArray[4]);
138           update_num_open(searchrow, customerArray[5]);
139
140           customer.style.display = '';
141           customer_select.style.display = 'none';
142           return true;
143       }
144   }
145
146   function search_invnum() {
147
148     this.style.color = '#000000'
149
150     var invnum_obj = this;
151     var searchrow = this.getAttribute('rownum');
152     var invnum = this.value;
153
154     if ( invnum == 'searching...' || invnum == 'Not found' || invnum == '' )
155       return;
156
157     if ( this.getAttribute('magic') == 'nosearch' ) {
158       this.setAttribute('magic', '');
159       return;
160     }
161
162     if ( document.getElementById('row'+searchrow).emptyrow ) {
163       newEmptyRow(searchrow);
164     }
165     var customer = document.getElementById('customer'+searchrow);
166     customer.value = 'searching...';
167     customer.disabled = true;
168     customer.style.color = '#000000';
169     customer.style.backgroundColor = '#dddddd';
170
171     var customer_select = document.getElementById('cust_select'+searchrow);
172
173     customer.style.display = '';
174     customer_select.style.display = 'none';
175     
176     update_balance_text(searchrow, '');
177     update_status_text(searchrow, '');
178     update_status_color(searchrow, '#000000');
179     update_num_open(searchrow, 0);
180
181     function search_invnum_update(customers) {
182       
183       var customerArray = eval('(' + customers + ')');
184       update_customer(searchrow, customerArray);
185
186 % if ( $opt{invnum_update_callback} ) {
187         <% $opt{invnum_update_callback} %>(searchrow)
188 % }
189
190     }
191
192     invnum_search( invnum, search_invnum_update );
193
194   }
195
196   function search_custnum() {
197
198     this.style.color = '#000000'
199
200     var display_custnum_obj = this;
201     var searchrow = this.getAttribute('rownum');
202     var custnum_obj = document.getElementById('custnum'+searchrow);
203     var display_custnum = this.value;
204
205     if ( display_custnum == 'searching...' || display_custnum == 'Not found' || display_custnum == '' )
206       return;
207
208     if ( this.getAttribute('magic') == 'nosearch' ) {
209       this.setAttribute('magic', '');
210       return;
211     }
212
213     if ( document.getElementById('row'+searchrow).emptyrow ) {
214       newEmptyRow(searchrow);
215     }
216
217     var customer_obj = document.getElementById('customer'+searchrow);
218     customer_obj.value = 'searching...';
219     customer_obj.disabled = true;
220     customer_obj.style.color = '#000000';
221     customer_obj.style.backgroundColor = '#dddddd';
222
223     var customer_select = document.getElementById('cust_select'+searchrow);
224
225     customer_obj.style.display = '';
226     customer_select.style.display = 'none';
227
228     var invnum = document.getElementById('invnum'+searchrow);
229     invnum.value = '';
230
231     update_balance_text(searchrow, '');
232     update_status_text( searchrow, '');
233     update_status_color(searchrow, '#000000');    
234     update_num_open(searchrow, 0);
235
236     function search_custnum_update(customers) {
237
238       var customerArrayArray = eval('(' + customers + ')') || [];
239
240       if ( customerArrayArray.length == 0 ) {
241
242         update_customer(searchrow, []);
243
244       } else if ( customerArrayArray.length == 1 ) {
245
246         update_customer(searchrow, customerArrayArray[0]);
247 % if ( $opt{custnum_update_callback} ) {
248           <% $opt{custnum_update_callback} %>(searchrow)
249 % }
250
251       } else {
252
253         custnum_obj.value = 'Multiple'; // or something
254         custnum_obj.style.color = '#ff0000';
255
256         //blank the current list
257         customer_select.options.length = 0;
258
259         opt(customer_select, '', 'Multiple customers match "' + custnum + '" - select one', '#ff0000');
260         //add the multiple customers
261         for ( var s = 0; s < customerArrayArray.length; s++ ) {
262           opt(customer_select,
263               JSON.stringify(customerArrayArray[s]),
264               customerArrayArray[s][1],
265               '#000000');
266         }
267
268         opt(customer_select, 'cancel', '(Edit search string)', '#000000');
269
270         customer_obj.style.display = 'none';
271
272         customer_select.style.display = '';
273
274       }
275
276     }
277
278     custnum_search(display_custnum, search_custnum_update );
279
280   }
281
282   function search_customer() {
283
284     var customer_obj = this;
285     var searchrow = this.getAttribute('rownum');
286     var customer = this.value;
287
288     if ( customer == 'searching...' || customer == 'Not found' || customer == '' )
289       return;
290
291     if ( this.getAttribute('magic') == 'nosearch' ) {
292       this.setAttribute('magic', '');
293       return;
294     }
295
296     if ( document.getElementById('row'+searchrow).emptyrow ) {
297       newEmptyRow(searchrow);
298     }
299     
300     var invnum = document.getElementById('invnum'+searchrow);
301     invnum.value = '';
302
303     var custnum_obj = document.getElementById('display_custnum'+searchrow);
304     custnum_obj.value = 'searching...';
305     custnum_obj.disabled = true;
306     custnum_obj.style.color = '#000000';
307     custnum_obj.style.backgroundColor = '#dddddd';
308
309     var customer_select = document.getElementById('cust_select'+searchrow);
310     
311     function search_customer_update(customers) {
312
313       var customerArrayArray = eval('(' + customers + ')') || [ [] ];
314
315       custnum_obj.disabled = false;
316       custnum_obj.style.backgroundColor = '#ffffff';
317
318       if ( customerArrayArray.length == 0 ) {
319
320         update_customer(searchrow, []);
321
322       } else if ( customerArrayArray.length == 1 ) {
323
324         update_customer(searchrow, customerArrayArray[0]);
325 % if ( $opt{custnum_update_callback} ) {
326         <% $opt{custnum_update_callback} %>(searchrow)
327 % }
328
329       } else {
330
331         custnum_obj.value = 'Multiple'; // or something
332         custnum_obj.style.color = '#ff0000';
333
334         var postPaymentBtn = document.getElementById('btnsubmit');
335         postPaymentBtn.disabled = true;
336
337         //blank the current list
338         customer_select.options.length = 0;
339
340         opt(customer_select, '', 'Multiple customers match "' + customer + '" - select one', '#ff0000');
341         //add the multiple customers
342         for ( var s = 0; s < customerArrayArray.length; s++ ) {
343           opt(customer_select,
344               JSON.stringify(customerArrayArray[s]),
345               customerArrayArray[s][1],
346               '#000000');
347         }
348
349         opt(customer_select, 'cancel', '(Edit search string)', '#000000');
350
351         customer_obj.style.display = 'none';
352
353         customer_select.style.display = '';
354
355       }
356
357     }
358
359     smart_search( customer, search_customer_update );
360
361   }
362
363   function select_customer() {
364
365     var custnum_balance_status = this.options[this.selectedIndex].value;
366     var customer = this.options[this.selectedIndex].text;
367
368     var searchrow = this.getAttribute('rownum');
369     var display_custnum_obj = document.getElementById('display_custnum'+searchrow);
370     var custnum_obj = document.getElementById('custnum'+searchrow);
371     var customer_obj = document.getElementById('customer'+searchrow);
372     var balance_obj = document.getElementById('balance'+searchrow);
373     var status_obj = document.getElementById('status'+searchrow);
374
375     if ( custnum_balance_status == '' ) {
376
377     } else if ( custnum_balance_status == 'cancel' ) {
378
379       display_custnum_obj.value = '';
380       custnum_obj.value = '';
381       custnum_obj.style.color = '#000000';
382
383       this.style.display = 'none';
384       customer_obj.style.display = '';
385       customer_obj.focus();
386
387     } else {
388     
389       update_customer(searchrow, JSON.parse(custnum_balance_status));
390
391 % if ( $opt{custnum_update_callback} ) {
392       <% $opt{custnum_update_callback} %>(searchrow)
393 % }
394
395     }
396
397   }
398
399   function opt(what,value,text,color) {
400     var optionName = new Option(text, value, false, false);
401     optionName.style.color = color;
402     var length = what.length;
403     what.options[length] = optionName;
404   }
405
406   function update_status_text(rownum, newval) {
407     document.getElementById('status'+rownum).value = newval;
408     document.getElementById('status'+rownum+'_text').innerHTML = newval;
409   }
410
411   function update_status_color(rownum, newval) {
412     document.getElementById('statuscolor'+rownum).value = newval;
413     document.getElementById('status'+rownum+'_text').style.color = newval;
414   }
415
416   function update_balance_text(rownum, newval) {
417     document.getElementById('balance'+rownum).value = newval;
418     document.getElementById('balance'+rownum+'_text').innerHTML = newval;
419   }
420
421   function update_num_open(rownum, newval) {
422     document.getElementById('num_open'+rownum).value = newval;
423     num_open_invoices[rownum] = newval;
424   }
425
426   // updates display of total rows based on value of totalrows
427   function updateTotalRow () {
428     if ( totalrows == 1 ) {
429       total_el.innerHTML =
430         'Total '
431           + totalrows
432           + ' <% $opt{name_singular} || 'customer' %>';
433     } else {
434       total_el.innerHTML =
435         'Total '
436           + totalrows
437           + ' <% PL($opt{name_singular} || 'customer') %>';
438     }
439   }
440
441   var total_el, rownum, totalrows, allrows;
442
443   function addDeleteButton (searchrow) {
444     var td_delete = document.getElementById('delete'+searchrow);
445     var button_delete = document.createElement('INPUT');
446     button_delete.setAttribute('rownum', searchrow);
447     button_delete.setAttribute('type', 'button');
448     button_delete.setAttribute('value', 'X');
449     button_delete.onclick = deleteRow;
450     button_delete.style.color = '#ff0000';
451     button_delete.style.fontWeight = 'bold';
452     button_delete.style.paddingLeft = '2px';
453     button_delete.style.paddingRight = '2px';
454     td_delete.appendChild(button_delete);
455   }
456
457   function newEmptyRow (searchrow) {
458     // add delete button to current row
459     addDeleteButton(searchrow);
460     // mark current row as non-empty
461     var oldemptyrow = document.getElementById('row'+searchrow);
462     oldemptyrow.emptyrow = false;
463     // update totalrows
464     totalrows++
465     updateTotalRow();
466     // add a new empty row
467     addRow();
468   }
469
470   function deleteRow() {
471     var thisrownum = this.getAttribute('rownum');
472 % if ( $opt{delete_row_callback} ) {
473     // callback
474     <% $opt{delete_row_callback} %>(thisrownum);
475 % }
476     // remove the actual row
477     var thisrow = document.getElementById('row'+thisrownum);
478     thisrow.parentNode.removeChild(thisrow);
479     // remove row from tally of all rows
480     var newrows = [];
481     for (i = 0; i < allrows.length; i++) {
482       if (allrows[i] == thisrownum) continue;
483       newrows.push(allrows[i]);
484     }
485     allrows = newrows;
486     totalrows--; // should never be deleting empty rows
487     updateTotalRow();
488     // recalculate column totals, if any
489 % my $col = 0;
490 % foreach my $footer ( @{$opt{footer}} ) {
491 %   if ($footer eq '_TOTAL' ) {
492     calc_total<% $col %>()
493 %   }
494 %   $col++;
495 % }
496   }
497
498   function addRow(values) {
499
500     var table = document.getElementById('OneTrueTable');
501     var tablebody = table.getElementsByTagName('tbody').item(0);
502
503     var row = table.insertRow(table.rows.length - 1);
504     var thisrownum = values ? values.rownum : rownum;
505     row.setAttribute('id', 'row'+thisrownum);
506     row.emptyrow = values ? false : true;
507     
508     var invnum_cell = document.createElement('TD');
509
510       var invnum_input = document.createElement('INPUT');
511       invnum_input.setAttribute('name', 'invnum'+thisrownum);
512       invnum_input.setAttribute('id',   'invnum'+thisrownum);
513       invnum_input.style.textAlign = 'right';
514       invnum_input.setAttribute('size', 8);
515       invnum_input.setAttribute('maxlength', 12);
516       invnum_input.setAttribute('rownum', thisrownum);
517       invnum_input.value = values ? values.invnum : '';
518       invnum_input.onfocus = clearhint_invnum;
519       invnum_input.onchange = search_invnum;
520       invnum_cell.appendChild(invnum_input);
521
522     row.appendChild(invnum_cell);
523
524     var custnum_cell = document.createElement('TD');
525
526       var display_custnum_input = document.createElement('INPUT');
527       display_custnum_input.setAttribute('name', 'display_custnum'+thisrownum);
528       display_custnum_input.setAttribute('id',   'display_custnum'+thisrownum);
529       display_custnum_input.style.textAlign = 'right';
530       display_custnum_input.setAttribute('size', 8);
531       display_custnum_input.setAttribute('maxlength', 12);
532       display_custnum_input.setAttribute('rownum', thisrownum);
533       display_custnum_input.value = values ? values.custnum : '';
534       display_custnum_input.onfocus = clearhint_custnum;
535       display_custnum_input.onchange = search_custnum;
536       custnum_cell.appendChild(display_custnum_input);
537
538       var custnum_input = document.createElement('INPUT');
539       custnum_input.type = 'hidden';
540       custnum_input.setAttribute('name', 'custnum'+thisrownum);
541       custnum_input.setAttribute('id',   'custnum'+thisrownum);
542       custnum_input.setAttribute('rownum', thisrownum);
543       custnum_input.value = values ? values.custnum : '';
544       custnum_cell.appendChild(custnum_input);
545
546     row.appendChild(custnum_cell);
547     
548     var status_cell = document.createElement('TD');
549       status_cell.style.textAlign = 'center';
550         
551       var status_span = document.createElement('SPAN');
552       status_span.setAttribute('id', 'status'+thisrownum+'_text');
553       status_span.style.fontWeight = 'bold';
554       status_span.style.color = values ? values.statuscolor : '';
555       status_span.setAttribute('rownum', thisrownum);
556       status_span.appendChild(
557         document.createTextNode(values ? values.status : '')
558       );
559       status_cell.appendChild(status_span);
560         
561       var status_input = document.createElement('INPUT');
562       status_input.setAttribute('type', 'hidden');
563       status_input.setAttribute('name', 'status'+thisrownum);
564       status_input.setAttribute('id',   'status'+thisrownum);
565       status_input.setAttribute('rownum', thisrownum);
566       status_input.value = values ? values.status : '';
567       status_cell.appendChild(status_input);
568
569       var statuscolor_input = document.createElement('INPUT');
570       statuscolor_input.setAttribute('type', 'hidden');
571       statuscolor_input.setAttribute('name', 'statuscolor'+thisrownum);
572       statuscolor_input.setAttribute('id',   'statuscolor'+thisrownum);
573       statuscolor_input.setAttribute('rownum', thisrownum);
574       statuscolor_input.value = values ? values.statuscolor : '';
575       status_cell.appendChild(statuscolor_input);
576
577     row.appendChild(status_cell);
578
579     var customer_cell = document.createElement('TD');
580
581       var customer_input = document.createElement('INPUT');
582       customer_input.setAttribute('name', 'customer'+thisrownum);
583       customer_input.setAttribute('id',   'customer'+thisrownum);
584       customer_input.setAttribute('size', 64);
585       customer_input.setAttribute('value', '(last name or company)' );
586       customer_input.setAttribute('rownum', thisrownum);
587       customer_input.value = values ? values.customer : '';
588       customer_input.onfocus = clearhint_customer;
589       customer_input.onclick = clearhint_customer;
590       customer_input.onchange = search_customer;
591       customer_cell.appendChild(customer_input);
592
593       var customer_select = document.createElement('SELECT');
594       customer_select.setAttribute('name', 'cust_select'+thisrownum);
595       customer_select.setAttribute('id',   'cust_select'+thisrownum);
596       customer_select.setAttribute('rownum', thisrownum);
597       customer_select.style.color = '#ff0000';
598       customer_select.style.display = 'none';
599       customer_select.onchange = select_customer;
600       customer_cell.appendChild(customer_select);
601
602     row.appendChild(customer_cell);
603     
604     var balance_cell = document.createElement('TD');
605
606       balance_cell.style.textAlign = 'right';
607       balance_cell.appendChild(document.createTextNode('<%$money_char%>'));
608
609       var balance_span = document.createElement('SPAN');
610       balance_span.setAttribute('id', 'balance'+thisrownum+'_text');
611       balance_span.setAttribute('rownum', thisrownum);
612       balance_cell.appendChild(balance_span);
613
614       balance_cell.appendChild(
615         document.createTextNode(String.fromCharCode(160) + (values ? values.balance : '')) //&nbsp;
616       );
617
618       var balance_input = document.createElement('INPUT');
619       balance_input.setAttribute('type', 'hidden');
620       balance_input.setAttribute('name', 'balance'+thisrownum);
621       balance_input.setAttribute('id',   'balance'+thisrownum);
622       balance_input.setAttribute('rownum', thisrownum);
623       balance_input.value = values ? values.balance : '';
624       balance_cell.appendChild(balance_input);
625
626       var num_open_input = document.createElement('INPUT');
627       num_open_input.setAttribute('type', 'hidden');
628       num_open_input.setAttribute('name', 'num_open'+thisrownum);
629       num_open_input.setAttribute('id',   'num_open'+thisrownum);
630       num_open_input.setAttribute('rownum', thisrownum);
631       balance_cell.appendChild(num_open_input);
632
633     row.appendChild(balance_cell);
634
635 %   my $col = 0;
636 %   foreach my $field ( @{$opt{fields}} ) {
637
638       var my_cell = document.createElement('TD');
639       my_cell.setAttribute('align', '<% $align{ $opt{align}->[$col] || 'l' } %>');
640 %     if ($opt{'color'}->[$col]) {
641       my_cell.style.color = '<% $opt{color}->[$col] %>';
642 %     }
643
644 %     if ($types->[$col] eq 'immutable') {
645         var my_text = document.createTextNode(values ? values.<% $field %> : '');
646         my_cell.appendChild(my_text);
647 %     }
648
649 %     my $name  = (ref($field) eq 'CODE') ? "column${col}_" : $field;
650       var my_input = document.createElement('INPUT');
651       my_input.setAttribute('name', '<% $name %>'+thisrownum);
652       my_input.setAttribute('id',   '<% $name %>'+thisrownum);
653       my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>';
654       my_input.setAttribute('size', <% $sizes->[$col] || 10 %>);
655       my_input.setAttribute('rownum', thisrownum);
656 %     if ( $types->[$col] eq 'immutable' ) {
657       my_input.setAttribute('type', 'hidden');
658 %     } elsif ( $types->[$col] eq 'checkbox' ) {
659       my_input.setAttribute('type', 'checkbox');
660       my_input.checked = (values && values.<% $field %>) ? true : false;
661 %     }
662       my_input.value = (values && values.<% $field %>) || '';
663 %     if ( $opt{onchange}->[$col] ) {
664         my_input.onchange   = <% $opt{onchange}->[$col] %>;
665 %     }
666 %     elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
667         my_input.onchange   = calc_total<%$col%>;
668         my_input.onkeyup    = calc_total<%$col%>;
669 %     }
670       my_cell.appendChild(my_input);
671
672     row.appendChild(my_cell);
673
674 %     $col++;
675 %   }
676
677     var td_delete = document.createElement('TD');
678     td_delete.setAttribute('id', 'delete'+thisrownum);
679     row.appendChild(td_delete);
680     if (values) {
681       addDeleteButton(thisrownum);
682     }
683
684     update_num_open(thisrownum, (values ? values.num_open : '0'));
685
686 % if ( $opt{add_row_callback} ) {
687     <% $opt{add_row_callback} %>(thisrownum, values);
688 % }
689
690     // update the total number of rows display
691     allrows.push(thisrownum);
692     if (values) totalrows++;
693     updateTotalRow();
694
695     // update the next available row number
696     if (thisrownum >= rownum) {
697       rownum = thisrownum + 1;
698     }
699
700   } // end of addRow
701
702
703 </SCRIPT>
704
705 <TABLE ID="OneTrueTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
706
707 <TR>
708   <TH>Inv #</TH>
709   <TH>Cust #</TH>
710   <TH>Status</TH>
711   <TH>Customer</TH>
712   <TH>Balance</TH>
713 % foreach my $header ( @{$opt{header}} ) {
714     <TH><% $header %></TH>
715 % }
716 </TR>
717
718 % my @rownums = sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param;
719 <TR id="row_total">
720   <TH COLSPAN=5 ID="_TOTAL_TOTAL">
721     Total <% @rownums || 0 %>
722     <% PL($opt{name_singular} || 'customer', ( @rownums || 0 ) ) %>
723   </TH>
724 % my $col = 0;
725 % foreach my $footer ( @{$opt{footer}} ) {
726 %   my $align = $align{ $opt{'footer_align'}->[$col] || 'c' };
727 %   if ($footer eq '_TOTAL' ) {
728 %     my $id = $opt{'fields'}->[$col];
729 %     $id = ref($id) ? "column${col}_TOTAL" : "${id}_TOTAL";
730       <TH ALIGN="<% $align %>" ID="<% $id %>">&nbsp;<% sprintf('%.2f', $total[$col] ) %></TH>
731 %   } else {
732       <TH ALIGN="<% $align %>"><% $footer %></TH>
733 %   }
734 %   $col++;
735 % }
736 </TR>
737
738 </TABLE>
739
740 <SCRIPT TYPE="text/javascript">
741
742 total_el =
743   document.getElementById("_TOTAL_TOTAL");
744
745 rownum = 1; // really more of a "next row", used by addrow
746 totalrows = 0; // will not include empty rows
747 allrows = []; // will include empty rows
748
749 % foreach my $row ( @rownums ) {
750 %   if ( grep($param->{$_.$row},qw(invnum display_custnum custnum status statuscolor customer balance),@{$opt{fields}} ) ) {
751
752 addRow({
753   rownum:<% $row %>,
754   num_open:<% $param->{"num_open$row"} |js_string %>,
755   invnum:<% $param->{"invnum$row"} |js_string %>,
756   display_custnum:<% $param->{"display_custnum$row"} |js_string %>,
757   custnum:<% $param->{"custnum$row"} |js_string %>,
758   status:<% $param->{"status$row"} |js_string %>,
759   statuscolor:<% $param->{"statuscolor$row"} |js_string %>,
760   customer:<% $param->{"customer$row"} |js_string %>,
761   balance:<% $param->{"balance$row"} |js_string %>,
762 %     my $col = 0;
763 %     foreach my $field ( @{$opt{fields}} ) {
764 %       my $value;
765 %       if ( ref($field) eq 'CODE' ) {
766 %         $value = &{$field}($row,$param) || '';
767 %       } else {
768 %         $value = $param->{"$field$row"} || '';
769 %       }
770 %       my $name  = (ref($field) eq 'CODE') ? "column${col}" : "$field";
771   <% $name %>:<% $value |js_string %>,
772 %       $col++;
773 %     }
774 });
775 %   }
776 % }
777
778 addRow();
779
780 % my $col = 0;
781 % foreach my $footer ( @{$opt{footer}} ) {
782 %   if ($footer eq '_TOTAL' ) {
783 %     my $name = $opt{fields}->[$col];
784 %     $name = ref($name) ? "column$col" : $name;
785       var th_el = document.getElementById("<%$name%>_TOTAL");
786       function calc_total<% $col %>() {
787         var row = 0;
788         var total = 0;
789         for (i = 0; i < allrows.length; i++) {
790           var value = document.getElementById("<%$name%>"+allrows[i]).value;
791           value = parseFloat(value);
792           if ( ! isNaN(value) ) {
793             total = total + value;
794           }
795         }
796         th_el.innerHTML = '&nbsp;' + total.toFixed(2);
797       }
798       calc_total<% $col %>()
799 %   }
800 %   $col++;
801 % }
802 </SCRIPT>
803
804 <% include('/elements/xmlhttp.html',
805               'url'  => $p. 'misc/xmlhttp-cust_main-search.cgi',
806               'subs' => [qw( custnum_search smart_search invnum_search )],
807            )
808 %>
809
810 <%init>
811
812 my(%opt) = @_;
813 my $conf = new FS::Conf;
814
815 my $types = $opt{'type'} ? [ @{$opt{'type'}} ] : [];
816 my $sizes = $opt{'size'} ? [ @{$opt{'size'}} ] : [];
817
818 my $param = $opt{param};
819 $param = $cgi->Vars if $cgi->param('error');
820
821 $opt{$_} ||= [] foreach qw(align color footer footer_align);
822
823 my @total = map 0, @{$opt{footer}};
824
825 my %align = (
826   'l' => 'left',
827   'r' => 'right',
828   'c' => 'center',
829 );
830
831 my $money_char = $conf->config('money_char') || '$';
832 </%init>