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