Bug 35241: Fix markup errors in point of sale template
[koha.git] / koha-tmpl / intranet-tmpl / prog / en / modules / tools / quotes-upload.tt
1 [% USE raw %]
2 [% USE Asset %]
3 [% PROCESS 'i18n.inc' %]
4 [% SET footerjs = 1 %]
5     [% INCLUDE 'doc-head-open.inc' %]
6     <title>[% FILTER collapse %]
7         [% t("Quote uploader") | html %] &rsaquo;
8         [% t("Quote editor") | html %] &rsaquo;
9         [% t("Tools") | html %] &rsaquo;
10         [% t("Koha") | html %]
11     [% END %]</title>
12     [% INCLUDE 'doc-head-close.inc' %]
13     [% Asset.css("css/uploader.css") | $raw %]
14     <style>
15     .jeditable-text {
16         display: block;
17         height: 5em;
18         margin-bottom: .5em;
19         min-width: 20em;
20         width: 70%;
21     }
22     </style>
23 </head>
24
25 <body id="tools_quotes" class="tools">
26 [% WRAPPER 'header.inc' %]
27     [% INCLUDE 'cat-search.inc' %]
28 [% END %]
29
30 [% WRAPPER 'sub-header.inc' %]
31     [% WRAPPER breadcrumbs %]
32         [% WRAPPER breadcrumb_item %]
33             <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a>
34         [% END %]
35         [% WRAPPER breadcrumb_item %]
36             <a href="/cgi-bin/koha/tools/quotes.pl">Quote editor</a>
37         [% END %]
38         [% WRAPPER breadcrumb_item bc_active= 1 %]
39             <span>Quote uploader</span>
40         [% END %]
41     [% END #/ WRAPPER breadcrumbs %]
42 [% END #/ WRAPPER sub-header.inc %]
43
44 <div class="main container-fluid">
45     <div class="row">
46         <div class="col-sm-10 col-sm-push-2">
47             <main>
48                 <div id="toolbar" class="btn-toolbar" style="display:none">
49                     <div class="btn-group"><a class="btn btn-primary" id="save_quotes" href="#"><i class="fa fa-save"></i> Save quotes</a></div>
50                     <div class="btn-group"><a href="/cgi-bin/koha/tools/quotes-upload.pl" id="cancel_quotes" class="btn btn-default"><i class="fa fa-times"></i> Cancel import</a></div>
51                 </div>
52
53                 <h1>Quote uploader</h1>
54
55                 <div id="messages" style="display: none;">
56                     <div class="import_success dialog message" style="display: none;"></div>
57                     <div class="import_error dialog alert" style="display: none;"></div>
58                 </div>
59
60                 <div id="instructions" class="page-section">
61                     <h2>Instructions</h2>
62                     <div id="file_uploader_inst">
63                         <ul>
64                         <li>The quote uploader accepts standard csv files with two columns: "source","text"</li>
65                         <li>Click the "Choose file" button and select the csv file to be uploaded.</li>
66                         <li>The file will be imported into an editable table for review prior to saving.</li>
67                         </ul>
68                     </div>
69                     <div id="file_editor_inst" style="display:none">
70                         <ul>
71                         <li>Click on any field to edit the contents; Press the &lt;Enter&gt; key to save edit.</li>
72                         <li>Click the 'Save quotes' button in the toolbar to save the entire batch of quotes.</li>
73                         </ul>
74                     </div>
75                 </div>
76
77                 <fieldset id="file_uploader" class="rows">
78                     <legend>Upload quotes</legend>
79                     <ol>
80                         <li>
81                                 <label for="uploadfile">Select the file to upload: </label>
82                                 <div id="file_upload">
83                                     <input type="file" name="file" />
84                                     <button id="cancel_upload" style="display:none">Cancel upload</button>
85                                     <div id="progress_bar"><div class="percent">0%</div></div>
86                                 </div>
87                         </li>
88                     </ol>
89                 </fieldset>
90
91                 <div class="page-section">
92                     <table id="quotes_editor" style="display:none">
93                     </table>
94                 </div>
95
96                 <fieldset id="footer" class="action" style="display:none">
97                 </fieldset>
98
99             </main>
100         </div> <!-- /.col-sm-10.col-sm-push-2 -->
101
102         <div class="col-sm-2 col-sm-pull-10">
103             <aside>
104                 [% INCLUDE 'tools-menu.inc' %]
105             </aside>
106         </div> <!-- /.col-sm-2.col-sm-pull-10 -->
107      </div> <!-- /.row -->
108
109 [% MACRO jsinclude BLOCK %]
110     [% Asset.js("js/tools-menu.js") | $raw %]
111     [% INCLUDE 'datatables.inc' %]
112     [% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %]
113     <script>
114         var oTable; //DataTable object
115         $(document).ready(function() {
116
117             $("#cancel_upload").on("click",function(e){
118                 e.preventDefault();
119                 fnAbortRead();
120             });
121             $("#cancel_quotes").on("click",function(){
122                 return confirm( _("Are you sure you want to cancel this import?") );
123             });
124
125         // Credits:
126         // FileReader() code copied and hacked from:
127         // http://www.html5rocks.com/en/tutorials/file/dndfiles/
128         // fnCSVToArray() gratefully borrowed from:
129         // http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
130
131         var reader;
132         var progress = document.querySelector('.percent');
133         $("#server_response").hide();
134
135         function yuiGetData() {
136             fnGetData(document.getElementById('quotes_editor'));
137         }
138
139         function fnAbortRead() {
140             reader.abort();
141         }
142
143         function fnErrorHandler(evt) {
144             switch(evt.target.error.code) {
145                 case evt.target.error.NOT_FOUND_ERR:
146                     alert(_("File not found!"));
147                     break;
148                 case evt.target.error.NOT_READABLE_ERR:
149                     alert(_("File is not readable"));
150                     break;
151                 case evt.target.error.ABORT_ERR:
152                     break; // noop
153                 default:
154                     alert(_("An error occurred reading this file."));
155             };
156         }
157
158         function fnUpdateProgress(evt) {
159             // evt is an ProgressEvent.
160             if (evt.lengthComputable) {
161                 var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
162                 // Increase the progress bar length.
163                 if (percentLoaded < 100) {
164                     progress.style.width = percentLoaded + '%';
165                     progress.textContent = percentLoaded + '%';
166                 }
167             }
168         }
169
170         function fnCSVToArray( strData, strDelimiter ){
171             // This will parse a delimited string into an array of
172             // arrays. The default delimiter is the comma, but this
173             // can be overriden in the second argument.
174
175             // Check to see if the delimiter is defined. If not,
176             // then default to comma.
177             strDelimiter = (strDelimiter || ",");
178
179             strData = escape_str(strData);
180
181             // Create a regular expression to parse the CSV values.
182             var objPattern = new RegExp(
183             (
184                 // Delimiters.
185                 "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
186                 // Quoted fields.
187                 "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
188                 // Standard fields.
189                 "([^\"\\" + strDelimiter + "\\r\\n]*))"
190             ),
191                 "gi"
192             );
193
194             // Create an array to hold our data. Give the array
195             // a default empty first row.
196             var arrData = [[]];
197
198             // Create an array to hold our individual pattern
199             // matching groups.
200             var arrMatches = null;
201
202             // Keep looping over the regular expression matches
203             // until we can no longer find a match.
204             while (arrMatches = objPattern.exec( strData )){
205
206                 // Get the delimiter that was found.
207                 var strMatchedDelimiter = arrMatches[ 1 ];
208
209                 // Check to see if the given delimiter has a length
210                 // (is not the start of string) and if it matches
211                 // field delimiter. If it does not, then we know
212                 // that this delimiter is a row delimiter.
213                 if ( strMatchedDelimiter.length && (strMatchedDelimiter != strDelimiter) ){
214                     // Since we have reached a new row of data,
215                     // add an empty row to our data array.
216                     // Note: if there is not more data, we will have to remove this row later
217                     arrData.push( [] );
218                 }
219
220                 // Now that we have our delimiter out of the way,
221                 // let's check to see which kind of value we
222                 // captured (quoted or unquoted).
223                 if (arrMatches[ 2 ]){
224                     // We found a quoted value. When we capture
225                     // this value, unescape any double quotes.
226                     var strMatchedValue = arrMatches[ 2 ].replace(
227                     new RegExp( "\"\"", "g" ),
228                         "\""
229                     );
230                 } else if (arrMatches[3]){
231                     // We found a non-quoted value.
232                     var strMatchedValue = arrMatches[ 3 ];
233                 } else {
234                     // There is no more valid data so remove the row we added earlier
235                     // Is there a better way? Perhaps a look-ahead regexp?
236                     arrData.splice(arrData.length-1, 1);
237                 }
238
239                 // Now that we have our value string, let's add
240                 // it to the data array.
241                 if ( arrData[ arrData.length - 1 ] ) {
242                     arrData[ arrData.length - 1 ].push( strMatchedValue );
243                 } else {
244                     $("#messages .import_error").text(_("Something went wrong, check your CSV file."));
245                 }
246             }
247
248             // Return the parsed data.
249             return( arrData );
250         }
251
252         function fnDataTable(aaData) {
253             for(var i=0; i<aaData.length; i++) {
254                 aaData[i].unshift(i+1); // Add a column w/quote number
255             }
256
257             /* Transition from the quote file uploader to the quote file editor interface */
258             $('#toolbar').show();
259             $('#file_editor_inst').show();
260             $('#file_uploader_inst').hide();
261             $('#save_quotes').show();
262             $('#file_uploader').hide();
263             $('#quotes_editor').show();
264             $("#save_quotes").on("click", yuiGetData);
265
266             oTable = $('#quotes_editor').dataTable( {
267                 "bAutoWidth"        : false,
268                 "bPaginate"         : true,
269                 "bSort"             : false,
270                 "sPaginationType"   : "full_numbers",
271                 "sDom": '<"top pager"iflp>rt<"bottom pager"flp><"clear">',
272                 "aaData"            : aaData,
273                 "pageLength": 20,
274                 "fixedHeader": true,
275                 "aoColumns"         : [
276                     {
277                         "sTitle"  : _("Number"),
278                         "sWidth"  : "2%",
279                     },
280                     {
281                         "sTitle"  : _("Source"),
282                         "sWidth"  : "15%",
283                     },
284                     {
285                         "sTitle"  : _("Quote"),
286                         "sWidth"  : "83%",
287                     },
288                 ],
289                "fnPreDrawCallback": function(oSettings) {
290                     return true;
291                 },
292                 "fnRowCallback": function( nRow, aData, iDisplayIndex ) {
293                     /* do foo on various cells in the current row */
294                     var quoteNum = $('td', nRow)[0].innerHTML;
295                     $(nRow).attr("id", quoteNum); /* set row ids to quote number */
296                     /* apply no_edit id to noEditFields */
297                     noEditFields = [0]; /* number */
298                     for (i=0; i<noEditFields.length; i++) {
299                         $('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
300                     }
301                     return nRow;
302                 },
303                "fnDrawCallback": function(oSettings) {
304                     /* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
305                     $('#quotes_editor tbody td[id!="no_edit"]').editable( function(value, settings) {
306                             value = escape_str(value);
307                             var cellPosition = oTable.fnGetPosition( this );
308                             oTable.fnUpdate(value, cellPosition[0], cellPosition[1], false, false);
309                             return(value);
310                         },
311                         {
312                             "callback"      : function( sValue, y ) {
313                                 oTable.fnDraw(false); /* no filter/sort or we lose our pagination */
314                             },
315                             "cancel" : _("Cancel"),
316                             "height": "none",
317                             "inputcssclass": "jeditable-text",
318                             "onblur": "ignore",
319                             "style": "",
320                             "submit" : _("Save"),
321                             "type": "textarea",
322                             "width": "none",
323                         }
324                     );
325                },
326             });
327             $('#footer').show();
328         }
329
330         function fnHandleFileSelect(evt) {
331             // Reset progress indicator on new file selection.
332             progress.style.width = '0%';
333             progress.textContent = '0%';
334
335             reader = new FileReader();
336             reader.onerror = fnErrorHandler;
337             reader.onprogress = fnUpdateProgress;
338             reader.onabort = function(e) {
339                 alert(_("File read cancelled"));
340                 parent.location='quotes-upload.pl';
341             };
342             reader.onloadstart = function(e) {
343                 $('#cancel_upload').show();
344                 $('#progress_bar').addClass("loading");
345             };
346             reader.onload = function(e) {
347                 // Ensure that the progress bar displays 100% at the end.
348                 progress.style.width = '100%';
349                 progress.textContent = '100%';
350                 $('#cancel_upload').hide();
351                 quotes = fnCSVToArray(e.target.result, ',');
352                 fnDataTable(quotes);
353             }
354
355             // perform various sanity checks on the target file prior to uploading...
356             var fileType = evt.target.files[0].type || 'unknown';
357             var fileSizeInK = Math.round(evt.target.files[0].size/1024);
358
359             if (!fileType.match(/comma-separated-values|csv|excel|calc/i)) {
360                 alert(_("Uploads limited to csv. Incorrect filetype: %s").format(fileType));
361                 parent.location='quotes-upload.pl';
362                 return;
363             }
364             if (fileSizeInK > 512) {
365                 if (!confirm(_("%s %s KB Do you really want to upload this file?").format(evt.target.files[0].name, fileSizeInK))) {
366                     parent.location='quotes-upload.pl';
367                     return;
368                 }
369             }
370             // Read in the image file as a text string.
371             reader.readAsText(evt.target.files[0]);
372         }
373
374         $('#file_upload').on('change', fnHandleFileSelect);
375
376         var MSG_IMPORT_SUCCESS = _("%s quotes imported successfully");
377         var MSG_IMPORT_ERROR   = _("%s quotes have not been imported. An error occurred");
378         function fnGetData(element) {
379             var lines = oTable.fnGetData();
380             $(lines).each(function(line){
381                 var s = this[1].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
382                 var t = this[2].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
383                 var data = {source: s, text: t};
384                 var success = 0; var error = 0;
385                 $.ajax({
386                     url      : "/api/v1/quotes",
387                     method   : "POST",
388                     data     : JSON.stringify(data),
389                     dataType : "application/json",
390                     success  : function(data) {
391                         $("#messages").addClass("message").show();
392                         var import_success = $("#messages .import_success");
393                         import_success.show();
394                         import_success.data("nb")
395                         nb_success = import_success.data("nb") || 0;
396                         nb_success++;
397                         $("#messages .import_success").text(MSG_IMPORT_SUCCESS.format(nb_success));
398                         import_success.data("nb", nb_success);
399                     },
400                     error    : function(xhr) {
401                         if (xhr.status==201) { this.success(null, "Created", xhr); return; }
402                         $("#messages").addClass("alert").show();
403                         var import_error = $("#messages .import_error");
404                         import_error.show();
405                         import_error.data("nb")
406                         nb_error = import_error.data("nb") || 0;
407                         nb_error++;
408                         $("#messages .import_error").text(MSG_IMPORT_ERROR.format(nb_error));
409                         import_error.data("nb", nb_error);
410                     },
411                 });
412             });
413         }
414
415         }); // $(document).ready
416     </script>
417 [% END %]
418
419 [% INCLUDE 'intranet-bottom.inc' %]