1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2011, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2011, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Simple dialog allowing user to choose between an Instance or Series for an appointment 26 * @constructor 27 * @class 28 * 29 * @author Santosh Sutar 30 * @param parent the element that created this view 31 * 32 * 33 * @extends DwtDialog 34 * @private 35 */ 36 ZmCalPrintDialog = function(params) { 37 38 var print = new DwtDialog_ButtonDescriptor(ZmCalPrintDialog.PRINT_BUTTON, ZmMsg.print, DwtDialog.ALIGN_RIGHT); 39 var cancel = new DwtDialog_ButtonDescriptor(ZmCalPrintDialog.PRINT_CANCEL_BUTTON, ZmMsg.cancel, DwtDialog.ALIGN_RIGHT); 40 var parent = params.parent || appCtxt.getShell(); 41 ZmDialog.call(this, {parent:parent, standardButtons:[DwtDialog.NO_BUTTONS], extraButtons: [print, cancel]}); 42 43 this.setButtonListener(ZmCalPrintDialog.PRINT_BUTTON, new AjxListener(this, this._printButtonListener)); 44 this.setButtonListener(ZmCalPrintDialog.PRINT_CANCEL_BUTTON, new AjxListener(this, this._printCancelButtonListener)); 45 this.setTitle(ZmMsg.printCalendar); 46 this.setContent(this._setHtml()); 47 this._createControls(); 48 this._setViewOptions(); 49 }; 50 51 ZmCalPrintDialog.prototype = new ZmDialog; 52 ZmCalPrintDialog.prototype.constructor = ZmCalPrintDialog; 53 54 ZmCalPrintDialog.PRINT_BUTTON = ++DwtDialog.LAST_BUTTON; 55 ZmCalPrintDialog.PRINT_CANCEL_BUTTON = ++DwtDialog.LAST_BUTTON; 56 ZmCalPrintDialog.DATE_FORMAT = "yyyyMMddTHHmmss"; 57 ZmCalPrintDialog.TIME_FORMAT = "HH:mm"; 58 59 // Public methods 60 61 ZmCalPrintDialog.prototype.toString = 62 function() { 63 return "ZmCalPrintDialog"; 64 }; 65 66 ZmCalPrintDialog.prototype.popup = 67 function(params) { 68 //this._keyPressedInField = false; //see comment in _handleKeyUp 69 70 // use reasonable defaults 71 params = params || {}; 72 73 var treeIds = this._treeIds = (params.treeIds && params.treeIds.length) 74 ? params.treeIds : [ZmOrganizer.FOLDER]; 75 76 //Omit the trash form the tree view 77 var omitParam = {}; 78 omitParam[ZmOrganizer.ID_TRASH] = true; 79 80 var popupParams = { 81 treeIds: treeIds, 82 omit: omitParam, 83 fieldId: this._htmlElId + "_calTreeContainer", 84 overviewId: params.overviewId, 85 noRootSelect: params.noRootSelect, 86 treeStyle: params.treeStyle || DwtTree.SINGLE_STYLE, // we don't want checkboxes! 87 appName: params.appName, 88 selectable: false, 89 forceSingle: params.forceSingle 90 }; 91 92 // make sure the requisite packages are loaded 93 var treeIdMap = {}; 94 for (var i = 0; i < treeIds.length; i++) { 95 treeIdMap[treeIds[i]] = true; 96 } 97 var ov = this._setOverview(popupParams, popupParams.forceSingle); 98 ZmDialog.prototype.popup.call(this); 99 100 this.currentViewId = params.currentViewId; 101 var cv = ZmCalViewController.VIEW_TO_OP[params.currentViewId]; 102 if(cv == ZmOperation.WORK_WEEK_VIEW) { 103 cv = ZmOperation.WEEK_VIEW; 104 } 105 else if (cv == ZmOperation.FB_VIEW) { 106 cv = ZmOperation.DAY_VIEW; 107 } 108 this._viewSelect.setSelectedValue(cv); 109 this._setViewOptions(); 110 this.setWorkingHours(params.workHours); 111 this._selDate.setValue(params.currentDate); 112 this._dateRangeFrom.setValue(new Date(params.timeRange.start)); 113 this._dateRangeTo.setValue(new Date(params.timeRange.end)); 114 if(ZmId.VIEW_CAL_WORK_WEEK == this.currentViewId) { 115 document.getElementById(this._htmlElId + "_workDaysOnly").checked = true; 116 } 117 }; 118 119 ZmCalPrintDialog.prototype.setWorkingHours = 120 function(wHrs) { 121 var fromTime = new Date(), 122 toTime = new Date(); 123 fromTime.setHours(wHrs.startTime[0]/100, wHrs.startTime[0]%100, 0, 0); 124 toTime.setHours(wHrs.endTime[0]/100, wHrs.endTime[0]%100, 0, 0); 125 this._fromTimeSelect.set(fromTime); 126 this._toTimeSelect.set(toTime); 127 }; 128 129 // Private / protected methods 130 131 ZmCalPrintDialog.prototype._setHtml = 132 function() { 133 var html = AjxTemplate.expand("calendar.Calendar#PrintDialog", {id: this._htmlElId}); 134 return html; 135 }; 136 137 ZmCalPrintDialog.prototype._createControls = 138 function() { 139 var i, 140 op, 141 list, 142 dateRangeRadio = document.getElementById(this._htmlElId + "_dateRangeRadio"), 143 selDateRadio = document.getElementById(this._htmlElId + "_selDateRadio"), 144 radioListener = AjxCallback.simpleClosure(this._setSelectedDateRadioListener, this); 145 this._selDate = new ZmDateInput(this, this._htmlElId + "_selDate", this._htmlElId + "_selDateContainer"); 146 147 this._todayButton = new DwtButton({parent:this, parentElement:this._htmlElId + "_todayButtonContainer"}); 148 this._todayButton.setText(ZmMsg.today); 149 this._todayButton.addSelectionListener(new AjxListener(this, this._setDateToToday)) 150 151 this._dateRangeFrom = new ZmDateInput(this, this._htmlElId + "_dateRangeFrom", this._htmlElId + "_dateRangeFromContainer"); 152 this._dateRangeTo = new ZmDateInput(this, this._htmlElId + "_dateRangeTo", this._htmlElId + "_dateRangeToContainer"); 153 154 this._viewSelect = new DwtSelect({parent:this, parentElement:this._htmlElId + "_printViewContainer"}); 155 list = [ 156 ZmOperation.DAY_VIEW, ZmOperation.WEEK_VIEW, 157 ZmOperation.MONTH_VIEW, ZmOperation.CAL_LIST_VIEW 158 ]; 159 for(i=0; i<list.length; i++) { 160 op = ZmOperation.defineOperation(list[i]); 161 this._viewSelect.addOption(op.text, false, op.id, op.image); 162 } 163 164 this._viewSelect.addChangeListener(new AjxListener(this, this._setViewOptions)); 165 166 this._fromTimeSelect = new DwtTimeInput(this, DwtTimeInput.START, this._htmlElId + "_fromHoursContainer"); 167 this._toTimeSelect = new DwtTimeInput(this, DwtTimeInput.END, this._htmlElId + "_toHoursContainer"); 168 this._printErrorMsgContainer = document.getElementById(this._htmlElId + "_printErrorMsgContainer"); 169 170 dateRangeRadio.onclick = radioListener; 171 selDateRadio.onclick = radioListener; 172 }; 173 174 ZmCalPrintDialog.prototype._setSelectedDateRadioListener = 175 function(ev) { 176 var target = DwtUiEvent.getTarget(ev); 177 if(target.id == this._htmlElId + "_selDateRadio") { 178 this._setSelectedDateEnabled(true); 179 } 180 else { 181 this._setSelectedDateEnabled(false); 182 } 183 }; 184 185 ZmCalPrintDialog.prototype._setDateToToday = 186 function(ev) { 187 var d = new Date(); 188 this._selDate.setValue(d); 189 }; 190 191 ZmCalPrintDialog.prototype._setSelectedDateEnabled = 192 function(enabled) { 193 var dateRangeRadio = document.getElementById(this._htmlElId + "_dateRangeRadio"), 194 selDateRadio = document.getElementById(this._htmlElId + "_selDateRadio"); 195 if(enabled) { 196 //Disable the date range controls 197 this._dateRangeFrom.setEnabled(false); 198 this._dateRangeTo.setEnabled(false); 199 dateRangeRadio.checked = false; 200 //dateRangeRadio.disabled = true; 201 202 //Enable selected date controls 203 this._selDate.setEnabled(true); 204 this._todayButton.setEnabled(true); 205 selDateRadio.checked = true; 206 //selDateRadio.disabled = false; 207 } 208 else { 209 //Enable date range controls 210 this._dateRangeFrom.setEnabled(true); 211 this._dateRangeTo.setEnabled(true); 212 dateRangeRadio.checked = true; 213 //dateRangeRadio.disabled = false; 214 215 //Disable selected date controls 216 this._selDate.setEnabled(false); 217 this._todayButton.setEnabled(false); 218 selDateRadio.checked = false; 219 //selDateRadio.disabled = true; 220 } 221 }; 222 223 ZmCalPrintDialog.prototype._validateDateTime = 224 function(ev) { 225 var hoursContainer = document.getElementById(this._htmlElId + "_hoursContainer"), 226 isValid = false; 227 228 if (this._selDate.getEnabled()) { //If we have choosen "Selected Date" 229 isValid = this._selDate.getValue(); 230 } 231 else if (this._dateRangeFrom.getEnabled() && 232 this._dateRangeTo.getEnabled()) { //If we have choosen "Date Range" 233 var startDate = this._dateRangeFrom.getTimeValue(); 234 var endDate = this._dateRangeTo.getTimeValue(); 235 isValid = startDate && endDate && endDate >= startDate; 236 } 237 238 if(isValid //If either "Selected Date" or "Date Range" is correct then we go for time validation 239 && Dwt.getVisible(hoursContainer) 240 && (this._selDate.getEnabled() || (startDate === endDate))) { //Only if dates are same does Time comparison matter 241 var startTime = this._fromTimeSelect.getValue(); 242 var endTime = this._toTimeSelect.getValue(); 243 244 if(endTime < startTime) { 245 isValid = false; 246 } 247 } 248 249 if(!isValid) { 250 Dwt.setDisplay(this._printErrorMsgContainer, Dwt.DISPLAY_BLOCK); 251 this._printErrorMsgContainer.innerHTML = ZmMsg.errorInvalidDates; 252 } 253 return isValid; 254 }; 255 256 ZmCalPrintDialog.prototype._setViewOptions = 257 function(ev) { 258 var val = this._viewSelect.getValue(); 259 260 var workDaysOnlyContainer = document.getElementById(this._htmlElId + "_workDaysOnlyContainer"); 261 var oneWeekPerPageContainer = document.getElementById(this._htmlElId + "_oneWeekPerPageContainer"); 262 var oneDayPerPageContainer = document.getElementById(this._htmlElId + "_oneDayPerPageContainer"); 263 var includeMiniCalContainer = document.getElementById(this._htmlElId + "_includeMiniCalContainer"); 264 var hoursContainer = document.getElementById(this._htmlElId + "_hoursContainer"); 265 266 267 Dwt.setDisplay(includeMiniCalContainer, Dwt.DISPLAY_BLOCK); 268 Dwt.setDisplay(hoursContainer, Dwt.DISPLAY_BLOCK); 269 this._resetCheckboxes(false); 270 271 switch(val) { 272 case ZmOperation.FB_VIEW: 273 case ZmOperation.DAY_VIEW: 274 Dwt.setDisplay(workDaysOnlyContainer, Dwt.DISPLAY_NONE); 275 Dwt.setDisplay(oneWeekPerPageContainer, Dwt.DISPLAY_NONE); 276 Dwt.setDisplay(oneDayPerPageContainer, Dwt.DISPLAY_BLOCK); 277 278 this._setSelectedDateEnabled(true); 279 break; 280 281 case ZmOperation.WORK_WEEK_VIEW: 282 case ZmOperation.WEEK_VIEW: 283 Dwt.setDisplay(workDaysOnlyContainer, Dwt.DISPLAY_BLOCK); 284 Dwt.setDisplay(oneWeekPerPageContainer, Dwt.DISPLAY_BLOCK); 285 Dwt.setDisplay(oneDayPerPageContainer, Dwt.DISPLAY_NONE); 286 287 this._setSelectedDateEnabled(false); 288 break; 289 290 case ZmOperation.MONTH_VIEW: 291 Dwt.setDisplay(workDaysOnlyContainer, Dwt.DISPLAY_BLOCK); 292 Dwt.setDisplay(oneWeekPerPageContainer, Dwt.DISPLAY_NONE); 293 Dwt.setDisplay(oneDayPerPageContainer, Dwt.DISPLAY_NONE); 294 Dwt.setDisplay(hoursContainer, Dwt.DISPLAY_NONE); 295 296 this._setSelectedDateEnabled(false); 297 break; 298 299 case ZmOperation.CAL_LIST_VIEW: 300 Dwt.setDisplay(workDaysOnlyContainer, Dwt.DISPLAY_NONE); 301 Dwt.setDisplay(oneWeekPerPageContainer, Dwt.DISPLAY_NONE); 302 Dwt.setDisplay(oneDayPerPageContainer, Dwt.DISPLAY_NONE); 303 Dwt.setDisplay(hoursContainer, Dwt.DISPLAY_NONE); 304 305 this._setSelectedDateEnabled(false); 306 break; 307 } 308 }; 309 310 311 ZmCalPrintDialog.prototype._printCancelButtonListener = 312 function() { 313 this.popdown(); 314 }; 315 316 ZmCalPrintDialog.prototype._printButtonListener = 317 function() { 318 if(!this._validateDateTime()) { 319 return false; 320 } 321 var url = this._getPrintOptions(); 322 this.popdown(); 323 window.open(url, "_blank"); 324 }; 325 326 ZmCalPrintDialog.prototype.popdown = 327 function() { 328 Dwt.setDisplay(this._printErrorMsgContainer, Dwt.DISPLAY_NONE); 329 this._resetCheckboxes(false); 330 DwtDialog.prototype.popdown.call(this); 331 }; 332 333 ZmCalPrintDialog.prototype._resetCheckboxes = 334 function(value) { 335 document.getElementById(this._htmlElId + "_workDaysOnly").checked = value; 336 document.getElementById(this._htmlElId + "_oneWeekPerPage").checked = value; 337 document.getElementById(this._htmlElId + "_oneDayPerPage").checked = value; 338 document.getElementById(this._htmlElId + "_includeMiniCal").checked = value; 339 }; 340 341 ZmCalPrintDialog.prototype._getPrintViewName = 342 function(view) { 343 var viewStyle; 344 switch (view) { 345 case ZmId.VIEW_CAL_DAY: viewStyle = "day"; break; 346 case ZmId.VIEW_CAL_WORK_WEEK: viewStyle = "workWeek"; break; 347 case ZmId.VIEW_CAL_WEEK: viewStyle = "week"; break; 348 case ZmId.VIEW_CAL_LIST: viewStyle = "list"; break; 349 default: viewStyle = "month"; break; // default is month 350 } 351 return viewStyle; 352 }; 353 354 ZmCalPrintDialog.prototype._getPrintOptions = 355 function() { 356 var cals, 357 calIds = [], 358 treeView, 359 i=0, 360 j=0, 361 params = [], 362 printURL = "", 363 selDate = this._selDate.getEnabled() ? this._selDate.getValue() : "", 364 dateRangeFrom = this._dateRangeFrom.getEnabled() ? this._dateRangeFrom.getValue() : new Date(selDate.getTime()), 365 dateRangeTo = this._dateRangeTo.getEnabled() ? this._dateRangeTo.getValue() : new Date(selDate.getTime()), 366 fromTime = AjxDateFormat.format(ZmCalPrintDialog.TIME_FORMAT, this._fromTimeSelect.getValue()), 367 toTime = AjxDateFormat.format(ZmCalPrintDialog.TIME_FORMAT, this._toTimeSelect.getValue()), 368 viewSelected = ZmCalViewController.OP_TO_VIEW[this._viewSelect.getValue()], 369 viewStyle = this._getPrintViewName(viewSelected), 370 workDaysOnly = document.getElementById(this._htmlElId + "_workDaysOnly").checked, 371 oneWeekPerPage = document.getElementById(this._htmlElId + "_oneWeekPerPage").checked, 372 oneDayPerPage = document.getElementById(this._htmlElId + "_oneDayPerPage").checked, 373 includeMiniCal = document.getElementById(this._htmlElId + "_includeMiniCal").checked, 374 375 //Create the string and pass it to the URL 376 treeView = this._opc.getOverview(this._curOverviewId).getTreeView(ZmOrganizer.CALENDAR); 377 cals = treeView.getSelected(); 378 379 for(j=0; j<cals.length; j++) { 380 calIds.push(cals[j].id); 381 } 382 383 if(viewSelected == ZmId.VIEW_CAL_MONTH) { 384 var endMonthDate = AjxDateUtil._daysPerMonth[dateRangeTo.getMonth()]; 385 if (dateRangeTo.getMonth() == '1' && !AjxDateUtil.isLeapYear(dateRangeTo.getYear())) { 386 //By default,_daysPerMonth sets number of days for Feb as 29. 387 //If it's not a leap year, set it to 28. 388 endMonthDate = '28'; 389 } 390 dateRangeTo.setDate(endMonthDate); 391 dateRangeFrom.setDate(1); 392 } 393 else if(viewSelected == ZmId.VIEW_CAL_WEEK || 394 viewSelected == ZmId.VIEW_CAL_WORK_WEEK) { 395 var fdow = appCtxt.get(ZmSetting.CAL_FIRST_DAY_OF_WEEK) || 0; 396 dateRangeFrom = AjxDateUtil.getFirstDayOfWeek(dateRangeFrom, fdow); 397 dateRangeTo = AjxDateUtil.getLastDayOfWeek(dateRangeTo, fdow); 398 } 399 400 dateRangeTo.setHours(23, 59, 59, 999); 401 dateRangeFrom = AjxDateFormat.format(ZmCalPrintDialog.DATE_FORMAT, dateRangeFrom); 402 dateRangeTo = AjxDateFormat.format(ZmCalPrintDialog.DATE_FORMAT, dateRangeTo); 403 404 params[i++] = "/h/printcalendar?"; 405 params[i++] = "l="; 406 params[i++] = calIds.join(','); 407 params[i++] = "&origView="; 408 params[i++] = this._getPrintViewName(this.currentViewId); 409 params[i++] = "&view="; 410 params[i++] = viewStyle; 411 params[i++] = "&date="; 412 params[i++] = dateRangeFrom; 413 params[i++] = "&endDate="; 414 params[i++] = dateRangeTo; 415 params[i++] = "&ft="; 416 params[i++] = fromTime; 417 params[i++] = "&tt="; 418 params[i++] = toTime; 419 params[i++] = "&wd="; 420 params[i++] = workDaysOnly; 421 params[i++] = "&ow="; 422 params[i++] = oneWeekPerPage; 423 params[i++] = "&od="; 424 params[i++] = oneDayPerPage; 425 params[i++] = "&imc="; 426 params[i++] = includeMiniCal; 427 params[i++] = "&wdays="; 428 params[i++] = workDaysOnly ? ZmCalPrintDialog.encodeWorkingDays() : ""; 429 params[i++] = "&tz="; 430 params[i++] = AjxTimezone.getServerId(AjxTimezone.DEFAULT); 431 params[i++] = "&skin="; 432 params[i++] = appCurrentSkin; 433 434 printURL = appContextPath + params.join(""); 435 return printURL; 436 }; 437 438 ZmCalPrintDialog.encodeWorkingDays = function () { 439 var wHrs = ZmCalBaseView.parseWorkingHours(ZmCalBaseView.getWorkingHours()), 440 wDays = []; 441 for (var i=0; i<wHrs.length; i++) { 442 if(wHrs[i].isWorkingDay) { 443 wDays.push(i); 444 } 445 } 446 return wDays.join(","); 447 }; 448 449 450 ZmDateInput = function(parent, id, parentElement) { 451 if (arguments.length == 0) return; 452 DwtComposite.call(this, {parent:parent, className:"ZmDateInput", parentElement: parentElement}); 453 this.id = id || Dwt.getNextId(); 454 this._setHtml(id); 455 }; 456 457 ZmDateInput.prototype = new DwtComposite; 458 ZmDateInput.prototype.constructor = ZmDateInput; 459 460 ZmDateInput.prototype.getValue = 461 function() { 462 var date = ""; 463 if(this.getEnabled()) { 464 date = AjxDateUtil.simpleParseDateStr(this._dateInputField.getValue()); 465 //date.setHours(23, 59, 59, 999); 466 } 467 return date; 468 }; 469 470 ZmDateInput.prototype.getTimeValue = 471 function() { 472 var dateObj = this.getEnabled() && AjxDateUtil.simpleParseDateStr(this._dateInputField.getValue()); 473 return dateObj ? dateObj.getTime() : null; 474 }; 475 476 ZmDateInput.prototype.setValue = 477 function(date) { 478 this._dateInputField.setValue(AjxDateUtil.simpleComputeDateStr(date)); 479 }; 480 481 ZmDateInput.prototype.setEnabled = 482 function(enabled) { 483 this._dateInputField.setEnabled(enabled); 484 this._dateButton.setEnabled(enabled); 485 }; 486 487 ZmDateInput.prototype.getEnabled = 488 function(enabled) { 489 return this._dateInputField.getEnabled() && this._dateButton.getEnabled(); 490 }; 491 492 493 // Private / protected methods 494 495 ZmDateInput.prototype._setHtml = 496 function(id) { 497 var dateButtonListener = new AjxListener(this, this._dateButtonListener), 498 dateCalSelectionListener = new AjxListener(this, this._dateCalSelectionListener); 499 500 this.getHtmlElement().innerHTML = AjxTemplate.expand("calendar.Appointment#ApptTimeInput", {id: this._htmlElId}); 501 this._dateButton = ZmCalendarApp.createMiniCalButton(this.parent, this._htmlElId + "_timeSelectBtn", dateButtonListener, dateCalSelectionListener); 502 this._dateButton.setSize("20"); 503 504 //create time select input field 505 var params = { 506 parent: this, 507 parentElement: (this._htmlElId + "_timeSelectInput"), 508 type: DwtInputField.STRING, 509 errorIconStyle: DwtInputField.ERROR_ICON_NONE, 510 validationStyle: DwtInputField.CONTINUAL_VALIDATION, 511 inputId: id 512 }; 513 514 this._dateInputField = new DwtInputField(params); 515 var timeInputEl = this._dateInputField.getInputElement(); 516 Dwt.setSize(timeInputEl, "80px", "2rem"); 517 timeInputEl.typeId = this.id; 518 }; 519 520 ZmDateInput.prototype._dateButtonListener = 521 function(ev) { 522 var cal, 523 menu, 524 calDate = AjxDateUtil.simpleParseDateStr(this._dateInputField.getValue()); 525 526 // if date was input by user and its foobar, reset to today's date 527 if (isNaN(calDate)) { 528 calDate = new Date(); 529 this._dateInputField.setValue(AjxDateUtil.simpleComputeDateStr(calDate)); 530 } 531 532 // always reset the date to current field's date 533 menu = ev.item.getMenu(); 534 cal = menu.getItem(0); 535 cal.setDate(calDate, true); 536 ev.item.popup(); 537 }; 538 539 ZmDateInput.prototype._dateCalSelectionListener = 540 function(ev) { 541 var newDate = AjxDateUtil.simpleComputeDateStr(ev.detail); 542 this._dateInputField.setValue(newDate); 543 544 }; 545