You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1458 lines
46 KiB
1458 lines
46 KiB
<?php
|
|
/*********************************************************************
|
|
class.schedule.php
|
|
|
|
Peter Rotich <peter@osticket.com>
|
|
Copyright (c) 2019 osTicket
|
|
http://www.osticket.com
|
|
|
|
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
|
See LICENSE.TXT for details.
|
|
|
|
vim: expandtab sw=4 ts=4 sts=4:
|
|
**********************************************************************/
|
|
|
|
/**
|
|
* Schedule
|
|
*
|
|
*/
|
|
class Schedule extends VerySimpleModel {
|
|
|
|
static $meta = array(
|
|
'table' => SCHEDULE_TABLE,
|
|
'ordering' => array('name'),
|
|
'pk' => array('id'),
|
|
'joins' => array(
|
|
'entries' => array(
|
|
'reverse' => 'ScheduleEntry.schedule',
|
|
),
|
|
),
|
|
);
|
|
|
|
// Supported Schedule types based on flags.
|
|
protected static $types = array(
|
|
'bizhrs' =>
|
|
/* @trans */ 'Business Hours',
|
|
'hdays' =>
|
|
/* @trans */ 'Holiday Hours',
|
|
);
|
|
|
|
// Supported Flags
|
|
// FLAG_BIZHRS: Schedule type of Business Hours otherwise Holiday Hours
|
|
// is assumed.
|
|
const FLAG_BIZHRS = 0x0001;
|
|
|
|
protected $_entries;
|
|
protected $_holidays;
|
|
protected $_form;
|
|
protected $_config;
|
|
|
|
public function setFlag($flag, $val) {
|
|
|
|
if ($val)
|
|
$this->flags |= $flag;
|
|
else
|
|
$this->flags &= ~$flag;
|
|
}
|
|
|
|
function get($field, $default=false) {
|
|
|
|
try {
|
|
return parent::get($field, $default);
|
|
} catch (Exception $e) {}
|
|
|
|
if (!isset($this->_config))
|
|
$this->getConfig();
|
|
|
|
if (isset($this->_config[$field]))
|
|
return $this->_config[$field];
|
|
}
|
|
|
|
function getConfig() {
|
|
|
|
if (!isset($this->_config) && $this->getId()) {
|
|
$_config = new Config('schedule.'.$this->getId());
|
|
$this->_config = $_config->getInfo();
|
|
}
|
|
|
|
return $this->_config;
|
|
}
|
|
|
|
|
|
function getId() {
|
|
return $this->get('id');
|
|
}
|
|
|
|
function getName() {
|
|
return $this->getLocal('name');
|
|
}
|
|
|
|
function getType() {
|
|
return ($this->flags & self::FLAG_BIZHRS) ? 'bizhrs' : 'hdays';
|
|
}
|
|
|
|
function getTypeDesc() {
|
|
$types = self::getTypes();
|
|
return $types[$this->getType()];
|
|
}
|
|
|
|
function getTimezone() {
|
|
global $cfg;
|
|
|
|
return $this->get('timezone') ?: $cfg->getTimezone();
|
|
}
|
|
|
|
function getDatetimeZone() {
|
|
return new DatetimeZone($this->getTimezone());
|
|
}
|
|
|
|
function getCreated() {
|
|
return $this->get('created');
|
|
}
|
|
|
|
function getUpdated() {
|
|
return $this->get('updated');
|
|
}
|
|
|
|
function getInfo() {
|
|
$info = $this->ht;
|
|
$info['type'] = $this->getType();
|
|
return $info;
|
|
}
|
|
|
|
function getNumEntries() {
|
|
return $this->getEntries()->count();
|
|
}
|
|
|
|
function getEntries() {
|
|
|
|
if (!$this->_entries) {
|
|
$this->_entries = ScheduleEntry::objects()
|
|
->filter(array('schedule_id' => $this->getId()));
|
|
}
|
|
return $this->_entries;
|
|
}
|
|
|
|
function getEntry($id) {
|
|
return $this->entries->findFirst(array('id' => $id));
|
|
}
|
|
|
|
function addEntry(ScheduleEntryForm $form, &$errors) {
|
|
|
|
if (!($vars=$form->process()))
|
|
return false;
|
|
|
|
if (!$this->isEntryUnique($vars, $errors))
|
|
$errors['error'] = $errors['error'] ?: __('Entry must be unique');
|
|
|
|
if ($errors)
|
|
return false;
|
|
|
|
$vars['schedule_id'] = $this->getId();
|
|
|
|
if (!($entry = ScheduleEntry::create($vars)))
|
|
return false;
|
|
|
|
$this->_entries = false;
|
|
return $entry;
|
|
}
|
|
|
|
function isEntryNameUnique($name, $id=0) {
|
|
$entry = $this->entries->findFirst(array('name' => $name));
|
|
return !($entry && $entry->getId() != $id);
|
|
}
|
|
|
|
function isEntryUnique($vars, &$errors) {
|
|
// Make sure name is inique
|
|
if (!$vars['name']
|
|
|| !$this->isEntryNameUnique($vars['name'], $vars['id'] ?: 0))
|
|
$errors['name'] = __('Name must be unique');
|
|
|
|
switch ($vars['repeats']) {
|
|
case 'weekly':
|
|
if ($vars['day'] < 6 ) { // Weekday
|
|
if ($this->entries->findFirst(['repeats' => 'weekdays']))
|
|
$errors['error'] = __('Weekdays entry already exists');
|
|
} elseif ($vars['day'] > 5) { //Weekend
|
|
if ($this->entries->findFirst(['repeats' => 'weekends']))
|
|
$errors['error'] = __('Weekends entry already exists');
|
|
}
|
|
break;
|
|
case 'weekdays':
|
|
if ($this->entries->findFirst(['repeats' => 'weekly',
|
|
'day__lt' => 6]))
|
|
$errors['error'] = __('Week day entry already exists');
|
|
break;
|
|
case 'weekends':
|
|
if ($this->entries->findFirst(['repeats' => 'weekly',
|
|
'day__gt' => 5]))
|
|
$errors['error'] = __('Weekend day entry already exists');
|
|
break;
|
|
case 'daily':
|
|
if (!$vars['id'] && $this->entries->count())
|
|
$errors['error'] = __('Other entries already exists');
|
|
break;
|
|
}
|
|
|
|
// Daily entry cannot coexist with other entries - mf is selfish af.
|
|
if (!$errors['error'] && strcasecmp($vars['repeats'], 'daily'))
|
|
if ($this->entries->findFirst(['repeats' => 'daily']))
|
|
$errors['error'] = __('Daily entry already exists');
|
|
|
|
|
|
if (!$errors['error']) {
|
|
$keys = array_intersect_key($vars, array_flip(
|
|
['repeats', 'day', 'week', 'month']));
|
|
$keys['schedule_id'] = $this->getId();
|
|
// Once entries can repeat on different dates
|
|
if ($keys['repeats'] == 'never')
|
|
$keys['starts_on'] = $vars['starts_on'];
|
|
|
|
$entries= ScheduleEntry::objects()
|
|
->filter($keys);
|
|
if ($vars['id'])
|
|
$entries->exclude(['id' => $vars['id']]);
|
|
|
|
if ($entries->count())
|
|
$errors['error'] = __('Entry matching the selection already exists');
|
|
}
|
|
|
|
return !count($errors);
|
|
}
|
|
|
|
function isHolidays() {
|
|
return !$this->isBusinessHours();
|
|
}
|
|
|
|
function isBusinessHours() {
|
|
return ($this->flags & self::FLAG_BIZHRS);
|
|
}
|
|
|
|
function getForm($source=null) {
|
|
|
|
if (!isset($this->_form))
|
|
$this->_form = self::basicForm($source ?: $this->getInfo());
|
|
|
|
return $this->_form;
|
|
}
|
|
|
|
function getTranslateTag($subtag) {
|
|
return _H(sprintf('schedule.%s.%s', $subtag, $this->id));
|
|
}
|
|
function getLocal($subtag) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $this->get($subtag);
|
|
}
|
|
|
|
function getHolidays() {
|
|
if (!$this->isBusinessHours())
|
|
return false;
|
|
|
|
if (!isset($this->_holidays)) {
|
|
$config = $this->getConfiguration();
|
|
$this->_holidays = $config['holidays'] ?: array();
|
|
}
|
|
|
|
return $this->_holidays;
|
|
}
|
|
|
|
function getNumHolidaysSchedules() {
|
|
return count($this->getHolidays() ?: array());
|
|
}
|
|
|
|
function getConfiguration() {
|
|
$config = $this->getConfig();
|
|
return JsonDataParser::decode($config['configuration']);
|
|
}
|
|
|
|
function saveConfiguration($vars) {
|
|
$config = new Config('schedule.'.$this->getId());
|
|
return $config->updateAll(array(
|
|
'configuration' => JsonDataEncoder::encode([
|
|
'holidays' => $vars['holidays'] ?: array()
|
|
]),
|
|
)
|
|
);
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
|
|
$form = $this->getForm($vars);
|
|
if (!$form->isValid())
|
|
return false;
|
|
|
|
$data = $form->getClean();
|
|
foreach (['name', 'timezone', 'description'] as $f)
|
|
$this->set($f, $data[$f]);
|
|
// Set Schedule type flag only on create.
|
|
if (!$this->getId() && isset($data['type']))
|
|
$this->setFlag(self::FLAG_BIZHRS, ($data['type'] == 'bizhrs'));
|
|
|
|
if (!$this->save(true))
|
|
return false;
|
|
|
|
// Update the config information
|
|
$this->saveConfiguration(array(
|
|
'holidays' => $vars['holidays']));
|
|
// Update sorting
|
|
foreach ($this->getEntries() as $e) {
|
|
$id = $e->getId();
|
|
if (isset($_POST["sort-{$id}"])) {
|
|
$e->sort = $_POST["sort-$id"];
|
|
$e->save(true);
|
|
}
|
|
}
|
|
|
|
// Reset the form so we don't cache old data
|
|
unset($this->_form);
|
|
|
|
return true;
|
|
}
|
|
|
|
function save($refetch=false) {
|
|
if (count($this->dirty))
|
|
$this->set('updated', new SqlFunction('NOW'));
|
|
if (isset($this->dirty['description']))
|
|
$this->description = Format::sanitize($this->description);
|
|
|
|
return parent::save($refetch);
|
|
}
|
|
|
|
function cloneEntries($schedule) {
|
|
if (!$schedule) return;
|
|
foreach ($schedule->getEntries() as $entry) {
|
|
$vars = $entry->ht;
|
|
unset($vars['id']);
|
|
$vars['schedule_id'] = $this->getId();
|
|
ScheduleEntry::create($vars);
|
|
}
|
|
}
|
|
|
|
function delete() {
|
|
|
|
// TODO: Deny delete for in-use schedules
|
|
|
|
if (!parent::delete())
|
|
return false;
|
|
|
|
$this->entries->delete();
|
|
|
|
return true;
|
|
}
|
|
|
|
function getEntryForm($source=null) {
|
|
return new ScheduleEntryForm($source, array(
|
|
'timezone' => $this->getTimezone(),
|
|
'holidays' => !$this->isBusinessHours(),
|
|
));
|
|
}
|
|
|
|
function getDiagnosticForm($source=null) {
|
|
|
|
if (!$source)
|
|
$source = ['date' => time(), 'hours' => 24];
|
|
|
|
return new ScheduleDiagnosticForm($source, array(
|
|
'timezone' => $this->getTimezone(),
|
|
));
|
|
}
|
|
|
|
static function create($ht=false, &$errors=array()) {
|
|
$inst = new static($ht);
|
|
$inst->set('created', new SqlFunction('NOW'));
|
|
if ($inst->save())
|
|
return $inst;
|
|
}
|
|
|
|
static function __create($vars) {
|
|
$s = static::create($vars);
|
|
$s->set('created', new SqlFunction('NOW'));
|
|
$s->set('updated', new SqlFunction('NOW'));
|
|
$s->save(true);
|
|
foreach ($vars['entries'] ?: array() as $info) {
|
|
$entry = new ScheduleEntry($info);
|
|
$entry->schedule_id = $s->getId();
|
|
$entry->save();
|
|
}
|
|
|
|
if (isset($vars['configuration']))
|
|
$s->saveConfiguration($vars['configuration']);
|
|
|
|
return $s;
|
|
}
|
|
|
|
static function getIdByName($name, $pid=0) {
|
|
$schedule = self::objects()->filter(array(
|
|
'name' => $name,
|
|
))->values_flat('id')->first();
|
|
return $schedule ? $schedule[0] : 0;
|
|
}
|
|
|
|
static function lookup($id) {
|
|
|
|
if (!($schedule = parent::lookup($id)))
|
|
return null;
|
|
|
|
return $schedule;
|
|
}
|
|
|
|
static function getSchedules($criteria=array()) {
|
|
$schedules = self::objects();
|
|
if ($criteria)
|
|
$schedules->filter($criteria);
|
|
|
|
$schedules->order_by('name');
|
|
|
|
return $schedules;
|
|
}
|
|
|
|
static function getTypes() {
|
|
static $translated = false;
|
|
if (!$translated) {
|
|
foreach (self::$types as $k => $v)
|
|
self::$types[$k] = __($v);
|
|
}
|
|
|
|
return self::$types;
|
|
}
|
|
|
|
|
|
static function basicForm($source=null) {
|
|
|
|
return new SimpleForm(array(
|
|
'name' => new TextboxField(array(
|
|
'required' => true,
|
|
'label' => __('Name'),
|
|
'configuration' => array(
|
|
'size' => 60,
|
|
'length' => 0,
|
|
'autofocus' => true,
|
|
),
|
|
)),
|
|
'type' => new ChoiceField(array(
|
|
'required' => true,
|
|
'editable' => false,
|
|
'label' => __('Type'),
|
|
'validator-error' => __('Selection required'),
|
|
'choices' => self::getTypes(),
|
|
'configuration' => array(
|
|
'mode' => 'view',
|
|
),
|
|
)),
|
|
'timezone' => new TimezoneField(array(
|
|
'required'=>false,
|
|
'label'=>__('Timezone'),
|
|
'hint' => __('Leave selection empty for Floating Timezone'),
|
|
'configuration' => array(
|
|
'autodetect'=>false,
|
|
'prompt' => __("Schedule's timezone")
|
|
),
|
|
)),
|
|
'description' => new TextareaField(array(
|
|
'label'=> 'Description',
|
|
'required'=>false,
|
|
'default'=>'',
|
|
'configuration' => array(
|
|
'html' => true,
|
|
'size' => 'small',
|
|
'placeholder' => __('Brief description of the schedule'),
|
|
),
|
|
)),
|
|
), $source);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* BusinessHoursSchedule - extension of schedule
|
|
*
|
|
*/
|
|
class BusinessHoursSchedule extends Schedule {
|
|
|
|
// Holidays schedules applicable to this schedule
|
|
protected $holidays;
|
|
|
|
public function addWorkingHours(Datetime $date, $hours,
|
|
&$timeline=array()) {
|
|
// Delegate adding working hours to Business Hours utility class
|
|
$bhrs = new BusinessHours($this);
|
|
if (!$bhrs->addWorkingHours($date, $hours))
|
|
return false;
|
|
|
|
$timeline = $bhrs->getTimeline();
|
|
|
|
return $date;
|
|
}
|
|
|
|
public function getHolidaysSchedules() {
|
|
|
|
if (!isset($this->holidays)) {
|
|
$this->holidays = array();
|
|
foreach ($this->getHolidays() ?:array() as $id) {
|
|
if (($s=HolidaysSchedule::lookup($id)))
|
|
$this->holidays[] = $s;
|
|
}
|
|
}
|
|
return $this->holidays;
|
|
}
|
|
|
|
static function getSchedules($criteria=array()) {
|
|
return parent::getSchedules($criteria + array(
|
|
'flags__hasbit' => Schedule::FLAG_BIZHRS));
|
|
}
|
|
|
|
static function lookup($id) {
|
|
return parent::lookup(array(
|
|
'id' => $id,
|
|
'flags__hasbit' => Schedule::FLAG_BIZHRS));
|
|
}
|
|
}
|
|
/*
|
|
* HolidaysSchedule
|
|
*
|
|
*/
|
|
class HolidaysSchedule extends Schedule {
|
|
|
|
static function getSchedules($criteria=array()) {
|
|
return parent::getSchedules($criteria)
|
|
->exclude(array('flags__hasbit' => Schedule::FLAG_BIZHRS));
|
|
}
|
|
|
|
static function lookup($id) {
|
|
return parent::lookup($id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ScheduleEntry: An entry in a schedule.
|
|
*
|
|
*/
|
|
class ScheduleEntry extends VerySimpleModel {
|
|
|
|
static $meta = array(
|
|
'table' => SCHEDULE_ENTRY_TABLE,
|
|
'pk' => array('id'),
|
|
'joins' => array(
|
|
'schedule' => array(
|
|
'null' => true,
|
|
'constraint' => array('schedule_id' => 'Schedule.id'),
|
|
),
|
|
),
|
|
);
|
|
|
|
var $_form;
|
|
|
|
protected static $frequencies = array(
|
|
'never' =>
|
|
/* @trans */ 'Once',
|
|
'daily' =>
|
|
/* @trans */ 'Daily',
|
|
'weekly' =>
|
|
/* @trans */ 'Weekly',
|
|
'monthly' =>
|
|
/* @trans */ 'Monthly',
|
|
'yearly' =>
|
|
/* @trans */ 'Yearly',
|
|
);
|
|
|
|
protected static $days = array(
|
|
|
|
1 =>
|
|
/* @trans */ 'Monday',
|
|
2 =>
|
|
/* @trans */ 'Tuesday',
|
|
3 =>
|
|
/* @trans */ 'Wednesday',
|
|
4 =>
|
|
/* @trans */ 'Thursday',
|
|
5 =>
|
|
/* @trans */ 'Friday',
|
|
6 =>
|
|
/* @trans */ 'Saturday',
|
|
7 =>
|
|
/* @trans */ 'Sunday',
|
|
);
|
|
|
|
protected static $weeks = array(
|
|
1 =>
|
|
/* @trans */ 'First',
|
|
2 =>
|
|
/* @trans */ 'Second',
|
|
3 =>
|
|
/* @trans */ 'Third',
|
|
4 =>
|
|
/* @trans */ 'Fourth',
|
|
5 =>
|
|
/* @trans */ 'Fifth',
|
|
-1 =>
|
|
/* @trans */ 'Last',
|
|
);
|
|
|
|
protected static $months = array( 1 =>
|
|
/* @trans */ 'January',
|
|
/* @trans */ 'February',
|
|
/* @trans */ 'March',
|
|
/* @trans */ 'April',
|
|
/* @trans */ 'May',
|
|
/* @trans */ 'June',
|
|
/* @trans */ 'July',
|
|
/* @trans */ 'August',
|
|
/* @trans */ 'September',
|
|
/* @trans */ 'October',
|
|
/* @trans */ 'November',
|
|
/* @trans */ 'December'
|
|
);
|
|
|
|
protected $_timezone;
|
|
protected $_starts;
|
|
protected $_ends;
|
|
protected $_stops;
|
|
|
|
function getId() {
|
|
return $this->get('id');
|
|
}
|
|
|
|
function getSchedule() {
|
|
return $this->schedule;
|
|
}
|
|
|
|
function getScheduleId() {
|
|
return $this->get('schedule_id');
|
|
}
|
|
|
|
function getCreated() {
|
|
return $this->get('created');
|
|
}
|
|
|
|
function getUpdated() {
|
|
return $this->get('updated');
|
|
}
|
|
|
|
function getTimezone() {
|
|
if (!isset($this->_timezone))
|
|
$this->_timezone = $this->getSchedule()->getTimezone();
|
|
|
|
return $this->_timezone;
|
|
}
|
|
|
|
function getDatetimeZone() {
|
|
return new DatetimeZone($this->getTimezone());
|
|
}
|
|
|
|
function diffTime(Datetime $date) {
|
|
// Set the time based on datetime given
|
|
$start = clone $this->getStartsDatetime();
|
|
list($h, $m, $s) = explode(':', $date->format('H:i:s'));
|
|
$start->setTime($h, $m, $s);
|
|
return $this->getEndsDatetime()->getTimestamp() - $start->getTimestamp();
|
|
}
|
|
|
|
function diff() {
|
|
return ($this->getEndsDatetime()->getTimestamp() -
|
|
$this->getStartsDatetime()->getTimestamp());
|
|
}
|
|
|
|
function getHours() {
|
|
return ($this->diff()+1)/3600;
|
|
}
|
|
|
|
function getMinutes() {
|
|
return $this->diff()/60;
|
|
}
|
|
|
|
function isFullDay() {
|
|
return ($this->getHours() >= 24);
|
|
}
|
|
|
|
function isWithinHours(Datetime $dt) {
|
|
return !($this->isAfterHours($dt) || $this->isBeforeHours($dt));
|
|
}
|
|
|
|
function isBeforeHours(Datetime $dt) {
|
|
return strtotime($dt->format('H:i:s')) <
|
|
strtotime($this->getStartsTime());
|
|
}
|
|
|
|
function isAfterHours(Datetime $dt) {
|
|
return strtotime($dt->format('H:i:s')) >
|
|
strtotime($this->getEndsTime());
|
|
}
|
|
|
|
function isOneTime() {
|
|
return !strcasecmp($this->getRepeats(), 'never');
|
|
}
|
|
|
|
function getStartsDatetime() {
|
|
if (!isset($this->_starts))
|
|
$this->_starts = new Datetime(sprintf('%s %s',
|
|
$this->ht['starts_on'], $this->ht['starts_at']),
|
|
$this->getDatetimeZone());
|
|
|
|
return $this->_starts;
|
|
}
|
|
|
|
function getStartsTime() {
|
|
return $this->getStartsDatetime()->format('H:i:s');
|
|
}
|
|
|
|
function getEndsDatetime() {
|
|
if (!isset($this->_ends))
|
|
$this->_ends = new Datetime(sprintf('%s %s',
|
|
$this->ht['ends_on'], $this->ht['ends_at']),
|
|
$this->getDatetimeZone());
|
|
|
|
return $this->_ends;
|
|
}
|
|
|
|
function getEndsTime() {
|
|
return $this->getEndsDatetime()->format('H:i:s');
|
|
}
|
|
|
|
function getStopsDatetime() {
|
|
if (!isset($this->_stops)) {
|
|
if ($this->ht['stops_on'])
|
|
$this->_stops = new Datetime($this->ht['stops_on'],
|
|
$this->getDatetimeZone());
|
|
elseif ($this->isOneTime())
|
|
$this->_stops = $this->getEndsDatetime();
|
|
}
|
|
|
|
return $this->_stops;
|
|
}
|
|
|
|
function getIntervalSpec(Datetime $dt) {
|
|
$info = $this->getInfo();
|
|
switch ($info['repeats']) {
|
|
case 'never':
|
|
$starts = $this->getStartsDatetime();
|
|
return $starts->format('Y-m-d');
|
|
break;
|
|
case 'daily':
|
|
return sprintf('%s %s',
|
|
'today', $dt->format('Y-m-d'));
|
|
break;
|
|
case 'weekdays':
|
|
if ($dt->format('N') > 5)
|
|
return 'weekday';
|
|
else
|
|
return sprintf('%s %s',
|
|
'today', $dt->format('Y-m-d'));
|
|
break;
|
|
case 'weekends':
|
|
if ($dt->format('N') > 5)
|
|
return sprintf('%s %s',
|
|
'today', $dt->format('Y-m-d'));
|
|
else
|
|
return sprintf('Next Saturday %s',
|
|
$dt->format('Y-m-d'));
|
|
break;
|
|
case 'weekly':
|
|
return sprintf('%s %s',
|
|
self::$days[$info['day']],
|
|
$dt->format('Y-m-d'));
|
|
break;
|
|
case 'monthly':
|
|
if (!$info['week'] && $info['day']> 0)
|
|
return sprintf('%s-%s',
|
|
$dt->format('Y-m'),
|
|
$info['day']);
|
|
else
|
|
return sprintf('%s %s of %s %d',
|
|
self::$weeks[$info['week']],
|
|
self::$days[$info['day']],
|
|
$dt->format('F'),
|
|
$dt->format('Y'));
|
|
break;
|
|
case 'yearly':
|
|
if ($info['week'] > 0) {
|
|
return sprintf('%s %s %s %s',
|
|
self::$months[$info['month']],
|
|
$dt->format('Y'),
|
|
self::$weeks[$info['week']],
|
|
self::$days[$info['day']]);
|
|
} elseif ($info['week'] == -1) {
|
|
return sprintf('last %s of %s %s',
|
|
self::$days[$info['day']],
|
|
self::$months[$info['month']],
|
|
$dt->format('Y'));
|
|
|
|
} else {
|
|
return sprintf('%s %d %d',
|
|
self::$months[$info['month']],
|
|
$info['day'],
|
|
$dt->format('Y'));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function getCurrent($from=null) {
|
|
if (!isset($this->_current) || $from) {
|
|
// Figure out starting point (from)
|
|
$from = is_object($from) ? clone $from : Format::parseDateTime($from ?: 'now');
|
|
$start = $this->getStartsDatetime();
|
|
if ($start->getTimestamp() > $from->getTimestamp())
|
|
$from = clone $start;
|
|
// Check to make sure we're still in scope
|
|
$stop = $this->getStopsDatetime();
|
|
if ($stop && $stop->getTimestamp() < $from->getTimestamp())
|
|
return null;
|
|
|
|
// Figure out start time for the entry.
|
|
$from->modify($this->getIntervalSpec($from));
|
|
$this->_current = clone $from;
|
|
}
|
|
return $this->_current;
|
|
}
|
|
|
|
function next() {
|
|
if (!($current=$this->getCurrent()))
|
|
return null;
|
|
|
|
// Advance the interval based on frequencry
|
|
switch ($this->ht['repeats']) {
|
|
case 'daily':
|
|
$current->modify('+1 day');
|
|
break;
|
|
case 'weekly':
|
|
$current->modify('+1 week');
|
|
break;
|
|
case 'weekdays':
|
|
$current->modify('+1 weekday');
|
|
break;
|
|
case 'weekends':
|
|
// Heavy lifting done in getIntervalSpec
|
|
$current->modify('+1 day');
|
|
break;
|
|
case 'monthly':
|
|
$current->modify('+1 month');
|
|
break;
|
|
case 'yearly':
|
|
$current->modify('+1 year');
|
|
break;
|
|
case 'never':
|
|
return null;
|
|
}
|
|
// Set interval spec for specific day/week/month
|
|
$current->modify($this->getIntervalSpec($current));
|
|
// Make sure we're still in scope
|
|
$stops = $this->getStopsDatetime();
|
|
if ($stops && $stops->getTimestamp() < $current->getTimestamp())
|
|
return null;
|
|
|
|
// Advance current position
|
|
$this->_current = $current;
|
|
|
|
return $current;
|
|
}
|
|
|
|
function getOccurrences($start=null, $end=null, $num=5) {
|
|
$occurrences = array();
|
|
if (($current = $this->getCurrent($start))) {
|
|
$start = $start ?: $current;
|
|
while (count($occurrences) < $num) {
|
|
$date = $current->format('Y-m-d');
|
|
if ($end && strtotime($date) > strtotime($end))
|
|
break;
|
|
if (strtotime($date) >= strtotime($start->format('Y-m-d')))
|
|
$occurrences[$date] = $this;
|
|
|
|
if (!($current=$this->next()))
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $occurrences;
|
|
}
|
|
|
|
function getName() {
|
|
return $this->getLocal('name');
|
|
}
|
|
|
|
function getRepeats() {
|
|
return $this->get('repeats');
|
|
}
|
|
|
|
function getDesc() {
|
|
$info = $this->getInfo();
|
|
$repeats = $this->getRepeats();
|
|
$days = self::getDays();
|
|
$weeks = self::getWeeks();
|
|
$months = self::getMonths();
|
|
$frequencies = self::getFrequencies();
|
|
$starts = $this->getStartsDatetime();
|
|
$ends = $this->getEndsDatetime();
|
|
|
|
$when = '';
|
|
$desc = $frequencies[$info['repeats']];
|
|
switch ($info['repeats']) {
|
|
case 'weekly':
|
|
$when = $days[$info['day']];
|
|
break;
|
|
case 'weekdays':
|
|
$desc = sprintf('%s (%s)',
|
|
__('Weekdays'), __('Mon-Fri'));
|
|
break;
|
|
case 'weekends':
|
|
$desc = sprintf('%s (%s)',
|
|
__('Weekends'), __('Sat-Sun'));
|
|
break;
|
|
case 'monthly':
|
|
if (!$info['week'])
|
|
$when = sprintf(__('%s of the Month'),
|
|
Format::ordinalsuffix($info['day']));
|
|
else
|
|
$when = sprintf('the %s %s',
|
|
$weeks[$info['week']], $days[$info['day']]);
|
|
break;
|
|
case 'yearly':
|
|
if (!$info['week']) {
|
|
$when =sprintf('%s %s',
|
|
$months[$info['month']],
|
|
Format::ordinalsuffix($info['day']));
|
|
} else {
|
|
$when = sprintf('the %s %s in %s',
|
|
$weeks[$info['week']],
|
|
$days[$info['day']],
|
|
$months[$info['month']]);
|
|
}
|
|
break;
|
|
case 'never':
|
|
$when = $starts->format('F jS, Y');
|
|
break;
|
|
}
|
|
|
|
if ($when)
|
|
$desc .=' '.sprintf(__('on %s'), $when);
|
|
|
|
if (!$this->isFullDay()) {
|
|
$desc .= sprintf(' <small>(%s - %s)</small>',
|
|
$starts->format('h:i a'),
|
|
$ends->format('h:i a'));
|
|
}
|
|
|
|
return $desc;
|
|
}
|
|
|
|
function getSortOrder() {
|
|
return $this->get('sort');
|
|
}
|
|
|
|
function getInfo() {
|
|
return $this->ht;
|
|
}
|
|
|
|
function getForm($source=null) {
|
|
|
|
if (!$this->_form) {
|
|
if (!$source) {
|
|
$source = $this->getInfo();
|
|
$starts = $this->getStartsDatetime();
|
|
$source['starts_on'] = $starts->getTimestamp();
|
|
$source['starts_at'] = $starts->format('h:i a');
|
|
$ends = $this->getEndsDatetime();
|
|
// TODO: Add support for 'ends_on' - We don't support date
|
|
// range at the moment - assuming ends on the same day.
|
|
$source['ends_at'] = $ends->format('h:i a');
|
|
if (($stops=$this->getStopsDatetime()))
|
|
$source['stops_on'] = $stops->setTimestamp();
|
|
|
|
// See if time spans all day.
|
|
if ($this->isFullDay())
|
|
$source['allday'] = true;
|
|
|
|
// Map UI fields
|
|
switch ($source['repeats']) {
|
|
case 'weekly':
|
|
$source['weekly_day'] = $source['day'];
|
|
break;
|
|
case 'weekdays':
|
|
case 'weekends':
|
|
// Keep the UI dumb af
|
|
$source['weekly_day'] = $source['repeats'];
|
|
$source['repeats'] = 'weekly';
|
|
break;
|
|
case 'monthly':
|
|
$source['monthly'] = $source['week'] ?: 'day';
|
|
$source['monthly_week'] = $source['week'];
|
|
$source['monthly_day'] = $source['day'];
|
|
break;
|
|
case 'yearly':
|
|
$source['yearly'] = $source['week'] ?: 'date';
|
|
$source['yearly_month'] = $source['month'];
|
|
$source['yearly_week'] = $source['week'];
|
|
$source['yearly_day'] = $source['day'];
|
|
break;
|
|
}
|
|
}
|
|
$this->_form = new ScheduleEntryForm($source);
|
|
}
|
|
|
|
return $this->_form;
|
|
}
|
|
|
|
function getFields() {
|
|
return $this->getForm()->getFields();
|
|
}
|
|
|
|
function getTranslateTag($subtag) {
|
|
return _H(sprintf('schedulentry.%s.%s', $subtag, $this->id));
|
|
}
|
|
function getLocal($subtag) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $this->get($subtag);
|
|
}
|
|
|
|
function toString() {
|
|
return $this->getName();
|
|
}
|
|
|
|
function __toString() {
|
|
return $this->toString();
|
|
}
|
|
|
|
function update(ScheduleEntryForm $form, &$errors=array()) {
|
|
|
|
if (!($vars=$form->process()))
|
|
return false;
|
|
|
|
$vars['id'] = $this->getId();
|
|
if (!$this->getSchedule()->isEntryUnique($vars, $errors))
|
|
return false;
|
|
|
|
foreach ($vars as $k => $v)
|
|
$this->set($k, $v);
|
|
|
|
return $this->save();
|
|
}
|
|
|
|
function delete() {
|
|
return parent::delete();
|
|
}
|
|
|
|
function save($refetch=false) {
|
|
if (count($this->dirty))
|
|
$this->set('updated', new SqlFunction('NOW'));
|
|
|
|
return parent::save($refetch);
|
|
}
|
|
|
|
static function getFrequencies() {
|
|
static $translated = false;
|
|
if (!$translated) {
|
|
foreach (static::$frequencies as $k=>$v)
|
|
static::$frequencies[$k] = __($v);
|
|
}
|
|
|
|
return static::$frequencies;
|
|
}
|
|
|
|
static function getDays() {
|
|
static $translated = false;
|
|
if (!$translated) {
|
|
foreach (static::$days as $k=>$v)
|
|
static::$days[$k] = __($v);
|
|
}
|
|
|
|
return static::$days;
|
|
}
|
|
|
|
static function getWeeks() {
|
|
static $translated = false;
|
|
if (!$translated) {
|
|
foreach (static::$weeks as $k=>$v)
|
|
static::$weeks[$k] = __($v);
|
|
}
|
|
|
|
return static::$weeks;
|
|
}
|
|
|
|
static function getMonths() {
|
|
static $translated = false;
|
|
if (!$translated) {
|
|
foreach (static::$months as $k=>$v)
|
|
static::$months[$k] = __($v);
|
|
}
|
|
|
|
return static::$months;
|
|
}
|
|
static function create($ht=false) {
|
|
$inst = new static($ht);
|
|
$inst->set('created', new SqlFunction('NOW'));
|
|
if ($inst->save())
|
|
return $inst;
|
|
|
|
}
|
|
}
|
|
|
|
class ScheduleEntryForm
|
|
extends AbstractForm {
|
|
static $layout = 'GridFormLayout';
|
|
function buildFields() {
|
|
$timezone = $this->options['timezone'];
|
|
$allday = ($this->options['holidays']);
|
|
$fields = array(
|
|
'name' => new TextboxField(array(
|
|
'required' => true,
|
|
'layout' => new GridFluidCell(8),
|
|
'label' => __('Name'),
|
|
'configuration' => array(
|
|
// 'translatable' => $item ? $item->getTranslateTag('value') : false,
|
|
'size' => 60,
|
|
'length' => 0,
|
|
'autofocus' => true,
|
|
),
|
|
)),
|
|
'starts_on' => new DatetimeField(array(
|
|
'label' => __('Starts on'),
|
|
'required' => true,
|
|
'layout' => new GridFluidCell(6),
|
|
'configuration' => array(
|
|
'time' => false,
|
|
'gmt' => false,
|
|
'future' => true,
|
|
'showtimezone' => false,
|
|
),
|
|
)),
|
|
'allday' => new BooleanField(array(
|
|
'required' => false,
|
|
'default' => $allday,
|
|
'label' => __('All Day'),
|
|
'layout' => new GridFluidCell(6),
|
|
'configuration'=>array(
|
|
'desc'=>__('Full 24-hour day'))
|
|
)),
|
|
'starts_at' => new TimeField(array(
|
|
'required' => true,
|
|
'default' => $allday ? '12:00 am' : '8:00 am',
|
|
'label' => __('Starts at'),
|
|
'layout' => new GridFluidCell(6),
|
|
'configuration' => array(
|
|
'timezone' => $timezone,
|
|
'showtimezone' => false,
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('allday__eq'=>false)),
|
|
VisibilityConstraint::HIDDEN),
|
|
)),
|
|
'ends_at' => new TimeField(array(
|
|
'required' => true,
|
|
'default' => $allday ? '11:59 pm' : '5:00 pm',
|
|
'label' => __('Ends at'),
|
|
'layout' => new GridFluidCell(6),
|
|
'configuration' => array(
|
|
'timezone' => $timezone,
|
|
'showtimezone' => false,
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('allday__eq'=>false)),
|
|
VisibilityConstraint::HIDDEN),
|
|
)),
|
|
'repeats' => new ChoiceField(array(
|
|
'required' => true,
|
|
'layout' => new GridFluidCell(6),
|
|
'label' => __('Repeats'),
|
|
'validator-error' => __('Selection required'),
|
|
'choices' => ScheduleEntry::getFrequencies(),
|
|
)),
|
|
'stops_on' => new DatetimeField(array(
|
|
'label' => __('Until'),
|
|
'required' => false,
|
|
'layout' => new GridFluidCell(4),
|
|
'configuration' => array(
|
|
'time' => false,
|
|
'gmt' => false,
|
|
'future' => true,
|
|
'placeholder' => __('Forever'),
|
|
'showtimezone' => false,
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('repeats__eq' => 'daily|weekly|monthly|yearly')),
|
|
VisibilityConstraint::VISIBLE ),
|
|
)),
|
|
'weekly_day' => new ChoiceField(array(
|
|
'required' => true,
|
|
'default' => "",
|
|
'layout' => new GridFluidCell(6),
|
|
'label' => __('Day of the Week'),
|
|
'choices' => ScheduleEntry::getDays() + array(
|
|
'weekdays' => sprintf('%s (%s)',
|
|
__('Weekdays'), __('Mon-Fri')),
|
|
'weekends' => sprintf('%s (%s)',
|
|
__('Weekends'), __('Sat-Sun'))),
|
|
'validator-error' => __('Selection required'),
|
|
'configuration'=>array('prompt'=>__('Select Day of the Week')),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('repeats__eq'=>'weekly')),
|
|
VisibilityConstraint::HIDDEN ),
|
|
)),
|
|
'monthly' => new ChoiceField(array(
|
|
'required' => true,
|
|
'default' => 'day',
|
|
'layout' => new GridFluidCell(6, array(
|
|
'break' => true)),
|
|
'label' => __('On the'),
|
|
'validator-error' => __('Selection required'),
|
|
'choices' => array(
|
|
'day' => __('Day of the Month Entered'),
|
|
'1' => __('First'),
|
|
'2' => __('Second'),
|
|
'3' => __('Third'),
|
|
'4' => __('Fourth'),
|
|
'5' => __('Fifth'),
|
|
'-1' => __('Last'),
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('repeats__eq'=>'monthly')),
|
|
VisibilityConstraint::HIDDEN ),
|
|
)),
|
|
'monthly_day' => new ChoiceField(array(
|
|
'required' => true,
|
|
'default' => "",
|
|
'layout' => new GridFluidCell(6),
|
|
'label' => __('Day'),
|
|
'choices' => ScheduleEntry::getDays(),
|
|
'validator-error' => __('Selection required'),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('monthly__neq'=>'day')),
|
|
VisibilityConstraint::HIDDEN ),
|
|
)),
|
|
'yearly' => new ChoiceField(array(
|
|
'required' => true,
|
|
'layout' => new GridFluidCell(4),
|
|
'label' => __('On the'),
|
|
'validator-error' => __('Selection required'),
|
|
'choices' => array(
|
|
'date' => __('Date Entered'),
|
|
'1' => __('First'),
|
|
'2' => __('Second'),
|
|
'3' => __('Third'),
|
|
'4' => __('Fourth'),
|
|
'5' => __('Fifth'),
|
|
'-1' => __('Last'),
|
|
),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('repeats__eq'=>'yearly')),
|
|
VisibilityConstraint::HIDDEN ),
|
|
)),
|
|
'yearly_day' => new ChoiceField(array(
|
|
'required' => true,
|
|
'default' => '',
|
|
'layout' => new GridFluidCell(4),
|
|
'label' => __('Day'),
|
|
'choices' => ScheduleEntry::getDays(),
|
|
'validator-error' => __('Selection required'),
|
|
'configuration'=>array('prompt'=>__('Day')),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('yearly__neq'=>'date')),
|
|
VisibilityConstraint::HIDDEN ),
|
|
)),
|
|
'yearly_month' => new ChoiceField(array(
|
|
'required' => true,
|
|
'default' => 0,
|
|
'layout' => new GridFluidCell(4),
|
|
'label' => __('In'),
|
|
'choices' => ScheduleEntry::getMonths(),
|
|
'validator-error' => __('Selection required'),
|
|
'configuration'=>array('prompt'=>__('Month')),
|
|
'visibility' => new VisibilityConstraint(
|
|
new Q(array('yearly__neq'=>'date')),
|
|
VisibilityConstraint::HIDDEN ),
|
|
)),
|
|
);
|
|
|
|
return $fields;
|
|
}
|
|
|
|
function process($validate=true) {
|
|
|
|
if (!$this->isValid())
|
|
return false;
|
|
|
|
// Parse && validate data
|
|
$errors = array();
|
|
$data = $this->getClean($validate);
|
|
$vars = array('name' => $data['name'], 'repeats' => $data['repeats']);
|
|
if (($startsOn=Format::parseDateTime($data['starts_on'])))
|
|
$vars['starts_on'] = $startsOn->format('Y-m-d');
|
|
else
|
|
$errors['starts_on'] = __('Valid Start Date required');
|
|
|
|
if ($data['stops_on'] && $startsOn &&
|
|
($stopsOn = Format::parseDateTime($data['stops_on']))) {
|
|
if ($stopsOn->getTimestamp() <= $startsOn->getTimestamp())
|
|
$errors['ends_on'] = __('Must be in the future');
|
|
}
|
|
|
|
if ($data['allday']) {
|
|
$data['starts_at'] = '00:00:00';
|
|
$data['ends_at'] = '23:59:59';
|
|
} else {
|
|
$data['starts_at'] = date('H:i:s', strtotime($data['starts_at']));
|
|
$data['ends_at'] = date('H:i:s', strtotime($data['ends_at']));
|
|
$startsAt = strtotime($vars['starts_on'].' '.$data['starts_at']);
|
|
$endsAt = strtotime($vars['starts_on'] .' '.$data['ends_at'])+59;
|
|
if ($startsAt >= $endsAt)
|
|
$errors['ends_at'] = __('Invalid time span');
|
|
}
|
|
|
|
if ($errors) {
|
|
// Replay any errors back on the form fields
|
|
foreach ($errors as $k => $error) {
|
|
if (($f=$this->getField($k)))
|
|
$f->addError($error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$dt = DateTimeImmutable::createFromMutable($startsOn);
|
|
// Start time
|
|
list($h,$m,$s) = explode(':', $data['starts_at']);
|
|
$starts = $dt->setTime($h, $m, 00);
|
|
$vars['starts_on'] = $starts->format('Y-m-d');
|
|
$vars['starts_at'] = $starts->format('H:i:s');
|
|
// end time
|
|
list($h,$m,$s) = explode(':', $data['ends_at']);
|
|
$ends = $dt->setTime($h, $m, $m == '00' ? 00 : 59);
|
|
$vars['ends_on'] = $ends->format('Y-m-d');
|
|
$vars['ends_at'] = $ends->format('H:i:s');
|
|
|
|
// Stop date
|
|
if ($stopsOn)
|
|
$vars['stops_on'] = $stopsOn->format('Y-m-d H:i:s');
|
|
|
|
switch ($data['repeats']) {
|
|
case 'weekly':
|
|
switch ($data['weekly_day']) {
|
|
// Under the hood we're overloading repeats on weekdays &
|
|
// weekends to keep the UI stupid and simple.
|
|
case 'weekdays':
|
|
case 'weekends':
|
|
$vars['repeats'] = $data['weekly_day'];
|
|
break;
|
|
default:
|
|
$vars['day'] = $data['weekly_day'];
|
|
break;
|
|
}
|
|
break;
|
|
case 'monthly':
|
|
$vars['day'] = $data['monthly_day'] ?: null;
|
|
if ($data['monthly'] == 'day')
|
|
$vars['day'] = $startsOn->format('d');
|
|
else
|
|
$vars['week'] = $data['monthly'];
|
|
break;
|
|
case 'yearly':
|
|
if ($data['yearly'] == 'date') {
|
|
$vars['week'] = null;
|
|
$vars['day'] = $startsOn->format('d');
|
|
$vars['month'] = $startsOn->format('m');
|
|
} else {
|
|
$vars['week'] = $data['yearly'];
|
|
$vars['day'] = $data['yearly_day'];
|
|
$vars['month'] = $data['yearly_month'];
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $vars;
|
|
}
|
|
|
|
function emitJavascript($options=array()) {
|
|
|
|
|
|
if (!($starts=$this->getField('starts_on')))
|
|
return;
|
|
|
|
$keys = array();
|
|
foreach (array('weekly_day' => 'day',
|
|
'monthly_day' => 'day',
|
|
'yearly_day' => 'day',
|
|
'yearly_week' => 'week',
|
|
'yearly_month' => 'month') as $k => $v) {
|
|
if (($f=$this->getField($k)))
|
|
$keys[$f->getWidget()->id] = $v;
|
|
}
|
|
$id = $starts->getWidget()->id;
|
|
?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$('#<?php echo $id; ?>').on('change', function() {
|
|
var keys = <?php echo JsonDataEncoder::encode($keys); ?>;
|
|
var d = $(this).datepicker('getDate');
|
|
if (d) {
|
|
var a = new Array();
|
|
a['month'] = d.getMonth()+1;
|
|
a['week'] = Math.ceil(d.getDate() / 7);
|
|
a['day'] = d.getDay() || 7;
|
|
console.log(a);
|
|
$.each(keys, function(key, value) {
|
|
$sel = $('#'+key);
|
|
$val = $('#'+key+' option:selected').val();
|
|
console.log(key, value, $val, a[value]);
|
|
$sel.removeClass('error');
|
|
if ($val.length == 0)
|
|
$sel.val(a[value]);
|
|
else if ($val != a[value])
|
|
$sel.addClass('error')
|
|
.bind('change fucus', function() {
|
|
$(this).removeClass('error');
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
parent::emitJavascript($options);
|
|
}
|
|
}
|
|
|
|
class ScheduleDiagnosticForm
|
|
extends AbstractForm {
|
|
static $layout = 'GridFormLayout';
|
|
function buildFields() {
|
|
$fields = array(
|
|
'date' => new DatetimeField(array(
|
|
'label' => __('Date Time'),
|
|
'required' => true,
|
|
'layout' => new GridFluidCell(6),
|
|
'configuration' => array(
|
|
'time' => true,
|
|
'gmt' => false,
|
|
'future' => false,
|
|
'max' => time(),
|
|
'showtimezone' => true,
|
|
),
|
|
)),
|
|
'hours' => new TextboxField(array(
|
|
'label' => __('Hours'),
|
|
'required' => true,
|
|
'layout' => new GridFluidCell(3),
|
|
'validator' => 'number',
|
|
'configuration' => ['size'=>10, 'length'=>10],
|
|
)),
|
|
);
|
|
return $fields;
|
|
}
|
|
|
|
function emitJavascript($options=array()) {
|
|
$date = $this->getField('date')->getWidget()->id;
|
|
$hours = $this->getField('hours')->getWidget()->id;
|
|
?>
|
|
<script type="text/javascript">
|
|
$(function() {
|
|
$('#<?php echo $date; ?>, #<?php echo $hours; ?>').on('change',
|
|
function() {
|
|
$('#diagnostic-results').hide();
|
|
});
|
|
});
|
|
</script>
|
|
<?php
|
|
parent::emitJavascript($options);
|
|
}
|
|
|
|
}
|
|
?>
|