Framework updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-12 17:25:07 +00:00
parent ee709ae86d
commit f70ca09f78
12 changed files with 1234 additions and 156 deletions

View File

@@ -30,50 +30,42 @@ use App\RSpade\Core\Files\File_Storage_Model;
* provides the basic structure for categorizing uploaded files.
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Table: _file_attachments
*
* @property int $id
* @property mixed $key
* @property int $file_storage_id
* @property mixed $file_name
* @property mixed $file_extension
* @property int $file_type_id
* @property int $width
* @property int $height
* @property int $duration
* @property bool $is_animated
* @property int $frame_count
* @property mixed $fileable_type
* @property int $fileable_id
* @property mixed $fileable_category
* @property mixed $fileable_type_meta
* @property int $fileable_order
* _AUTO_GENERATED_
* @property integer $id
* @property string $key
* @property integer $file_storage_id
* @property string $file_name
* @property string $file_extension
* @property integer $file_type_id
* @property integer $width
* @property integer $height
* @property integer $duration
* @property boolean $is_animated
* @property integer $frame_count
* @property integer $fileable_type
* @property integer $fileable_id
* @property string $fileable_category
* @property string $fileable_type_meta
* @property integer $fileable_order
* @property string $fileable_meta
* @property int $site_id
* @property mixed $session_id
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
*
* @property-read string $file_type_id__label
* @property-read string $file_type_id__constant
*
* @method static array file_type_id__enum() Get all enum definitions with full metadata
* @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
*
* @property integer $site_id
* @property string $session_id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $created_by
* @property integer $updated_by
* @method static mixed file_type_id_enum()
* @method static mixed file_type_id_enum_select()
* @method static mixed file_type_id_enum_ids()
* @property-read mixed $file_type_id_constant
* @property-read mixed $file_type_id_label
* @mixin \Eloquent
*/
class File_Attachment_Model extends Rsx_Site_Model_Abstract
{
/**
* _AUTO_GENERATED_ Enum constants
*/
{
/** __AUTO_GENERATED: */
const FILE_TYPE_IMAGE = 1;
const FILE_TYPE_ANIMATED_IMAGE = 2;
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_DOCUMENT = 6;
const FILE_TYPE_OTHER = 7;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
/**

View File

@@ -111,9 +111,18 @@ class Rsx_Date {
// CURRENT DATE
// =========================================================================
/**
* Alias for today()
*
* @returns {string}
*/
static now() {
return this.today();
}
/**
* 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}
*/
@@ -236,4 +245,309 @@ class Rsx_Date {
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);
}
}

View File

@@ -609,4 +609,157 @@ class Rsx_Time {
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);
}
}

View File

