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. * 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 */
/** /**

View File

@@ -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);
}
} }

View File

@@ -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);
}
} }

View File

@@ -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 */
// ========================================================================= // =========================================================================

View File

@@ -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 */
/** /**

View File

@@ -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
// ========================================================================= // =========================================================================

View File

@@ -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
// ========================================================================= // =========================================================================

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
} }
} }
} }

View 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");
}
};

View File

@@ -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