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