<?php
|
|
use JVBase\managers\CacheManager;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* @return string
|
*/
|
function jvbLastMonth():string
|
{
|
$first_of_this_month = strtotime(date('Y-m-01'));
|
|
// Subtract one day to get the last day of the previous month
|
$last_of_prev_month = strtotime('-1 day', $first_of_this_month);
|
|
// Return the month name of that date
|
return date('F', $last_of_prev_month);
|
}
|
|
/**
|
* Format a date in relative time (e.g., "2 days ago")
|
* @param string $dateStr - Date string or DateTime object
|
* @return string - Formatted relative time
|
*/
|
function jvbFormatTimeAgo(string $dateStr):string
|
{
|
// Convert input to DateTime if it's a string
|
if (is_string($dateStr)) {
|
$date = new DateTime($dateStr);
|
} elseif ($dateStr instanceof DateTime) {
|
$date = $dateStr;
|
} else {
|
return '';
|
}
|
|
$now = new DateTime();
|
$diff = $now->getTimestamp() - $date->getTimestamp();
|
|
$seconds = floor($diff);
|
$minutes = floor($seconds / 60);
|
$hours = floor($minutes / 60);
|
$days = floor($hours / 24);
|
$weeks = floor($days / 7);
|
$months = floor($days / 30);
|
|
// Within the last 24 hours - show hours ago
|
if ($hours < 24) {
|
if ($hours === 0) {
|
return $minutes === 0 ? 'Just now' : sprintf('%d %s ago', $minutes, $minutes === 1 ? 'minute' : 'minutes');
|
}
|
return sprintf('%d %s ago', $hours, $hours === 1 ? 'hour' : 'hours');
|
}
|
|
// Within the last 7 days - show days ago
|
if ($days < 7) {
|
return sprintf('%d %s ago', $days, $days === 1 ? 'day' : 'days');
|
}
|
|
// Within the last 4 weeks - show week-based format
|
if ($weeks < 4) {
|
if ($weeks === 1) {
|
return 'Last week';
|
}
|
return 'A few weeks ago';
|
}
|
|
// Within the last 2 months - show "Last month"
|
if ($months < 2) {
|
return 'Last month';
|
}
|
|
// For anything older, show the full date
|
return $date->format('F j, Y');
|
}
|
|
|
/**
|
* @param string $time
|
*
|
* @return string
|
*/
|
function jvbFormat12HourTime(string $time):string
|
{
|
if (!$time) {
|
return '';
|
}
|
// Split the time into hours and minutes
|
list($hours, $minutes) = explode(':', $time);
|
|
// Convert to integers
|
$hours = (int)$hours;
|
$minutes = (int)$minutes;
|
|
// Determine AM/PM
|
$ampm = ($hours >= 12) ? 'pm' : 'am';
|
|
// Convert to 12-hour format
|
$hours = $hours % 12;
|
if ($hours === 0) {
|
$hours = 12;
|
}
|
|
// Format the final string
|
if ($minutes === 0) {
|
return $hours . $ampm; // No minutes needed for whole hours (e.g., 10am)
|
} else {
|
return $hours . ':' . sprintf('%02d', $minutes) . $ampm; // Include minutes (e.g., 5:30pm)
|
}
|
}
|
|
/**
|
* @param string $start
|
* @param string $end
|
* @param bool $shortened
|
*
|
* @return string
|
*/
|
function formatRange(string $start, string $end, bool $shortened = false):string
|
{
|
// Create a formatting function
|
$formatDay = function ($day) use ($shortened) {
|
return ucfirst($shortened ? substr($day, 0, 3) : $day);
|
};
|
|
// Apply formatting to both days
|
$formattedStart = $formatDay($start);
|
$formattedEnd = $formatDay($end);
|
|
// Return single day or range as appropriate
|
return ($start === $end) ? $formattedStart : "$formattedStart-$formattedEnd";
|
}
|
|
/**
|
* @param int $ID
|
* @param JVBase\Meta\MetaManager $meta
|
*
|
* @return string
|
*/
|
function jvbRenderHours(int $ID, JVBase\Meta\MetaManager $meta):string
|
{
|
$cache = CacheManager::for('hours-'.$ID, WEEK_IN_SECONDS);
|
$key = 'hours_display';
|
$cached = $cache->get($key);
|
|
if ($cached !== false) {
|
return $cached;
|
}
|
|
if (!$meta) {
|
if (term_exists((int)$ID)) {
|
$type = 'term';
|
} elseif (get_post_status((int)$ID)) {
|
$type = 'post';
|
} else {
|
$type = 'user';
|
}
|
$meta = new JVBase\meta\MetaManager($ID, $type);
|
}
|
|
$hours = $meta->getValue('hours');
|
$byAppt = $meta->getValue('by_appointment');
|
$walkins = $meta->getValue('walkins');
|
|
$out = '';
|
|
if (!empty($hours) && jvbHasOperatingHours($hours)) {
|
$out = jvbGetFormattedHours($hours, true, true);
|
} else {
|
$out = '<p class="no-hours">Hours not available</p>';
|
}
|
|
// Add appointment and walk-in information
|
$notes = [];
|
if ($byAppt) {
|
$notes[] = 'By appointment only';
|
}
|
if ($walkins) {
|
$notes[] = 'Walk-ins welcome';
|
}
|
|
if (!empty($notes)) {
|
$out .= '<p class="hours-notes"><small>' . implode(' • ', $notes) . '</small></p>';
|
}
|
|
$cache->set($key, $out);
|
return $out;
|
}
|
|
/**
|
* @param array $daysList
|
* @param $short
|
*
|
* @return string
|
*/
|
function jvbCondenseDayRange(array $daysList, $short = false):string
|
{
|
if (count($daysList) === 7 || $daysList[0] === 'daily') {
|
return 'Daily';
|
}
|
// Define the complete list of days in order
|
$allDays = jvbFullWeekdays();
|
|
// Create a mapping of day names to their indices
|
$dayIndices = array_flip($allDays);
|
|
// Split the input into individual days and normalize
|
$days = array_map('trim', $daysList);
|
$days = array_map('strtolower', $days);
|
if ($short) {
|
}
|
|
// Sort the days according to their position in the week
|
usort($days, function ($a, $b) use ($dayIndices) {
|
return $dayIndices[$a] - $dayIndices[$b];
|
});
|
|
// Find consecutive ranges
|
$ranges = [];
|
$rangeStart = null;
|
$rangeEnd = null;
|
|
foreach ($days as $i => $day) {
|
if ($rangeStart === null) {
|
$rangeStart = $day;
|
$rangeEnd = $day;
|
} elseif ($dayIndices[$day] == $dayIndices[$rangeEnd] + 1) {
|
// This day is consecutive to the previous one
|
$rangeEnd = $day;
|
} else {
|
// This day breaks the sequence, save the current range
|
$ranges[] = formatRange($rangeStart, $rangeEnd, $short);
|
$rangeStart = $day;
|
$rangeEnd = $day;
|
}
|
}
|
|
// Add the last range
|
if ($rangeStart !== null) {
|
$ranges[] = formatRange($rangeStart, $rangeEnd, $short);
|
}
|
|
// Join the ranges with commas
|
return implode(', ', $ranges);
|
}
|
|
/**
|
* @param string $range wed-thur or wednesday-thursday
|
*
|
* @return string
|
*/
|
function jvbExpandDayRange(string $range):string
|
{
|
if ($range === 'daily') {
|
return 'daily';
|
}
|
// Define the complete list of days in order
|
$allDays = jvbFullWeekdays();
|
$shortDays = ['mon', 'tue', 'wed', 'thu', 'fri','sat','sun'];
|
// Split the range into start and end days
|
[$startDay, $endDay] = explode('-', strtolower($range));
|
error_log('Start Day: '.print_r($startDay, true));
|
error_log('End Day: '.print_r($endDay, true));
|
$days = (in_array($startDay, $allDays)) ?
|
$allDays :
|
((in_array($startDay, $shortDays)) ? $shortDays : false);
|
error_log('Final Days: '.print_r($days, true));
|
if (!$days) {
|
return '';
|
}
|
// Find the positions of start and end days
|
$startIndex = array_search(strtolower($startDay), $days);
|
$endIndex = array_search(strtolower($endDay), $days);
|
|
// Handle case where end comes before start in the week (wrapping around)
|
if ($startIndex > $endIndex) {
|
$endIndex += 7;
|
}
|
|
// Extract the days in the range
|
$result = [];
|
for ($i = $startIndex; $i <= $endIndex; $i++) {
|
$result[] = $allDays[$i % 7];
|
}
|
if (count($result) === 7) {
|
$result = ['daily'];
|
}
|
|
// Join with commas and return
|
return implode(',', $result);
|
}
|
|
/**
|
* @return array
|
*/
|
function jvbFullWeekdays():array
|
{
|
return ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
}
|
|
|
/**
|
* Check if business is currently open based on stored hours
|
*
|
* @param array $hours_data Array of hours data from wp_option
|
* @param string $timezone Timezone string (default: 'America/Edmonton')
|
* @return bool True if currently open, false if closed
|
*/
|
function jvbIsCurrentlyOpen($hours_data = null, $timezone = 'America/Edmonton') {
|
// Get hours data if not provided
|
if ($hours_data === null) {
|
$hours_data = get_option(BASE.'hours', []);
|
}
|
|
// Return false if no hours data
|
if (empty($hours_data) || !is_array($hours_data)) {
|
return false;
|
}
|
|
// Set timezone for current time
|
$current_time = new DateTime('now', new DateTimeZone($timezone));
|
$current_day = strtolower($current_time->format('l')); // monday, tuesday, etc.
|
$current_time_string = $current_time->format('H:i'); // 24-hour format
|
|
// Get today's hours
|
$today_hours = $hours_data[$current_day] ?? [];
|
|
// Return false if closed today
|
if (empty($today_hours) || !($today_hours['open'] ?? false)) {
|
return false;
|
}
|
|
$open_time = $today_hours['time_opens'] ?? '';
|
$close_time = $today_hours['time_closes'] ?? '';
|
|
if (!$open_time || !$close_time) {
|
return false;
|
}
|
|
// Handle different time formats (with or without seconds)
|
$open_time = date('H:i', strtotime($open_time));
|
$close_time = date('H:i', strtotime($close_time));
|
|
// Check if current time is within operating hours
|
if ($close_time > $open_time) {
|
// Normal case: opens and closes on same day
|
return ($current_time_string >= $open_time && $current_time_string <= $close_time);
|
} else {
|
// Handles overnight hours (e.g., 22:00 to 02:00)
|
return ($current_time_string >= $open_time || $current_time_string <= $close_time);
|
}
|
}
|
|
/**
|
* Check if current time is between two specified times
|
*
|
* @param string $start_time Start time in H:i format (e.g., '10:00')
|
* @param string $end_time End time in H:i format (e.g., '15:15')
|
* @param string $timezone Timezone string (default: 'America/Edmonton')
|
* @return bool True if current time is within range, false otherwise
|
*/
|
function jvbIsTimeBetween($start_time=null, $end_time=null, $timezone = 'America/Edmonton') {
|
if (!$start_time && !$end_time) {
|
$hours = get_option(BASE.'today_hours');
|
$start_time = $hours['time_start'];
|
$end_time = $hours['time_end'];
|
}
|
|
// Get current time in specified timezone
|
$current_time = new DateTime('now', new DateTimeZone($timezone));
|
$current_time_string = $current_time->format('H:i');
|
|
// Normalize time formats (handle with or without seconds)
|
$start_time = date('H:i', strtotime($start_time));
|
$end_time = date('H:i', strtotime($end_time));
|
|
// Check if current time is within range
|
if ($end_time > $start_time) {
|
// Normal case: same day (e.g., 10:00 to 15:15)
|
return ($current_time_string >= $start_time && $current_time_string <= $end_time);
|
} else {
|
// Overnight case: spans midnight (e.g., 22:00 to 02:00)
|
return ($current_time_string >= $start_time || $current_time_string <= $end_time);
|
}
|
}
|
|
/**
|
* Get next opening time for business
|
*
|
* @param array $hours_data Day-based hours data
|
* @param string $timezone Timezone string
|
* @return string|null Next opening time description or null if never opens
|
*/
|
function jvbGetNextOpeningTime(array $hours_data, string $timezone = 'America/Edmonton'): ?string {
|
if (!jvbHasOperatingHours($hours_data)) {
|
return null;
|
}
|
|
$current_time = new DateTime('now', new DateTimeZone($timezone));
|
$weekdays = jvbFullWeekdays();
|
|
// Check next 7 days
|
for ($i = 0; $i < 7; $i++) {
|
$check_date = clone $current_time;
|
$check_date->modify("+{$i} day");
|
$day_name = strtolower($check_date->format('l'));
|
|
$day_data = $hours_data[$day_name] ?? [];
|
if (empty($day_data) || !($day_data['open'] ?? false)) {
|
continue;
|
}
|
|
$open_time = $day_data['time_opens'] ?? '';
|
if (!$open_time) {
|
continue;
|
}
|
|
// If it's today, make sure we haven't passed opening time
|
if ($i === 0) {
|
$today_open = DateTime::createFromFormat('H:i', $open_time, new DateTimeZone($timezone));
|
$today_open->setDate(
|
$current_time->format('Y'),
|
$current_time->format('m'),
|
$current_time->format('d')
|
);
|
|
if ($current_time >= $today_open) {
|
continue; // Already passed today's opening time
|
}
|
}
|
|
// Format the result
|
if ($i === 0) {
|
return 'Opens today at ' . jvbFormat12HourTime($open_time);
|
} elseif ($i === 1) {
|
return 'Opens tomorrow at ' . jvbFormat12HourTime($open_time);
|
} else {
|
$day_formatted = ucfirst($day_name);
|
return "Opens {$day_formatted} at " . jvbFormat12HourTime($open_time);
|
}
|
}
|
|
return null;
|
}
|
|
|
/**
|
* Check if business has any operating hours
|
*
|
* @param array $hours_data Day-based hours data
|
* @return bool True if has any open days, false if always closed
|
*/
|
function jvbHasOperatingHours(array $hours_data): bool {
|
foreach ($hours_data as $day_data) {
|
if (!empty($day_data) && ($day_data['open'] ?? false)) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
/**
|
* Get formatted hours for display with additional context
|
*
|
* @param array $hours_data Day-based hours data
|
* @param bool $include_notes Include additional notes like "By appointment" etc.
|
* @param bool $short Use short day names
|
* @return string Complete formatted hours display
|
*/
|
function jvbGetFormattedHours(array $hours_data, bool $include_notes = true, bool $short = true): string {
|
$condensed_hours = jvbRenderCondensedHours($hours_data, $short, 'list-none');
|
|
$output = $condensed_hours;
|
|
if ($include_notes) {
|
$notes = [];
|
|
// Check for special conditions (you can extend this based on your needs)
|
$has_weekend_hours = !empty($hours_data['saturday']) || !empty($hours_data['sunday']);
|
$has_all_days = true;
|
|
foreach (jvbFullWeekdays() as $day) {
|
if (empty($hours_data[$day]) || !($hours_data[$day]['open'] ?? false)) {
|
$has_all_days = false;
|
break;
|
}
|
}
|
|
if (!$has_all_days) {
|
$notes[] = '<small><i>Hours may be different on holidays</i></small>';
|
}
|
|
if (!empty($notes)) {
|
$output .= '<p>' . implode(' ', $notes) . '</p>';
|
}
|
}
|
|
return $output;
|
}
|
|
|
/**
|
* Render condensed hours as HTML list
|
*
|
* @param array $hours_data Day-based hours data
|
* @param bool $short Use short day names
|
* @param string $class_name CSS class for the list
|
* @return string HTML formatted hours list
|
*/
|
function jvbRenderCondensedHours(array $hours_data, bool $short = true, string $class_name = 'hours-list'): string {
|
$condensed = jvbCondenseHours($hours_data, $short);
|
|
if (empty($condensed)) {
|
return '<p class="no-hours">Hours not available</p>';
|
}
|
|
$html = "<ul class=\"{$class_name}\">";
|
foreach ($condensed as $hours_line) {
|
$html .= "<li>{$hours_line}</li>";
|
}
|
$html .= "</ul>";
|
|
return $html;
|
}
|
|
/**
|
* Create a unique signature for hours to compare identical time slots
|
*
|
* @param array $day_data Single day's hours data
|
* @return string Unique signature for the hours
|
*/
|
function jvbGetHoursSignature(array $day_data): string {
|
if (empty($day_data) || !($day_data['open'] ?? false)) {
|
return 'closed';
|
}
|
|
$opens = $day_data['time_opens'] ?? '';
|
$closes = $day_data['time_closes'] ?? '';
|
|
// Normalize times to ensure consistent comparison
|
$opens = date('H:i', strtotime($opens));
|
$closes = date('H:i', strtotime($closes));
|
|
return "{$opens}-{$closes}";
|
}
|
|
/**
|
* Group consecutive days with identical hours
|
*
|
* @param array $hours_data Day-based hours data
|
* @return array Grouped hours with consecutive days
|
*/
|
function jvbGroupIdenticalHours(array $hours_data): array {
|
$weekdays = jvbFullWeekdays();
|
$groups = [];
|
$current_group = null;
|
|
foreach ($weekdays as $day) {
|
$day_data = $hours_data[$day] ?? [];
|
|
// Skip closed days (empty arrays or open = false)
|
if (empty($day_data) || !($day_data['open'] ?? false)) {
|
// Finalize current group if we hit a closed day
|
if ($current_group !== null) {
|
$groups[] = $current_group;
|
$current_group = null;
|
}
|
continue;
|
}
|
|
$hours_signature = jvbGetHoursSignature($day_data);
|
|
// Start new group or continue existing one
|
if ($current_group === null) {
|
// Start new group
|
$current_group = [
|
'time_opens' => $day_data['time_opens'] ?? '',
|
'time_closes' => $day_data['time_closes'] ?? '',
|
'signature' => $hours_signature,
|
'days' => [$day]
|
];
|
} elseif ($current_group['signature'] === $hours_signature) {
|
// Same hours, add to current group
|
$current_group['days'][] = $day;
|
} else {
|
// Different hours, finalize current group and start new one
|
$groups[] = $current_group;
|
$current_group = [
|
'time_opens' => $day_data['time_opens'] ?? '',
|
'time_closes' => $day_data['time_closes'] ?? '',
|
'signature' => $hours_signature,
|
'days' => [$day]
|
];
|
}
|
}
|
|
// Don't forget the last group
|
if ($current_group !== null) {
|
$groups[] = $current_group;
|
}
|
|
return $groups;
|
}
|
|
/**
|
* Condense hours data into readable format with consecutive day ranges
|
*
|
* @param array $hours_data Day-based hours data from new structure
|
* @param bool $short Use short day names (Mon vs Monday)
|
* @return array Array of condensed hours strings
|
*/
|
function jvbCondenseHours(array $hours_data, bool $short = true): array {
|
if (empty($hours_data)) {
|
return [];
|
}
|
|
// Group identical hours together
|
$grouped_hours = jvbGroupIdenticalHours($hours_data);
|
|
// Convert groups to readable format
|
$condensed = [];
|
foreach ($grouped_hours as $group) {
|
$days_display = jvbCondenseDayRange($group['days'], $short);
|
$open_time = jvbFormat12HourTime($group['time_opens']);
|
$close_time = jvbFormat12HourTime($group['time_closes']);
|
|
$condensed[] = "{$days_display} {$open_time} - {$close_time}";
|
}
|
|
return $condensed;
|
}
|