mirror of
https://github.com/itflow-org/itflow
synced 2026-08-28 02:15:13 +00:00
Add Holiday / Closure Days to SLA to pause SLA timer on defined days, also has a US Holiday Importer
This commit is contained in:
26
admin/database_updates/2.7.4.php
Normal file
26
admin/database_updates/2.7.4.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.7.4 (from 2.7.3)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// Closure days for the SLA business calendar. A date listed here yields no
|
||||
// business minutes at all, exactly like a weekday that is not a business
|
||||
// day - holidays are a calendar concept, not a per-ticket pause, so they
|
||||
// never touch sla_history.
|
||||
//
|
||||
// holiday_date is UNIQUE so the US federal holiday generator can be run
|
||||
// repeatedly (and over a year that was partly entered by hand) without
|
||||
// creating duplicates.
|
||||
|
||||
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `business_holidays` (
|
||||
`holiday_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`holiday_date` date NOT NULL,
|
||||
`holiday_name` varchar(200) NOT NULL,
|
||||
`holiday_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`holiday_id`),
|
||||
UNIQUE KEY `holiday_date` (`holiday_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
40
admin/modals/sla/holiday_add.php
Normal file
40
admin/modals/sla/holiday_add.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once '../../includes/modal_header.php';
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-calendar-times me-2"></i>New Closure Day</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label>Date <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-calendar-day"></i></span>
|
||||
<input type="date" class="form-control" name="holiday_date" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" name="holiday_name" placeholder="e.g. Christmas Day, Office closed for move" maxlength="200" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="text-muted">SLA clocks do not run on this day at all, the same as a day outside your business days. Open tickets have their targets recalculated when you save.</small>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_holiday" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Add</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -113,12 +113,7 @@ if (isset($_POST['edit_sla_settings'])) {
|
||||
getSlaSettings(true);
|
||||
|
||||
// Business hours feed the due date math - re-stamp open SLA tickets
|
||||
$restamped = 0;
|
||||
$sql_tickets = mysqli_query($mysqli, "SELECT ticket_id, ticket_sla_id FROM tickets WHERE ticket_sla_id > 0 AND ticket_closed_at IS NULL AND ticket_archived_at IS NULL");
|
||||
while ($ticket_row = mysqli_fetch_assoc($sql_tickets)) {
|
||||
applyTicketSla($ticket_row['ticket_id'], $ticket_row['ticket_sla_id']);
|
||||
$restamped++;
|
||||
}
|
||||
$restamped = restampOpenSlaTickets();
|
||||
|
||||
logAudit("Settings", "Edit", "$session_name edited SLA / business hours settings");
|
||||
|
||||
@@ -128,6 +123,105 @@ if (isset($_POST['edit_sla_settings'])) {
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['add_holiday'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
// Deliberately NOT validateDate() - that falls back to today's date on bad
|
||||
// input, which would silently close the office today. Reject instead. The
|
||||
// round-trip comparison also catches impossible dates like 2026-02-30,
|
||||
// which createFromFormat would otherwise roll forward into March.
|
||||
$holiday_date_input = $_POST['holiday_date'] ?? '';
|
||||
$parsed_date = DateTime::createFromFormat('Y-m-d', $holiday_date_input);
|
||||
|
||||
if (!$parsed_date || $parsed_date->format('Y-m-d') !== $holiday_date_input) {
|
||||
flashAlert("Enter a valid date for the closure day.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$holiday_name_input = trim($_POST['holiday_name'] ?? '');
|
||||
if ($holiday_name_input === '') {
|
||||
flashAlert("Enter a name for the closure day.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$holiday_date = escapeSql($holiday_date_input);
|
||||
$holiday_name = escapeSql($holiday_name_input);
|
||||
|
||||
// INSERT IGNORE rather than an error: the date is UNIQUE, and re-adding a day
|
||||
// that is already listed is a no-op the operator does not need telling about
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO business_holidays SET holiday_date = '$holiday_date', holiday_name = '$holiday_name'");
|
||||
|
||||
getBusinessHolidays(true);
|
||||
$restamped = restampOpenSlaTickets();
|
||||
|
||||
logAudit("Settings", "Create", "$session_name added SLA closure day $holiday_date - $holiday_name");
|
||||
|
||||
flashAlert("Closure day added - targets recalculated on $restamped open ticket(s)");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['delete_holiday'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$holiday_id = intval($_POST['holiday_id']);
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT holiday_date, holiday_name FROM business_holidays WHERE holiday_id = $holiday_id LIMIT 1"));
|
||||
if (!$row) {
|
||||
flashAlert("Closure day not found.", 'error');
|
||||
redirect();
|
||||
}
|
||||
$holiday_date = escapeSql($row['holiday_date']);
|
||||
$holiday_name = escapeSql($row['holiday_name']);
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM business_holidays WHERE holiday_id = $holiday_id");
|
||||
|
||||
getBusinessHolidays(true);
|
||||
$restamped = restampOpenSlaTickets();
|
||||
|
||||
logAudit("Settings", "Delete", "$session_name removed SLA closure day $holiday_date - $holiday_name");
|
||||
|
||||
flashAlert("Closure day removed - targets recalculated on $restamped open ticket(s)");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['generate_holidays'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$holiday_year = intval($_POST['holiday_year']);
|
||||
|
||||
if ($holiday_year < 2000 || $holiday_year > 2100) {
|
||||
flashAlert("Enter a year between 2000 and 2100.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
// Existing rows win - INSERT IGNORE leaves a hand-entered name on a date the
|
||||
// generator also produces, so running this over a partly-filled year is safe
|
||||
$added = 0;
|
||||
foreach (usFederalHolidays($holiday_year) as $holiday) {
|
||||
$holiday_date = escapeSql($holiday['date']);
|
||||
$holiday_name = escapeSql($holiday['name']);
|
||||
mysqli_query($mysqli, "INSERT IGNORE INTO business_holidays SET holiday_date = '$holiday_date', holiday_name = '$holiday_name'");
|
||||
$added += mysqli_affected_rows($mysqli) > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
getBusinessHolidays(true);
|
||||
$restamped = restampOpenSlaTickets();
|
||||
|
||||
logAudit("Settings", "Create", "$session_name generated $added US federal holiday closure day(s) for $holiday_year");
|
||||
|
||||
flashAlert("Added $added US federal holiday(s) for $holiday_year - targets recalculated on $restamped open ticket(s)");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['save_sla_assignments'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
@@ -22,6 +22,15 @@ while ($active_sla_row = mysqli_fetch_assoc($sql_active_slas)) {
|
||||
$active_slas[intval($active_sla_row['sla_id'])] = $active_sla_row['sla_name'];
|
||||
}
|
||||
|
||||
// Closure days, newest first - past ones are kept as a record of what was applied
|
||||
$holidays = [];
|
||||
$sql_holidays = mysqli_query($mysqli, "SELECT holiday_date, holiday_id, holiday_name FROM business_holidays ORDER BY holiday_date DESC");
|
||||
while ($holiday_row = mysqli_fetch_assoc($sql_holidays)) {
|
||||
$holidays[] = $holiday_row;
|
||||
}
|
||||
|
||||
$holiday_year = intval(date('Y'));
|
||||
|
||||
// Global default assignments: [priority] = sla_id (per-client overrides live on the client edit modal)
|
||||
$assignments = [];
|
||||
$sql_assignments = mysqli_query($mysqli, "SELECT sla_assignment_priority, sla_assignment_sla_id FROM sla_assignments WHERE sla_assignment_client_id = 0");
|
||||
@@ -196,4 +205,70 @@ while ($assignment_row = mysqli_fetch_assoc($sql_assignments)) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-calendar-times me-2"></i>Holidays & Closure Days</h3>
|
||||
<div class="card-tools">
|
||||
<form action="post.php" method="post" class="d-inline" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<div class="input-group input-group-sm d-inline-flex w-auto align-middle me-2">
|
||||
<input type="number" class="form-control" name="holiday_year" min="2000" max="2100" value="<?= $holiday_year ?>" required>
|
||||
<button type="submit" name="generate_holidays" class="btn btn-secondary"><i class="fas fa-magic me-2"></i>Add US Holidays</button>
|
||||
</div>
|
||||
</form>
|
||||
<button type="button" class="btn btn-sm btn-primary ajax-modal" data-modal-url="modals/sla/holiday_add.php"><i class="fas fa-plus me-2"></i>New Closure Day</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<p class="text-muted">
|
||||
SLA clocks stop completely on these dates, the same as a day outside your business days.
|
||||
Use them for public holidays or any other day the office is shut. Adding or removing one
|
||||
recalculates the targets on every open ticket.
|
||||
</p>
|
||||
|
||||
<?php if (empty($holidays)) { ?>
|
||||
<p class="text-secondary mb-0">No closure days configured - SLA clocks run on every business day.</p>
|
||||
<?php } else { ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-hover align-middle">
|
||||
<thead class="border-bottom">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Day</th>
|
||||
<th>Name</th>
|
||||
<th class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($holidays as $holiday) {
|
||||
$holiday_id = intval($holiday['holiday_id']);
|
||||
$holiday_date_raw = $holiday['holiday_date'];
|
||||
$holiday_date = escapeHtml(date('M j, Y', strtotime($holiday_date_raw)));
|
||||
$holiday_day = escapeHtml(date('l', strtotime($holiday_date_raw)));
|
||||
$holiday_name = escapeHtml($holiday['holiday_name']);
|
||||
$holiday_is_past = strtotime($holiday_date_raw) < strtotime(date('Y-m-d'));
|
||||
?>
|
||||
<tr class="<?php if ($holiday_is_past) { echo "text-secondary"; } ?>">
|
||||
<td><?= $holiday_date ?></td>
|
||||
<td><?= $holiday_day ?></td>
|
||||
<td><?= $holiday_name ?></td>
|
||||
<td class="text-end">
|
||||
<form action="post.php" method="post" onsubmit="return confirm('Remove this closure day? Open ticket targets will be recalculated.');">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
<input type="hidden" name="holiday_id" value="<?= $holiday_id ?>">
|
||||
<button type="submit" name="delete_holiday" class="btn btn-sm btn-light border"><i class="fas fa-fw fa-trash text-danger"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
|
||||
19
db.sql
19
db.sql
@@ -397,6 +397,23 @@ CREATE TABLE `budget` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `business_holidays`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `business_holidays`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `business_holidays` (
|
||||
`holiday_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`holiday_date` date NOT NULL,
|
||||
`holiday_name` varchar(200) NOT NULL,
|
||||
`holiday_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`holiday_id`),
|
||||
UNIQUE KEY `holiday_date` (`holiday_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `calendar_event_attendees`
|
||||
--
|
||||
@@ -3196,4 +3213,4 @@ CREATE TABLE `vendors` (
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2026-08-26 23:37:25
|
||||
-- Dump completed on 2026-08-27 17:57:04
|
||||
|
||||
@@ -59,8 +59,119 @@ function getSlaSettings($refresh = false)
|
||||
return $sla_settings;
|
||||
}
|
||||
|
||||
// Closure days for the business calendar, fetched once per request. Returns a
|
||||
// map of 'Y-m-d' => holiday name so the day-walk in addBusinessMinutes and
|
||||
// businessMinutesBetween can do an O(1) lookup rather than a query per day -
|
||||
// those loops run up to 731 iterations.
|
||||
//
|
||||
// Callers that have just written to business_holidays pass true, same contract
|
||||
// as getSlaSettings().
|
||||
function getBusinessHolidays($refresh = false)
|
||||
{
|
||||
global $mysqli;
|
||||
|
||||
static $holidays = null;
|
||||
|
||||
if ($refresh) {
|
||||
$holidays = null;
|
||||
}
|
||||
|
||||
if (!is_null($holidays)) {
|
||||
return $holidays;
|
||||
}
|
||||
|
||||
$holidays = [];
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT holiday_date, holiday_name FROM business_holidays");
|
||||
if ($sql) {
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$holidays[$row['holiday_date']] = $row['holiday_name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $holidays;
|
||||
}
|
||||
|
||||
// US federal holidays for a calendar year, as a list of ['date' => 'Y-m-d',
|
||||
// 'name' => string]. Six of the eleven float on an nth-weekday rule, which
|
||||
// strtotime() understands directly, so no recurrence table is needed - the
|
||||
// generator writes concrete dates and the lookup above stays an exact match.
|
||||
//
|
||||
// Fixed-date holidays are shifted to the OBSERVED day (Saturday -> the Friday
|
||||
// before, Sunday -> the Monday after), because that is the weekday a business
|
||||
// actually closes. The floating ones always land on a Monday or Thursday and
|
||||
// need no shift.
|
||||
function usFederalHolidays($year)
|
||||
{
|
||||
$year = intval($year);
|
||||
|
||||
$observed = function ($date) {
|
||||
$day = intval(date('N', strtotime($date)));
|
||||
if ($day == 6) {
|
||||
return date('Y-m-d', strtotime($date . ' -1 day'));
|
||||
}
|
||||
if ($day == 7) {
|
||||
return date('Y-m-d', strtotime($date . ' +1 day'));
|
||||
}
|
||||
return $date;
|
||||
};
|
||||
|
||||
$fixed = [
|
||||
"$year-01-01" => "New Year's Day",
|
||||
"$year-06-19" => 'Juneteenth',
|
||||
"$year-07-04" => 'Independence Day',
|
||||
"$year-11-11" => 'Veterans Day',
|
||||
"$year-12-25" => 'Christmas Day',
|
||||
];
|
||||
|
||||
$floating = [
|
||||
"third monday of january $year" => 'Martin Luther King Jr. Day',
|
||||
"third monday of february $year" => "Presidents' Day",
|
||||
"last monday of may $year" => 'Memorial Day',
|
||||
"first monday of september $year" => 'Labor Day',
|
||||
"second monday of october $year" => 'Columbus Day',
|
||||
"fourth thursday of november $year" => 'Thanksgiving Day',
|
||||
];
|
||||
|
||||
$holidays = [];
|
||||
|
||||
foreach ($fixed as $date => $name) {
|
||||
$holidays[] = ['date' => $observed($date), 'name' => $name];
|
||||
}
|
||||
|
||||
foreach ($floating as $rule => $name) {
|
||||
$holidays[] = ['date' => date('Y-m-d', strtotime($rule)), 'name' => $name];
|
||||
}
|
||||
|
||||
usort($holidays, function ($a, $b) {
|
||||
return strcmp($a['date'], $b['date']);
|
||||
});
|
||||
|
||||
return $holidays;
|
||||
}
|
||||
|
||||
// Re-stamp every open SLA ticket. Business hours and closure days both feed the
|
||||
// due-date math, so anything that changes the calendar has to run this or the
|
||||
// change only applies to tickets raised afterwards - which is the opposite of
|
||||
// what an operator adding next week's shutdown expects. Returns the count.
|
||||
function restampOpenSlaTickets()
|
||||
{
|
||||
global $mysqli;
|
||||
|
||||
$restamped = 0;
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_sla_id FROM tickets WHERE ticket_sla_id > 0 AND ticket_closed_at IS NULL AND ticket_archived_at IS NULL");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
applyTicketSla($row['ticket_id'], $row['ticket_sla_id']);
|
||||
$restamped++;
|
||||
}
|
||||
|
||||
return $restamped;
|
||||
}
|
||||
|
||||
// Add $minutes of business time to a datetime, honouring the configured
|
||||
// business days and hours. Returns a Y-m-d H:i:s string in the app timezone
|
||||
// business days and hours, and skipping closure days entirely. Returns a
|
||||
// Y-m-d H:i:s string in the app timezone
|
||||
// (includes/inc_set_timezone.php has already set it). With no usable business
|
||||
// calendar configured the clock is treated as 24x7.
|
||||
function addBusinessMinutes($start_datetime, $minutes)
|
||||
@@ -82,6 +193,8 @@ function addBusinessMinutes($start_datetime, $minutes)
|
||||
return $cursor->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$holidays = getBusinessHolidays();
|
||||
|
||||
$remaining_seconds = $minutes * 60;
|
||||
|
||||
// Walk forward a day at a time consuming available business time. Interval
|
||||
@@ -90,10 +203,13 @@ function addBusinessMinutes($start_datetime, $minutes)
|
||||
// the honest reading. Guard: two years of calendar.
|
||||
for ($i = 0; $i < 731; $i++) {
|
||||
|
||||
if (in_array(intval($cursor->format('N')), $business_days)) {
|
||||
$date_key = $cursor->format('Y-m-d');
|
||||
|
||||
$window_start = new DateTime($cursor->format('Y-m-d') . " $day_start");
|
||||
$window_end = new DateTime($cursor->format('Y-m-d') . " $day_end");
|
||||
// A closure day yields no business time, same as a non-business weekday
|
||||
if (in_array(intval($cursor->format('N')), $business_days) && !isset($holidays[$date_key])) {
|
||||
|
||||
$window_start = new DateTime($date_key . " $day_start");
|
||||
$window_end = new DateTime($date_key . " $day_end");
|
||||
|
||||
if ($cursor < $window_start) {
|
||||
$cursor = $window_start;
|
||||
@@ -123,7 +239,8 @@ function addBusinessMinutes($start_datetime, $minutes)
|
||||
|
||||
// Business minutes elapsed between two datetimes - the inverse of
|
||||
// addBusinessMinutes, used to measure how much of a resolution budget an
|
||||
// interval actually consumed.
|
||||
// interval actually consumed. Skips closure days for the same reason: time
|
||||
// nobody was working must not be charged to a ticket's budget.
|
||||
function businessMinutesBetween($start_datetime, $end_datetime)
|
||||
{
|
||||
$start = new DateTime($start_datetime);
|
||||
@@ -142,6 +259,8 @@ function businessMinutesBetween($start_datetime, $end_datetime)
|
||||
return intval(floor(($end->getTimestamp() - $start->getTimestamp()) / 60));
|
||||
}
|
||||
|
||||
$holidays = getBusinessHolidays();
|
||||
|
||||
$seconds = 0;
|
||||
$cursor = clone $start;
|
||||
|
||||
@@ -152,10 +271,12 @@ function businessMinutesBetween($start_datetime, $end_datetime)
|
||||
break;
|
||||
}
|
||||
|
||||
if (in_array(intval($cursor->format('N')), $business_days)) {
|
||||
$date_key = $cursor->format('Y-m-d');
|
||||
|
||||
$window_start = new DateTime($cursor->format('Y-m-d') . " $day_start");
|
||||
$window_end = new DateTime($cursor->format('Y-m-d') . " $day_end");
|
||||
if (in_array(intval($cursor->format('N')), $business_days) && !isset($holidays[$date_key])) {
|
||||
|
||||
$window_start = new DateTime($date_key . " $day_start");
|
||||
$window_end = new DateTime($date_key . " $day_end");
|
||||
|
||||
$from = $cursor > $window_start ? $cursor : $window_start;
|
||||
$to = $end < $window_end ? $end : $window_end;
|
||||
|
||||
Reference in New Issue
Block a user