@@ -23,46 +23,41 @@ use App\RSpade\Core\Models\User_Profile_Model;
* See: php artisan rsx:man acls
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Table: users
*
* @property int $id
* @property int $login_user_id
* @property int $site_id
* @property mixed $first_name
* @property mixed $last_name
* @property mixed $phone
* @property int $role_id
* @property bool $is_enabled
* @property int $user_role_id
* @property mixed $email
* @property string $deleted_at
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
* @property int $deleted_by
* @property mixed $invite_code
* @property string $invite_accepted_at
* @property string $invite_expires_at
*
* @property-read string $role_id__label
* @property-read string $role_id__constant
*
* @method static array role_id__enum() Get all enum definitions with full metadata
* @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
*
* _AUTO_GENERATED_
* @property integer $id
* @property integer $login_user_id
* @property integer $site_id
* @property string $first_name
* @property string $last_name
* @property string $phone
* @property integer $role_id
* @property boolean $is_enabled
* @property integer $user_role_id
* @property string $email
* @property \Carbon\Carbon $deleted_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $created_by
* @property integer $updated_by
* @property integer $deleted_by
* @property string $invite_code
* @property \Carbon\Carbon $invite_accepted_at
* @property \Carbon\Carbon $invite_expires_at
* @method static mixed role_id_enum()
* @method static mixed role_id_enum_select()
* @method static mixed role_id_enum_ids()
* @property-read mixed $role_id_constant
* @property-read mixed $role_id_label
* @property-read mixed $role_id_permissions
* @property-read mixed $role_id_can_admin_roles
* @property-read mixed $role_id_selectable
* @mixin \Eloquent
*/
class User_Model extends Rsx_Site_Model_Abstract
{
/**
* _AUTO_GENERATED_ Enum constants
*/
{
/** __AUTO_GENERATED: */
const ROLE_DEVELOPER = 100;
const ROLE_ROOT_ADMIN = 200;
const ROLE_SITE_OWNER = 300;
@@ -71,9 +66,6 @@ class User_Model extends Rsx_Site_Model_Abstract
const ROLE_USER = 600;
const ROLE_VIEWER = 700;
const ROLE_DISABLED = 800;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
// =========================================================================

View File

@@ -11,44 +11,33 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* and two-factor authentication via email or SMS.
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Table: user_verifications
*
* @property int $id
* @property mixed $email
* @property mixed $verification_code
* @property int $verification_type_id
* @property string $verified_at
* @property string $expires_at
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
*
* @property-read string $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
*
* _AUTO_GENERATED_
* @property integer $id
* @property string $email
* @property string $verification_code
* @property integer $verification_type_id
* @property \Carbon\Carbon $verified_at
* @property \Carbon\Carbon $expires_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $created_by
* @property integer $updated_by
* @method static mixed verification_type_id_enum()
* @method static mixed verification_type_id_enum_select()
* @method static mixed verification_type_id_enum_ids()
* @property-read mixed $verification_type_id_constant
* @property-read mixed $verification_type_id_label
* @mixin \Eloquent
*/
class User_Verification_Model extends Rsx_Model_Abstract
{
/**
* _AUTO_GENERATED_ Enum constants
*/
{
/** __AUTO_GENERATED: */
const VERIFICATION_TYPE_EMAIL = 1;
const VERIFICATION_TYPE_SMS = 2;
const VERIFICATION_TYPE_EMAIL_RECOVERY = 3;
const VERIFICATION_TYPE_SMS_RECOVERY = 4;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
/**

View File

@@ -113,16 +113,26 @@ class Rsx_Date
// CURRENT DATE
// =========================================================================
/**
* Alias for today()
*
* @return string
*/
public static function now(): string
{
return static::today();
}
/**
* 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
*/
public static function today(): string
{
$user_tz = Rsx_Time::get_user_timezone();
return Carbon::now($user_tz)->format('Y-m-d');
$tz = Rsx_Time::get_user_timezone();
return Carbon::now($tz)->format('Y-m-d');
}
// =========================================================================
@@ -230,6 +240,299 @@ class Rsx_Date
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
// =========================================================================

View File

@@ -31,6 +31,40 @@ use App\RSpade\Core\Session\Session;
*/
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
// =========================================================================
@@ -210,24 +244,65 @@ class Rsx_Time
/**
* Get the current user's timezone
* Resolution: user setting site default config default America/Chicago
* Result is cached and invalidated when session user/site changes
*
* @return string IANA timezone identifier
*/
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
$login_user = Session::get_login_user();
if ($login_user && !empty($login_user->timezone)) {
return $login_user->timezone;
$timezone = $login_user->timezone;
}
// Check site default (future enhancement)
// $site = Session::get_site();
// if ($site && !empty($site->timezone)) {
// return $site->timezone;
// }
// Check site default
if ($timezone === null) {
$site = Session::get_site();
if ($site && !empty($site->timezone)) {
$timezone = $site->timezone;
}
}
// 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');
}
@@ -308,6 +383,28 @@ class Rsx_Time
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
*
@@ -517,6 +614,170 @@ class Rsx_Time
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
// =========================================================================