Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -30,50 +30,42 @@ use App\RSpade\Core\Files\File_Storage_Model;
|
|||||||
* provides the basic structure for categorizing uploaded files.
|
* provides the basic structure for categorizing uploaded files.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* _AUTO_GENERATED_ Database type hints - do not edit manually
|
* _AUTO_GENERATED_
|
||||||
* Generated on: 2025-12-26 02:43:29
|
* @property integer $id
|
||||||
* Table: _file_attachments
|
* @property string $key
|
||||||
*
|
* @property integer $file_storage_id
|
||||||
* @property int $id
|
* @property string $file_name
|
||||||
* @property mixed $key
|
* @property string $file_extension
|
||||||
* @property int $file_storage_id
|
* @property integer $file_type_id
|
||||||
* @property mixed $file_name
|
* @property integer $width
|
||||||
* @property mixed $file_extension
|
* @property integer $height
|
||||||
* @property int $file_type_id
|
* @property integer $duration
|
||||||
* @property int $width
|
* @property boolean $is_animated
|
||||||
* @property int $height
|
* @property integer $frame_count
|
||||||
* @property int $duration
|
* @property integer $fileable_type
|
||||||
* @property bool $is_animated
|
* @property integer $fileable_id
|
||||||
* @property int $frame_count
|
* @property string $fileable_category
|
||||||
* @property mixed $fileable_type
|
* @property string $fileable_type_meta
|
||||||
* @property int $fileable_id
|
* @property integer $fileable_order
|
||||||
* @property mixed $fileable_category
|
|
||||||
* @property mixed $fileable_type_meta
|
|
||||||
* @property int $fileable_order
|
|
||||||
* @property string $fileable_meta
|
* @property string $fileable_meta
|
||||||
* @property int $site_id
|
* @property integer $site_id
|
||||||
* @property mixed $session_id
|
* @property string $session_id
|
||||||
* @property string $created_at
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property string $updated_at
|
* @property \Carbon\Carbon $updated_at
|
||||||
* @property int $created_by
|
* @property integer $created_by
|
||||||
* @property int $updated_by
|
* @property integer $updated_by
|
||||||
*
|
* @method static mixed file_type_id_enum()
|
||||||
* @property-read string $file_type_id__label
|
* @method static mixed file_type_id_enum_select()
|
||||||
* @property-read string $file_type_id__constant
|
* @method static mixed file_type_id_enum_ids()
|
||||||
*
|
* @property-read mixed $file_type_id_constant
|
||||||
* @method static array file_type_id__enum() Get all enum definitions with full metadata
|
* @property-read mixed $file_type_id_label
|
||||||
* @method static array file_type_id__enum_select() Get selectable items for dropdowns
|
|
||||||
* @method static array file_type_id__enum_labels() Get simple id => label map
|
|
||||||
* @method static array file_type_id__enum_ids() Get array of all valid enum IDs
|
|
||||||
*
|
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class File_Attachment_Model extends Rsx_Site_Model_Abstract
|
class File_Attachment_Model extends Rsx_Site_Model_Abstract
|
||||||
{
|
{
|
||||||
/**
|
/** __AUTO_GENERATED: */
|
||||||
* _AUTO_GENERATED_ Enum constants
|
|
||||||
*/
|
|
||||||
const FILE_TYPE_IMAGE = 1;
|
const FILE_TYPE_IMAGE = 1;
|
||||||
const FILE_TYPE_ANIMATED_IMAGE = 2;
|
const FILE_TYPE_ANIMATED_IMAGE = 2;
|
||||||
const FILE_TYPE_VIDEO = 3;
|
const FILE_TYPE_VIDEO = 3;
|
||||||
@@ -81,9 +73,6 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
|
|||||||
const FILE_TYPE_TEXT = 5;
|
const FILE_TYPE_TEXT = 5;
|
||||||
const FILE_TYPE_DOCUMENT = 6;
|
const FILE_TYPE_DOCUMENT = 6;
|
||||||
const FILE_TYPE_OTHER = 7;
|
const FILE_TYPE_OTHER = 7;
|
||||||
|
|
||||||
/** __AUTO_GENERATED: */
|
|
||||||
|
|
||||||
/** __/AUTO_GENERATED */
|
/** __/AUTO_GENERATED */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,9 +111,18 @@ class Rsx_Date {
|
|||||||
// CURRENT DATE
|
// CURRENT DATE
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for today()
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static now() {
|
||||||
|
return this.today();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get today's date as "YYYY-MM-DD"
|
* Get today's date as "YYYY-MM-DD"
|
||||||
* Uses the user's timezone to determine what "today" is
|
* Uses user → site → default timezone from rsxapp
|
||||||
*
|
*
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@@ -236,4 +245,309 @@ class Rsx_Date {
|
|||||||
|
|
||||||
return Math.round((ms2 - ms1) / (1000 * 60 * 60 * 24));
|
return Math.round((ms2 - ms1) / (1000 * 60 * 60 * 24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format as relative date ("Today", "Yesterday", "3 days ago", "in 5 days")
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static relative(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = this.diff_days(this.today(), parsed);
|
||||||
|
|
||||||
|
if (days === 0) {
|
||||||
|
return 'Today';
|
||||||
|
} else if (days === 1) {
|
||||||
|
return 'Tomorrow';
|
||||||
|
} else if (days === -1) {
|
||||||
|
return 'Yesterday';
|
||||||
|
} else if (days > 1) {
|
||||||
|
return `in ${days} days`;
|
||||||
|
} else {
|
||||||
|
return `${Math.abs(days)} days ago`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// ARITHMETIC
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add days to a date
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @param {number} days Can be negative to subtract
|
||||||
|
* @returns {string|null} "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
static add_days(date, days) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
d.setDate(d.getDate() + days);
|
||||||
|
|
||||||
|
return d.getFullYear() + '-' +
|
||||||
|
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(d.getDate()).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// WEEK/MONTH BOUNDARIES
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Monday of the week containing the date
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string|null} "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
static start_of_week(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
const dow = d.getDay();
|
||||||
|
// Convert to Monday=0 based, then subtract to get Monday
|
||||||
|
const daysToSubtract = (dow === 0) ? 6 : dow - 1;
|
||||||
|
d.setDate(d.getDate() - daysToSubtract);
|
||||||
|
|
||||||
|
return d.getFullYear() + '-' +
|
||||||
|
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(d.getDate()).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Sunday of the week containing the date
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string|null} "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
static end_of_week(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
const dow = d.getDay();
|
||||||
|
// Days to add to get to Sunday
|
||||||
|
const daysToAdd = (dow === 0) ? 0 : 7 - dow;
|
||||||
|
d.setDate(d.getDate() + daysToAdd);
|
||||||
|
|
||||||
|
return d.getFullYear() + '-' +
|
||||||
|
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(d.getDate()).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first day of the month containing the date
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string|null} "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
static start_of_month(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month] = parsed.split('-').map(Number);
|
||||||
|
return year + '-' + String(month).padStart(2, '0') + '-01';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last day of the month containing the date
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string|null} "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
static end_of_month(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month] = parsed.split('-').map(Number);
|
||||||
|
// Day 0 of next month = last day of current month
|
||||||
|
const d = new Date(year, month, 0);
|
||||||
|
|
||||||
|
return d.getFullYear() + '-' +
|
||||||
|
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||||
|
String(d.getDate()).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if date falls on a weekend (Saturday or Sunday)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static is_weekend(date) {
|
||||||
|
const dow = this.dow(date);
|
||||||
|
if (dow === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return dow === 0 || dow === 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if date falls on a weekday (Monday-Friday)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
static is_weekday(date) {
|
||||||
|
const dow = this.dow(date);
|
||||||
|
if (dow === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return dow >= 1 && dow <= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// COMPONENT EXTRACTORS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of month (1-31)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static day(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseInt(parsed.split('-')[2], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of week (0=Sunday, 6=Saturday)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static dow(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
return d.getDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full day name ("Monday", "Tuesday", etc.)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static dow_human(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short day name ("Mon", "Tue", etc.)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static dow_short(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month (1-12)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static month(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseInt(parsed.split('-')[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full month name ("January", "February", etc.)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static month_human(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('en-US', { month: 'long' }).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short month name ("Jan", "Feb", etc.)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static month_human_short(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = parsed.split('-').map(Number);
|
||||||
|
const d = new Date(year, month - 1, day);
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('en-US', { month: 'short' }).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get year (e.g., 2025)
|
||||||
|
*
|
||||||
|
* @param {*} date
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static year(date) {
|
||||||
|
const parsed = this.parse(date);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseInt(parsed.split('-')[0], 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,4 +609,157 @@ class Rsx_Time {
|
|||||||
stop: () => clearInterval(interval)
|
stop: () => clearInterval(interval)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// COMPONENT EXTRACTORS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of month (1-31)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static day(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
return parseInt(this.format_in_timezone(time, { day: 'numeric' }), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of week (0=Sunday, 6=Saturday)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static dow(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
// Get the day name and map to number
|
||||||
|
const dayName = this.format_in_timezone(time, { weekday: 'short' });
|
||||||
|
const dayMap = { 'Sun': 0, 'Mon': 1, 'Tue': 2, 'Wed': 3, 'Thu': 4, 'Fri': 5, 'Sat': 6 };
|
||||||
|
return dayMap[dayName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full day name ("Monday", "Tuesday", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static dow_human(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
return this.format_in_timezone(time, { weekday: 'long' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short day name ("Mon", "Tue", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static dow_short(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
return this.format_in_timezone(time, { weekday: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month (1-12)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static month(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
// Use 2-digit to get padded month, then parse
|
||||||
|
const monthStr = this.format_in_timezone(time, { month: '2-digit' });
|
||||||
|
return parseInt(monthStr, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full month name ("January", "February", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static month_human(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
return this.format_in_timezone(time, { month: 'long' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short month name ("Jan", "Feb", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static month_human_short(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
return this.format_in_timezone(time, { month: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get year (e.g., 2025)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static year(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
return parseInt(this.format_in_timezone(time, { year: 'numeric' }), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hour (0-23)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static hour(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
// Use hour12: false and 2-digit for consistent 24-hour format
|
||||||
|
const hourStr = this.format_in_timezone(time, { hour: '2-digit', hour12: false });
|
||||||
|
// "24" is returned for midnight in some locales, treat as 0
|
||||||
|
const hour = parseInt(hourStr, 10);
|
||||||
|
return hour === 24 ? 0 : hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get minute (0-59)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
static minute(time) {
|
||||||
|
const date = this.parse(time);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
return parseInt(this.format_in_timezone(time, { minute: '2-digit' }), 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,46 +23,41 @@ use App\RSpade\Core\Models\User_Profile_Model;
|
|||||||
* See: php artisan rsx:man acls
|
* See: php artisan rsx:man acls
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* _AUTO_GENERATED_ Database type hints - do not edit manually
|
* _AUTO_GENERATED_
|
||||||
* Generated on: 2025-12-26 02:43:29
|
* @property integer $id
|
||||||
* Table: users
|
* @property integer $login_user_id
|
||||||
*
|
* @property integer $site_id
|
||||||
* @property int $id
|
* @property string $first_name
|
||||||
* @property int $login_user_id
|
* @property string $last_name
|
||||||
* @property int $site_id
|
* @property string $phone
|
||||||
* @property mixed $first_name
|
* @property integer $role_id
|
||||||
* @property mixed $last_name
|
* @property boolean $is_enabled
|
||||||
* @property mixed $phone
|
* @property integer $user_role_id
|
||||||
* @property int $role_id
|
* @property string $email
|
||||||
* @property bool $is_enabled
|
* @property \Carbon\Carbon $deleted_at
|
||||||
* @property int $user_role_id
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property mixed $email
|
* @property \Carbon\Carbon $updated_at
|
||||||
* @property string $deleted_at
|
* @property integer $created_by
|
||||||
* @property string $created_at
|
* @property integer $updated_by
|
||||||
* @property string $updated_at
|
* @property integer $deleted_by
|
||||||
* @property int $created_by
|
* @property string $invite_code
|
||||||
* @property int $updated_by
|
* @property \Carbon\Carbon $invite_accepted_at
|
||||||
* @property int $deleted_by
|
* @property \Carbon\Carbon $invite_expires_at
|
||||||
* @property mixed $invite_code
|
* @method static mixed role_id_enum()
|
||||||
* @property string $invite_accepted_at
|
* @method static mixed role_id_enum_select()
|
||||||
* @property string $invite_expires_at
|
* @method static mixed role_id_enum_ids()
|
||||||
*
|
* @property-read mixed $role_id_constant
|
||||||
* @property-read string $role_id__label
|
* @property-read mixed $role_id_label
|
||||||
* @property-read string $role_id__constant
|
* @property-read mixed $role_id_permissions
|
||||||
*
|
* @property-read mixed $role_id_can_admin_roles
|
||||||
* @method static array role_id__enum() Get all enum definitions with full metadata
|
* @property-read mixed $role_id_selectable
|
||||||
* @method static array role_id__enum_select() Get selectable items for dropdowns
|
|
||||||
* @method static array role_id__enum_labels() Get simple id => label map
|
|
||||||
* @method static array role_id__enum_ids() Get array of all valid enum IDs
|
|
||||||
*
|
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class User_Model extends Rsx_Site_Model_Abstract
|
class User_Model extends Rsx_Site_Model_Abstract
|
||||||
{
|
{
|
||||||
/**
|
/** __AUTO_GENERATED: */
|
||||||
* _AUTO_GENERATED_ Enum constants
|
|
||||||
*/
|
|
||||||
const ROLE_DEVELOPER = 100;
|
const ROLE_DEVELOPER = 100;
|
||||||
const ROLE_ROOT_ADMIN = 200;
|
const ROLE_ROOT_ADMIN = 200;
|
||||||
const ROLE_SITE_OWNER = 300;
|
const ROLE_SITE_OWNER = 300;
|
||||||
@@ -71,9 +66,6 @@ class User_Model extends Rsx_Site_Model_Abstract
|
|||||||
const ROLE_USER = 600;
|
const ROLE_USER = 600;
|
||||||
const ROLE_VIEWER = 700;
|
const ROLE_VIEWER = 700;
|
||||||
const ROLE_DISABLED = 800;
|
const ROLE_DISABLED = 800;
|
||||||
|
|
||||||
/** __AUTO_GENERATED: */
|
|
||||||
|
|
||||||
/** __/AUTO_GENERATED */
|
/** __/AUTO_GENERATED */
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -11,44 +11,33 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
|
|||||||
* and two-factor authentication via email or SMS.
|
* and two-factor authentication via email or SMS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* _AUTO_GENERATED_ Database type hints - do not edit manually
|
* _AUTO_GENERATED_
|
||||||
* Generated on: 2025-12-26 02:43:29
|
* @property integer $id
|
||||||
* Table: user_verifications
|
* @property string $email
|
||||||
*
|
* @property string $verification_code
|
||||||
* @property int $id
|
* @property integer $verification_type_id
|
||||||
* @property mixed $email
|
* @property \Carbon\Carbon $verified_at
|
||||||
* @property mixed $verification_code
|
* @property \Carbon\Carbon $expires_at
|
||||||
* @property int $verification_type_id
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property string $verified_at
|
* @property \Carbon\Carbon $updated_at
|
||||||
* @property string $expires_at
|
* @property integer $created_by
|
||||||
* @property string $created_at
|
* @property integer $updated_by
|
||||||
* @property string $updated_at
|
* @method static mixed verification_type_id_enum()
|
||||||
* @property int $created_by
|
* @method static mixed verification_type_id_enum_select()
|
||||||
* @property int $updated_by
|
* @method static mixed verification_type_id_enum_ids()
|
||||||
*
|
* @property-read mixed $verification_type_id_constant
|
||||||
* @property-read string $verification_type_id__label
|
* @property-read mixed $verification_type_id_label
|
||||||
* @property-read string $verification_type_id__constant
|
|
||||||
*
|
|
||||||
* @method static array verification_type_id__enum() Get all enum definitions with full metadata
|
|
||||||
* @method static array verification_type_id__enum_select() Get selectable items for dropdowns
|
|
||||||
* @method static array verification_type_id__enum_labels() Get simple id => label map
|
|
||||||
* @method static array verification_type_id__enum_ids() Get array of all valid enum IDs
|
|
||||||
*
|
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class User_Verification_Model extends Rsx_Model_Abstract
|
class User_Verification_Model extends Rsx_Model_Abstract
|
||||||
{
|
{
|
||||||
/**
|
/** __AUTO_GENERATED: */
|
||||||
* _AUTO_GENERATED_ Enum constants
|
|
||||||
*/
|
|
||||||
const VERIFICATION_TYPE_EMAIL = 1;
|
const VERIFICATION_TYPE_EMAIL = 1;
|
||||||
const VERIFICATION_TYPE_SMS = 2;
|
const VERIFICATION_TYPE_SMS = 2;
|
||||||
const VERIFICATION_TYPE_EMAIL_RECOVERY = 3;
|
const VERIFICATION_TYPE_EMAIL_RECOVERY = 3;
|
||||||
const VERIFICATION_TYPE_SMS_RECOVERY = 4;
|
const VERIFICATION_TYPE_SMS_RECOVERY = 4;
|
||||||
|
|
||||||
/** __AUTO_GENERATED: */
|
|
||||||
|
|
||||||
/** __/AUTO_GENERATED */
|
/** __/AUTO_GENERATED */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -113,16 +113,26 @@ class Rsx_Date
|
|||||||
// CURRENT DATE
|
// CURRENT DATE
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for today()
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function now(): string
|
||||||
|
{
|
||||||
|
return static::today();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get today's date as "YYYY-MM-DD"
|
* Get today's date as "YYYY-MM-DD"
|
||||||
* Uses the user's timezone to determine what "today" is
|
* Uses user → site → default timezone resolution
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function today(): string
|
public static function today(): string
|
||||||
{
|
{
|
||||||
$user_tz = Rsx_Time::get_user_timezone();
|
$tz = Rsx_Time::get_user_timezone();
|
||||||
return Carbon::now($user_tz)->format('Y-m-d');
|
return Carbon::now($tz)->format('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -230,6 +240,299 @@ class Rsx_Date
|
|||||||
return $carbon1->diffInDays($carbon2, false);
|
return $carbon1->diffInDays($carbon2, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format as relative date ("Today", "Yesterday", "3 days ago", "in 5 days")
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function relative($date): string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$days = static::diff_days(static::today(), $parsed);
|
||||||
|
|
||||||
|
if ($days === 0) {
|
||||||
|
return 'Today';
|
||||||
|
} elseif ($days === 1) {
|
||||||
|
return 'Tomorrow';
|
||||||
|
} elseif ($days === -1) {
|
||||||
|
return 'Yesterday';
|
||||||
|
} elseif ($days > 1) {
|
||||||
|
return "in {$days} days";
|
||||||
|
} else {
|
||||||
|
return abs($days) . ' days ago';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// ARITHMETIC
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add days to a date
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @param int $days Can be negative to subtract
|
||||||
|
* @return string|null "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
public static function add_days($date, int $days): ?string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed)->addDays($days);
|
||||||
|
return $carbon->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// WEEK/MONTH BOUNDARIES
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Monday of the week containing the date
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string|null "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
public static function start_of_week($date): ?string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed)->startOfWeek(Carbon::MONDAY);
|
||||||
|
return $carbon->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Sunday of the week containing the date
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string|null "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
public static function end_of_week($date): ?string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed)->endOfWeek(Carbon::SUNDAY);
|
||||||
|
return $carbon->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first day of the month containing the date
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string|null "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
public static function start_of_month($date): ?string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed)->startOfMonth();
|
||||||
|
return $carbon->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last day of the month containing the date
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string|null "YYYY-MM-DD" or null if invalid input
|
||||||
|
*/
|
||||||
|
public static function end_of_month($date): ?string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed)->endOfMonth();
|
||||||
|
return $carbon->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if date falls on a weekend (Saturday or Sunday)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_weekend($date): bool
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->isWeekend();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if date falls on a weekday (Monday-Friday)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_weekday($date): bool
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->isWeekday();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// COMPONENT EXTRACTORS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of month (1-31)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function day($date): ?int
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) explode('-', $parsed)[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of week (0=Sunday, 6=Saturday)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function dow($date): ?int
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->dayOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full day name ("Monday", "Tuesday", etc.)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function dow_human($date): string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->format('l');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short day name ("Mon", "Tue", etc.)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function dow_short($date): string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->format('D');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month (1-12)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function month($date): ?int
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) explode('-', $parsed)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full month name ("January", "February", etc.)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function month_human($date): string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->format('F');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short month name ("Jan", "Feb", etc.)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function month_human_short($date): string
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $parsed);
|
||||||
|
return $carbon->format('M');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get year (e.g., 2025)
|
||||||
|
*
|
||||||
|
* @param mixed $date
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function year($date): ?int
|
||||||
|
{
|
||||||
|
$parsed = static::parse($date);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) explode('-', $parsed)[0];
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// DATABASE
|
// DATABASE
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -31,6 +31,40 @@ use App\RSpade\Core\Session\Session;
|
|||||||
*/
|
*/
|
||||||
class Rsx_Time
|
class Rsx_Time
|
||||||
{
|
{
|
||||||
|
// =========================================================================
|
||||||
|
// TIMEZONE CACHING
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached user timezone
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
private static ?string $_cached_user_timezone = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User ID when timezone was cached (for invalidation)
|
||||||
|
* @var int|null
|
||||||
|
*/
|
||||||
|
private static ?int $_cached_user_id = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site ID when timezone was cached (for invalidation)
|
||||||
|
* @var int|null
|
||||||
|
*/
|
||||||
|
private static ?int $_cached_site_id = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached timezone (called when session user/site changes)
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function clear_timezone_cache(): void
|
||||||
|
{
|
||||||
|
static::$_cached_user_timezone = null;
|
||||||
|
static::$_cached_user_id = null;
|
||||||
|
static::$_cached_site_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// CURRENT TIME
|
// CURRENT TIME
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -210,24 +244,65 @@ class Rsx_Time
|
|||||||
/**
|
/**
|
||||||
* Get the current user's timezone
|
* Get the current user's timezone
|
||||||
* Resolution: user setting → site default → config default → America/Chicago
|
* Resolution: user setting → site default → config default → America/Chicago
|
||||||
|
* Result is cached and invalidated when session user/site changes
|
||||||
*
|
*
|
||||||
* @return string IANA timezone identifier
|
* @return string IANA timezone identifier
|
||||||
*/
|
*/
|
||||||
public static function get_user_timezone(): string
|
public static function get_user_timezone(): string
|
||||||
{
|
{
|
||||||
|
$current_user_id = Session::get_login_user_id();
|
||||||
|
$current_site_id = Session::get_site_id();
|
||||||
|
|
||||||
|
// Check if cache is valid
|
||||||
|
if (static::$_cached_user_timezone !== null
|
||||||
|
&& static::$_cached_user_id === $current_user_id
|
||||||
|
&& static::$_cached_site_id === $current_site_id) {
|
||||||
|
return static::$_cached_user_timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss - recalculate
|
||||||
|
$timezone = null;
|
||||||
|
|
||||||
// Check logged-in user's preference
|
// Check logged-in user's preference
|
||||||
$login_user = Session::get_login_user();
|
$login_user = Session::get_login_user();
|
||||||
if ($login_user && !empty($login_user->timezone)) {
|
if ($login_user && !empty($login_user->timezone)) {
|
||||||
return $login_user->timezone;
|
$timezone = $login_user->timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check site default (future enhancement)
|
// Check site default
|
||||||
// $site = Session::get_site();
|
if ($timezone === null) {
|
||||||
// if ($site && !empty($site->timezone)) {
|
$site = Session::get_site();
|
||||||
// return $site->timezone;
|
if ($site && !empty($site->timezone)) {
|
||||||
// }
|
$timezone = $site->timezone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Config default
|
// Config default
|
||||||
|
if ($timezone === null) {
|
||||||
|
$timezone = config('rsx.datetime.default_timezone', 'America/Chicago');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
static::$_cached_user_timezone = $timezone;
|
||||||
|
static::$_cached_user_id = $current_user_id;
|
||||||
|
static::$_cached_site_id = $current_site_id;
|
||||||
|
|
||||||
|
return $timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current site's timezone (ignoring user preference)
|
||||||
|
* Resolution: site default → config default → America/Chicago
|
||||||
|
*
|
||||||
|
* @return string IANA timezone identifier
|
||||||
|
*/
|
||||||
|
public static function get_site_timezone(): string
|
||||||
|
{
|
||||||
|
$site = Session::get_site();
|
||||||
|
if ($site && !empty($site->timezone)) {
|
||||||
|
return $site->timezone;
|
||||||
|
}
|
||||||
|
|
||||||
return config('rsx.datetime.default_timezone', 'America/Chicago');
|
return config('rsx.datetime.default_timezone', 'America/Chicago');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +383,28 @@ class Rsx_Time
|
|||||||
return $end_carbon->diffInSeconds($start_carbon, false);
|
return $end_carbon->diffInSeconds($start_carbon, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seconds until a future time (negative if past)
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function seconds_until($time): int
|
||||||
|
{
|
||||||
|
return static::diff_seconds(static::now(), $time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seconds since a past time (negative if future)
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function seconds_since($time): int
|
||||||
|
{
|
||||||
|
return static::diff_seconds($time, static::now());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format duration as human-readable string
|
* Format duration as human-readable string
|
||||||
*
|
*
|
||||||
@@ -517,6 +614,170 @@ class Rsx_Time
|
|||||||
return static::format($time, 'M j, Y g:i A T', $timezone);
|
return static::format($time, 'M j, Y g:i A T', $timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// COMPONENT EXTRACTORS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of month (1-31)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function day($time): ?int
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) static::to_user_timezone($carbon)->format('j');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of week (0=Sunday, 6=Saturday)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function dow($time): ?int
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return static::to_user_timezone($carbon)->dayOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full day name ("Monday", "Tuesday", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function dow_human($time): string
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return static::to_user_timezone($carbon)->format('l');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short day name ("Mon", "Tue", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function dow_short($time): string
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return static::to_user_timezone($carbon)->format('D');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month (1-12)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function month($time): ?int
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) static::to_user_timezone($carbon)->format('n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full month name ("January", "February", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function month_human($time): string
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return static::to_user_timezone($carbon)->format('F');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short month name ("Jan", "Feb", etc.)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function month_human_short($time): string
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return static::to_user_timezone($carbon)->format('M');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get year (e.g., 2025)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function year($time): ?int
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) static::to_user_timezone($carbon)->format('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hour (0-23)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function hour($time): ?int
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) static::to_user_timezone($carbon)->format('G');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get minute (0-59)
|
||||||
|
* Uses user's timezone
|
||||||
|
*
|
||||||
|
* @param mixed $time
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public static function minute($time): ?int
|
||||||
|
{
|
||||||
|
$carbon = static::parse($time);
|
||||||
|
if (!$carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) static::to_user_timezone($carbon)->format('i');
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// DATABASE HELPERS
|
// DATABASE HELPERS
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -5,41 +5,29 @@ namespace App\RSpade\Lib\Flash;
|
|||||||
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
|
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* _AUTO_GENERATED_ Database type hints - do not edit manually
|
* _AUTO_GENERATED_
|
||||||
* Generated on: 2025-12-26 02:43:30
|
* @property integer $id
|
||||||
* Table: _flash_alerts
|
* @property integer $session_id
|
||||||
*
|
* @property integer $type_id
|
||||||
* @property int $id
|
|
||||||
* @property int $session_id
|
|
||||||
* @property int $type_id
|
|
||||||
* @property string $message
|
* @property string $message
|
||||||
* @property string $created_at
|
* @property \Carbon\Carbon $created_at
|
||||||
* @property int $created_by
|
* @property integer $created_by
|
||||||
* @property int $updated_by
|
* @property integer $updated_by
|
||||||
* @property string $updated_at
|
* @property \Carbon\Carbon $updated_at
|
||||||
*
|
* @method static mixed type_id_enum()
|
||||||
* @property-read string $type_id__label
|
* @method static mixed type_id_enum_select()
|
||||||
* @property-read string $type_id__constant
|
* @method static mixed type_id_enum_ids()
|
||||||
*
|
* @property-read mixed $type_id_constant
|
||||||
* @method static array type_id__enum() Get all enum definitions with full metadata
|
* @property-read mixed $type_id_label
|
||||||
* @method static array type_id__enum_select() Get selectable items for dropdowns
|
|
||||||
* @method static array type_id__enum_labels() Get simple id => label map
|
|
||||||
* @method static array type_id__enum_ids() Get array of all valid enum IDs
|
|
||||||
*
|
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
*/
|
*/
|
||||||
class Flash_Alert_Model extends Rsx_Model_Abstract
|
class Flash_Alert_Model extends Rsx_Model_Abstract
|
||||||
{
|
{
|
||||||
/**
|
/** __AUTO_GENERATED: */
|
||||||
* _AUTO_GENERATED_ Enum constants
|
|
||||||
*/
|
|
||||||
const TYPE_SUCCESS = 1;
|
const TYPE_SUCCESS = 1;
|
||||||
const TYPE_ERROR = 2;
|
const TYPE_ERROR = 2;
|
||||||
const TYPE_INFO = 3;
|
const TYPE_INFO = 3;
|
||||||
const TYPE_WARNING = 4;
|
const TYPE_WARNING = 4;
|
||||||
|
|
||||||
/** __AUTO_GENERATED: */
|
|
||||||
|
|
||||||
/** __/AUTO_GENERATED */
|
/** __/AUTO_GENERATED */
|
||||||
|
|
||||||
// Enum constants (auto-generated by rsx:migrate:document_models)
|
// Enum constants (auto-generated by rsx:migrate:document_models)
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ SYNOPSIS
|
|||||||
|
|
||||||
$today = Rsx_Date::today();
|
$today = Rsx_Date::today();
|
||||||
$formatted = Rsx_Date::format($date);
|
$formatted = Rsx_Date::format($date);
|
||||||
|
$relative = Rsx_Date::relative($date);
|
||||||
$is_past = Rsx_Date::is_past($date);
|
$is_past = Rsx_Date::is_past($date);
|
||||||
|
$next_week = Rsx_Date::add_days($date, 7);
|
||||||
|
$monday = Rsx_Date::start_of_week($date);
|
||||||
|
$month_name = Rsx_Date::month_human($date);
|
||||||
|
|
||||||
JavaScript Datetime:
|
JavaScript Datetime:
|
||||||
const now = Rsx_Time.now();
|
const now = Rsx_Time.now();
|
||||||
@@ -29,7 +33,10 @@ SYNOPSIS
|
|||||||
JavaScript Date:
|
JavaScript Date:
|
||||||
const today = Rsx_Date.today();
|
const today = Rsx_Date.today();
|
||||||
const formatted = Rsx_Date.format(date);
|
const formatted = Rsx_Date.format(date);
|
||||||
|
const relative = Rsx_Date.relative(date);
|
||||||
const is_past = Rsx_Date.is_past(date);
|
const is_past = Rsx_Date.is_past(date);
|
||||||
|
const next_week = Rsx_Date.add_days(date, 7);
|
||||||
|
const monday = Rsx_Date.start_of_week(date);
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
RSpade provides two separate classes for handling temporal values:
|
RSpade provides two separate classes for handling temporal values:
|
||||||
@@ -111,6 +118,9 @@ RSX_DATE CLASS
|
|||||||
PHP: $today = Rsx_Date::today();
|
PHP: $today = Rsx_Date::today();
|
||||||
JS: const today = Rsx_Date.today();
|
JS: const today = Rsx_Date.today();
|
||||||
|
|
||||||
|
now()
|
||||||
|
Alias for today(). Provided for consistency with Rsx_Time::now().
|
||||||
|
|
||||||
Formatting
|
Formatting
|
||||||
format($date)
|
format($date)
|
||||||
Display format: "Dec 24, 2025"
|
Display format: "Dec 24, 2025"
|
||||||
@@ -118,12 +128,42 @@ RSX_DATE CLASS
|
|||||||
format_iso($date)
|
format_iso($date)
|
||||||
Ensures "YYYY-MM-DD" format.
|
Ensures "YYYY-MM-DD" format.
|
||||||
|
|
||||||
|
relative($date)
|
||||||
|
Human-readable relative date.
|
||||||
|
"Today", "Yesterday", "Tomorrow", "3 days ago", "in 5 days"
|
||||||
|
|
||||||
Comparison
|
Comparison
|
||||||
is_today($date) True if date is today
|
is_today($date) True if date is today
|
||||||
is_past($date) True if date is before today
|
is_past($date) True if date is before today
|
||||||
is_future($date) True if date is after today
|
is_future($date) True if date is after today
|
||||||
diff_days($d1, $d2) Days between dates (positive if d2 > d1)
|
diff_days($d1, $d2) Days between dates (positive if d2 > d1)
|
||||||
|
|
||||||
|
Arithmetic
|
||||||
|
add_days($date, $days)
|
||||||
|
Add (or subtract with negative) days from date.
|
||||||
|
Returns "YYYY-MM-DD" or null if invalid input.
|
||||||
|
|
||||||
|
Week/Month Boundaries
|
||||||
|
start_of_week($date) Monday of the week containing date
|
||||||
|
end_of_week($date) Sunday of the week containing date
|
||||||
|
start_of_month($date) First day of month (YYYY-MM-01)
|
||||||
|
end_of_month($date) Last day of month
|
||||||
|
|
||||||
|
is_weekend($date) True if Saturday or Sunday
|
||||||
|
is_weekday($date) True if Monday-Friday
|
||||||
|
|
||||||
|
Component Extractors
|
||||||
|
All extractors return null for invalid input.
|
||||||
|
|
||||||
|
day($date) Day of month (1-31)
|
||||||
|
dow($date) Day of week integer (0=Sunday, 6=Saturday)
|
||||||
|
dow_human($date) Full day name ("Monday", "Tuesday", etc.)
|
||||||
|
dow_short($date) Short day name ("Mon", "Tue", etc.)
|
||||||
|
month($date) Month number (1-12)
|
||||||
|
month_human($date) Full month name ("January", "February")
|
||||||
|
month_human_short($date) Short month name ("Jan", "Feb")
|
||||||
|
year($date) Four-digit year (e.g., 2025)
|
||||||
|
|
||||||
Database
|
Database
|
||||||
to_database($date)
|
to_database($date)
|
||||||
Returns "YYYY-MM-DD" for database storage (same as ISO format).
|
Returns "YYYY-MM-DD" for database storage (same as ISO format).
|
||||||
@@ -181,11 +221,11 @@ RSX_TIME CLASS
|
|||||||
diff_seconds($start, $end)
|
diff_seconds($start, $end)
|
||||||
Seconds between two datetimes.
|
Seconds between two datetimes.
|
||||||
|
|
||||||
seconds_until($time) (JS only)
|
seconds_until($time)
|
||||||
Seconds until future time.
|
Seconds until future time (negative if in past).
|
||||||
|
|
||||||
seconds_since($time) (JS only)
|
seconds_since($time)
|
||||||
Seconds since past time.
|
Seconds since past time (negative if in future).
|
||||||
|
|
||||||
duration_to_human($seconds, $short)
|
duration_to_human($seconds, $short)
|
||||||
Long: "2 hours and 30 minutes"
|
Long: "2 hours and 30 minutes"
|
||||||
@@ -206,6 +246,22 @@ RSX_TIME CLASS
|
|||||||
is_future($time) True if datetime is in the future
|
is_future($time) True if datetime is in the future
|
||||||
is_today($time) True if datetime is today (in user's timezone)
|
is_today($time) True if datetime is today (in user's timezone)
|
||||||
|
|
||||||
|
Component Extractors
|
||||||
|
Extract date/time components from a datetime. All require timezone
|
||||||
|
parameter ($tz) or use user's timezone if omitted.
|
||||||
|
Return null for invalid input.
|
||||||
|
|
||||||
|
day($time, $tz) Day of month (1-31)
|
||||||
|
dow($time, $tz) Day of week integer (0=Sunday, 6=Saturday)
|
||||||
|
dow_human($time, $tz) Full day name ("Monday", "Tuesday", etc.)
|
||||||
|
dow_short($time, $tz) Short day name ("Mon", "Tue", etc.)
|
||||||
|
month($time, $tz) Month number (1-12)
|
||||||
|
month_human($time, $tz) Full month name ("January", "February")
|
||||||
|
month_human_short($time, $tz) Short month name ("Jan", "Feb")
|
||||||
|
year($time, $tz) Four-digit year (e.g., 2025)
|
||||||
|
hour($time, $tz) Hour (0-23)
|
||||||
|
minute($time, $tz) Minute (0-59)
|
||||||
|
|
||||||
Live Updates (JavaScript only)
|
Live Updates (JavaScript only)
|
||||||
countdown($element, target_time, options)
|
countdown($element, target_time, options)
|
||||||
Live countdown to future time. Updates every second.
|
Live countdown to future time. Updates every second.
|
||||||
@@ -224,8 +280,12 @@ RSX_TIME CLASS
|
|||||||
TIMEZONE INITIALIZATION
|
TIMEZONE INITIALIZATION
|
||||||
User timezone is resolved in order:
|
User timezone is resolved in order:
|
||||||
1. login_users.timezone (user's preference)
|
1. login_users.timezone (user's preference)
|
||||||
2. config('rsx.datetime.default_timezone')
|
2. sites.timezone (site's default timezone)
|
||||||
3. 'America/Chicago' (hardcoded fallback)
|
3. config('rsx.datetime.default_timezone')
|
||||||
|
4. 'America/Chicago' (hardcoded fallback)
|
||||||
|
|
||||||
|
Timezone is cached per-request and auto-invalidates when user or site
|
||||||
|
changes (e.g., after login/logout or site switch).
|
||||||
|
|
||||||
Page Load
|
Page Load
|
||||||
On page load, window.rsxapp includes:
|
On page load, window.rsxapp includes:
|
||||||
@@ -397,6 +457,7 @@ CONFIGURATION
|
|||||||
],
|
],
|
||||||
|
|
||||||
User timezone stored in login_users.timezone column.
|
User timezone stored in login_users.timezone column.
|
||||||
|
Site timezone stored in sites.timezone column (default: 'America/Chicago').
|
||||||
|
|
||||||
MODEL CASTING
|
MODEL CASTING
|
||||||
Model date/datetime columns are automatically cast to strings via custom
|
Model date/datetime columns are automatically cast to strings via custom
|
||||||
@@ -434,4 +495,4 @@ SEE ALSO
|
|||||||
AUTHOR
|
AUTHOR
|
||||||
RSpade Framework
|
RSpade Framework
|
||||||
|
|
||||||
RSpade December 2025 TIME(7)
|
RSpade January 2026 TIME(7)
|
||||||
|
|||||||
@@ -376,6 +376,11 @@
|
|||||||
"created_at": "2025-12-27T22:53:05+00:00",
|
"created_at": "2025-12-27T22:53:05+00:00",
|
||||||
"created_by": "root",
|
"created_by": "root",
|
||||||
"command": "php artisan make:migration:safe convert_tasks_polymorphic_to_type_refs"
|
"command": "php artisan make:migration:safe convert_tasks_polymorphic_to_type_refs"
|
||||||
|
},
|
||||||
|
"2026_01_12_073624_add_timezone_to_sites_table.php": {
|
||||||
|
"created_at": "2026-01-12T07:36:24+00:00",
|
||||||
|
"created_by": "root",
|
||||||
|
"command": "php artisan make:migration:safe add_timezone_to_sites_table"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
23
database/migrations/2026_01_12_073624_add_timezone_to_sites_table.php
Executable file
23
database/migrations/2026_01_12_073624_add_timezone_to_sites_table.php
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* IMPORTANT: Use raw MySQL queries for clarity and auditability
|
||||||
|
* ✅ DB::statement("ALTER TABLE sites ADD COLUMN new_field VARCHAR(255)")
|
||||||
|
* ❌ Schema::table() with Blueprint
|
||||||
|
*
|
||||||
|
* Migrations must be self-contained - no Model/Service references
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
DB::statement("ALTER TABLE sites ADD COLUMN timezone VARCHAR(50) NOT NULL DEFAULT 'America/Chicago' AFTER name");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -964,7 +964,7 @@ Sessions persist 365 days. Never implement "Remember Me".
|
|||||||
|
|
||||||
Pattern recognition:
|
Pattern recognition:
|
||||||
- `Rsx_Time::now()`, `Rsx_Time::format()`, `Rsx_Time::relative()`
|
- `Rsx_Time::now()`, `Rsx_Time::format()`, `Rsx_Time::relative()`
|
||||||
- `Rsx_Date::today()`, `Rsx_Date::format()`, `Rsx_Date::is_past()`
|
- `Rsx_Date::today()`, `Rsx_Date::format()`, `Rsx_Date::relative()` - uses user → site → default timezone
|
||||||
- `Rsx_Time::to_database()` for UTC storage
|
- `Rsx_Time::to_database()` for UTC storage
|
||||||
- Functions throw if wrong type passed
|
- Functions throw if wrong type passed
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user