From fd29bdd2a921de42a3ef24f0d31c3c764fee28af Mon Sep 17 00:00:00 2001 From: tech Date: Tue, 5 May 2026 19:03:08 -0500 Subject: [PATCH] Fresh start: PHP project baseline --- .gitignore | 11 + assets/ktc-app.js | 158 ++++++++++++++ includes/class-admin.php | 370 ++++++++++++++++++++++++++++++++ includes/class-logic.php | 377 +++++++++++++++++++++++++++++++++ krista-time-clock.php | 26 +++ ktc_sync_ldap_display_name.php | 32 +++ 6 files changed, 974 insertions(+) create mode 100644 .gitignore create mode 100755 assets/ktc-app.js create mode 100755 includes/class-admin.php create mode 100755 includes/class-logic.php create mode 100755 krista-time-clock.php create mode 100755 ktc_sync_ldap_display_name.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c6d9a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Ignore editor temp files +*~ +*.swp + +# Ignore manual backups +*.bak* +*.old + +# Ignore OS noise +.DS_Store +Thumbs.db diff --git a/assets/ktc-app.js b/assets/ktc-app.js new file mode 100755 index 0000000..c1cae6e --- /dev/null +++ b/assets/ktc-app.js @@ -0,0 +1,158 @@ + +function ktc_open_edit_modal(id, ts, isToday) { + document.getElementById("ktc_edit_id").value=id; + const origTimeDisplay = document.getElementById("ktc_modal_orig_time"); + if (origTimeDisplay) { origTimeDisplay.innerText = "Original: " + ts; } + document.getElementById("ktc_edit_modal").style.display="block"; +} + +// --- Guardrail 1: Punch Lockout --- +document.addEventListener('DOMContentLoaded', function() { + const btn = document.getElementById('ktc-punch-button'); + const timerDiv = document.getElementById('ktc-timer'); + const countdown = document.getElementById('ktc-countdown'); + const form = document.getElementById('ktc-punch-form'); + + function updateTimer() { + const expiry = localStorage.getItem('ktc_lockout_expiry'); + if (expiry) { + const now = Date.now(); + const timeLeft = Math.ceil((expiry - now) / 1000); + + if (timeLeft > 0) { + btn.disabled = true; + btn.style.opacity = '0.5'; + btn.style.cursor = 'not-allowed'; + timerDiv.style.display = 'block'; + countdown.innerText = timeLeft; + setTimeout(updateTimer, 1000); // Check again in 1s + } else { + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + timerDiv.style.display = 'none'; + localStorage.removeItem('ktc_lockout_expiry'); + } + } + } + + // Initial check on page load + updateTimer(); + + form?.addEventListener('submit', function(e) { + // Set expiry for 60 seconds from now + const expiryTime = Date.now() + 60000; + localStorage.setItem('ktc_lockout_expiry', expiryTime); + // The page will now refresh, and updateTimer() will catch it on reload + }); +}); + +// --- Guardrail 2: Modal Future-Time Validation --- +document.getElementById('ktc_proposed_time')?.addEventListener('input', function() { + const submitBtn = this.form.querySelector('button[type="submit"]'); + // If the input has a 'max' attribute and value exceeds it + if (this.max && this.value > this.max) { + this.style.borderColor = 'red'; + this.style.backgroundColor = '#fff0f0'; + submitBtn.disabled = true; + submitBtn.style.opacity = '0.5'; + } else { + this.style.borderColor = '#ccc'; + this.style.backgroundColor = '#fff'; + submitBtn.disabled = false; + submitBtn.style.opacity = '1'; + } +}); + +function ktc_open_edit_modal(id, ts, isToday) { + document.getElementById("ktc_edit_id").value = id; + document.getElementById("ktc_modal_orig_time").innerText = "Original: " + ts; + + const timeInput = document.getElementById("ktc_proposed_time"); + if (isToday) { + // Set max to current Chicago time (HH:MM) + const now = new Date().toLocaleTimeString('en-GB', { + timeZone: ktcSovereignTZ, + hour: '2-digit', + minute: '2-digit' + }); + timeInput.setAttribute('max', now); + } else { + timeInput.removeAttribute('max'); + } + + document.getElementById("ktc_edit_modal").style.display = "block"; +} + +// --- Guardrail 3: The Live Heartbeat --- +function ktc_start_heartbeat() { + const elapsedElement = document.getElementById('ktc-live-elapsed'); + if (!elapsedElement) return; + + // Pull the initial seconds we injected via PHP + let totalSeconds = parseInt(elapsedElement.getAttribute('data-seconds')); + + setInterval(() => { + totalSeconds++; + + // Math to format seconds into "Xh Ym Zs" or "Xh Ym" + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + // Update the display (Adding seconds for that "Live" feel) + elapsedElement.innerText = `${hours}h ${minutes}m ${seconds}s`; + }, 1000); +} + +/** + * Consolidated KTC Heartbeat: Handles Real-Time Clock & Title Teleportation + */ +function ktc_init_digital_clock() { + const clock = document.getElementById('ktc-digital-clock'); + if (!clock) return; + + // Use a single interval to handle both time updates and UI placement + setInterval(() => { + // 1. Generate the time string using our Sovereign Timezone + const tz = typeof ktcSovereignTZ !== 'undefined' ? ktcSovereignTZ : 'America/Chicago'; + const timeString = new Date().toLocaleTimeString('en-US', { + timeZone: tz, + hour12: true, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + // 2. Update the display + clock.innerText = timeString; + + // 3. One-time Teleport Check + // We only attempt to move it if it's not already inside the 'ktc-clock-target' + const pageTitle = document.querySelector('.entry-title, h1.page-title, h1'); + const isAlreadyTeleported = document.getElementById('ktc-clock-target'); + + if (pageTitle && !isAlreadyTeleported) { + const target = document.createElement('span'); + target.id = 'ktc-clock-target'; + target.style.marginLeft = '20px'; + target.style.fontSize = '0.6em'; + target.style.verticalAlign = 'middle'; + target.style.color = '#888'; // Subtle color for the clock in the title + + pageTitle.appendChild(target); + target.appendChild(clock); // Physically moves the element in the DOM + + // Cleanup the source container if it exists + const sourceContainer = document.getElementById('ktc-clock-teleport-source'); + if (sourceContainer) sourceContainer.remove(); + } + }, 1000); +} +// Initialize on DOM load +document.addEventListener('DOMContentLoaded', ktc_init_digital_clock); + +// Initialize on load +document.addEventListener('DOMContentLoaded', ktc_start_heartbeat); + + diff --git a/includes/class-admin.php b/includes/class-admin.php new file mode 100755 index 0000000..1242ea3 --- /dev/null +++ b/includes/class-admin.php @@ -0,0 +1,370 @@ + admin_url( 'admin-ajax.php' ), + 'lockoutTime' => 60 // Seconds for the button cooldown + )); + + $tz_string = get_option( 'ktc_timezone', 'America/Chicago' ); + wp_add_inline_script( 'ktc-app-js', "const ktcSovereignTZ = '{$tz_string}';", 'before' ); + + } + + public static function init() { + add_shortcode( 'krista_timeclock', array( __CLASS__, 'render_timeclock' ) ); + add_shortcode( 'ktc_portal_welcome', array( __CLASS__, 'render_welcome' ) ); + add_action( 'wp_footer', array( __CLASS__, 'inject_ui_scripts' ) ); + add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) ); + add_action( 'admin_menu', array( __CLASS__, 'register_settings_page' ) ); + + add_action( 'admin_init', function() { + if ( ! current_user_can('manage_options') && ! defined('DOING_AJAX') && $GLOBALS['pagenow'] !== 'profile.php' && current_user_can('ktc_user') ) { + wp_safe_redirect( home_url('/') ); exit; + } + }); + + add_action( 'admin_init', function() { + add_settings_section( 'ktc_main_section', 'General Configuration', null, 'ktc-settings' ); + add_settings_field( 'ktc_week_start_field', 'Week Starts On', function() { + $val = get_option('ktc_week_start', 'Monday'); + echo ""; + }, 'ktc-settings', 'ktc_main_section' ); + add_settings_field( 'ktc_timezone_field', 'Sovereign Timezone', array( __CLASS__, 'render_timezone_field' ), 'ktc-settings', 'ktc_main_section' ); + }); + + } + +/** + * Renders a unified dashboard header. + * Consolidates Admin 'Pulse' and User 'Stats' based on capability. + */ + public static function render_welcome() { + if ( ! is_user_logged_in() ) return 'Please log in to continue'; + + $user = wp_get_current_user(); + $is_admin = current_user_can('manage_options'); + + // Everyone gets Personal Stats + $stats = KTC_Logic::get_user_stats( $user->ID ); + $user_stat_data = [ + ['label' => 'This Week', 'val' => $stats['week'] . ' hrs', 'color' => '#2196F3'], + ['label' => 'This Month', 'val' => $stats['month'] . ' hrs', 'color' => '#4CAF50'], + ['label' => 'My Pending TORs', 'val' => $stats['pending_tor'], 'color' => '#FF9800'] + ]; + + ob_start(); ?> + + +
+
+
+

Welcome, display_name ); ?>

+

Personal Time Tracking Overview

+
+
+ +
+ +
+
+ +
+ +
+
+ + + 'Active Now', 'val' => $pulse['active_now'], 'color' => '#4CAF50'], + ['label' => 'Pending Edits', 'val' => $pulse['pending_edits'], 'color' => '#2196F3'], + ['label' => 'System TORs', 'val' => $pulse['total_pending_tor'], 'color' => '#FF9800'] + ]; + ?> +
+
+

System Pulse

+ ADMIN CONSOLE +
+ +
+ +
+
+ +
+ +
+ +
+ 🛠️ System Settings + ⚙️ Global Profile +
+
+ + + +
+ 🚪 Logout +
+ + + + + + $label
+ $val"; + } + + public static function render_timeclock() { + if ( ! is_user_logged_in() ) return '

Please log in.

'; + global $wpdb; + $user_id = get_current_user_id(); + $table_name = $wpdb->prefix . 'ktc_logs'; + + $status = $wpdb->get_var( $wpdb->prepare( "SELECT action_type FROM $table_name WHERE user_id = %d ORDER BY timestamp DESC LIMIT 1", $user_id ) ) ?: 'clock_out'; + $btn_color = ($status === 'clock_in') ? '#d9534f' : '#5cb85c'; + $next_action = ($status === 'clock_in') ? 'clock_out' : 'clock_in'; + + ob_start(); ?> +
+ +
Current Shift:
+ +
Status: Off the Clock
+ +
Weekly Total: hrs
+
+ +
+ + + + +
+ + get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d ORDER BY timestamp DESC LIMIT 10", $user_id ) ); + if ( $history ): ?> +

Recent Activity

+ + timestamp); + $orig_time = wp_date( 'D, M j, g:i a', $ts_local ); + $is_today = (wp_date('Y-m-d', $ts_local) === (new DateTime('now', new DateTimeZone(KTC_TIMEZONE)))->format('Y-m-d')) ? 'true' : 'false'; + ?> + + + + + + +
action_type)); ?>
+ + + + get_results("SELECT * FROM {$wpdb->prefix}ktc_logs WHERE status = 'pending_correction' ORDER BY timestamp DESC"); + if ( ! $pending ) return ''; + ob_start(); ?> +
+

Admin: Correction Queue

+ user_id); ?> +
+ display_name; ?>: proposed_timestamp)); ?> +
+ Approve + Reject +
+
+ +
+ + + + + + 'integer', 'default' => 12] ); + register_setting( 'ktc_settings_group', 'ktc_enable_teams', ['type' => 'boolean', 'default' => 0] ); + register_setting( 'ktc_settings_group', 'ktc_teams_webhook', ['type' => 'string'] ); + + // 3. Section + add_settings_section( + 'ktc_main_section', + 'Core Configuration', + '__return_false', + 'ktc-settings' + ); + + // 4. Fields - Triple check that these method names exist in this class + add_settings_field( + 'ktc_timezone_field', + 'Timezone', + array( __CLASS__, 'render_timezone_field' ), + 'ktc-settings', + 'ktc_main_section' + ); + + add_settings_field( + 'ktc_alert_threshold_field', + 'Zombie Alert Threshold (Hours)', + array( __CLASS__, 'render_threshold_field' ), + 'ktc-settings', + 'ktc_main_section' + ); + + add_settings_field( + 'ktc_teams_config_field', + 'MS Teams Integration', + array( 'KTC_Admin', 'render_teams_fields' ), + 'ktc-settings', + 'ktc_main_section' + ); + } + +public static function render_settings_content() { + ?> +
+

Krista Time Clock Settings

+
+ +
+
+ '; + echo '

Hours before a shift is flagged as a "Zombie Shift".

'; +} + +/** + * Renders the Teams Toggle and Webhook URL + */ +public static function render_teams_fields() { + $enabled = get_option( 'ktc_enable_teams', 0 ); + $webhook = get_option( 'ktc_teams_webhook', '' ); + ?> + +

+ +

Enter your Microsoft Teams Incoming Webhook URL.

+ + +

This defines the legal "Source of Truth" for all timestamps, bypassing server or WP site drift.

+ prefix . 'ktc_logs'; + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $table_name ( + id bigint(20) NOT NULL AUTO_INCREMENT, + user_id bigint(20) NOT NULL, + timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + proposed_timestamp datetime DEFAULT NULL, + action_type varchar(20) NOT NULL, + notes text, + status varchar(20) DEFAULT 'approved' NOT NULL, + PRIMARY KEY (id) + ) $charset_collate;"; + + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + dbDelta( $sql ); + + if ( ! get_role( 'ktc_user' ) ) { + add_role( 'ktc_user', 'KTC Timeclock User', array( 'read' => true ) ); + } + + $page_slug = 'clock'; + $page_check = get_page_by_path($page_slug); + + if ( ! $page_check ) { + $page_id = wp_insert_post( array( + 'post_title' => 'Clock', + 'post_content' => '[ktc_portal_welcome] [krista_timeclock]', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_name' => $page_slug + )); + } else { + $page_id = $page_check->ID; + } + + if ( $page_id ) { + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $page_id ); + } + } + + // --- Time Core Methods --- + + public static function get_ktc_timezone() { + return new DateTimeZone( get_option( 'ktc_timezone', 'America/Chicago' ) ); + } + + public static function get_now() { + return new DateTime( 'now', self::get_ktc_timezone() ); + } + + public static function get_local_timestamp( $date_string ) { + try { + $dt = new DateTime( $date_string, self::get_ktc_timezone() ); + return $dt->getTimestamp(); + } catch ( Exception $e ) { + return current_time( 'timestamp', 1 ); + } + } + + // --- Action Handlers --- + + public static function handle_actions() { + if ( ! is_user_logged_in() ) return; + + // Admin Approval Actions + if ( isset( $_GET['ktc_admin_action'] ) && current_user_can( 'manage_options' ) ) { + self::handle_admin_actions(); + return; + } + + // POST Route Guard + if ( ! isset( $_POST['ktc_action'] ) ) return; + + if ( ! isset( $_POST['ktc_nonce'] ) || ! wp_verify_nonce( $_POST['ktc_nonce'], 'ktc_punch_action' ) ) { + wp_die( 'Security check failed. Please refresh the page.' ); + } + + $action = $_POST['ktc_action']; + + switch ( $action ) { + case 'punch': + self::execute_punch_request(); + break; + + case 'edit_request': + self::execute_edit_request(); + break; + } + + wp_safe_redirect( home_url('/') ); + exit; + } + + /** + * THE DOUBLE-IN GUARD (Missing in previous version) + */ +private static function execute_punch_request() { + global $wpdb; + $user_id = get_current_user_id(); + $table_name = $wpdb->prefix . 'ktc_logs'; + + // 1. Force a clean check of the LAST recorded action + $last_action = $wpdb->get_var( $wpdb->prepare( + "SELECT action_type FROM $table_name WHERE user_id = %d ORDER BY id DESC LIMIT 1", + $user_id + )); + + // 2. The Toggle Logic: If last was 'clock_in', next is 'clock_out'. + // Anything else (including NULL) results in 'clock_in'. + $next_action = ( $last_action === 'clock_in' ) ? 'clock_out' : 'clock_in'; + + // 3. Debug/Audit Trail: Ensure we are sending what we think we are + return $wpdb->insert( + $table_name, + array( + 'user_id' => $user_id, + 'timestamp' => self::get_now()->format('Y-m-d H:i:s'), + 'action_type' => $next_action, + 'status' => 'approved' + ), + array( '%d', '%s', '%s', '%s' ) + ); +} + private static function execute_edit_request() { + global $wpdb; + $table_name = $wpdb->prefix . 'ktc_logs'; + $user_id = get_current_user_id(); + $log_id = intval( $_POST['ktc_edit_id'] ); + + $orig = $wpdb->get_var( $wpdb->prepare( + "SELECT timestamp FROM $table_name WHERE id = %d AND user_id = %d", + $log_id, $user_id + ) ); + + if ( $orig ) { + $tz = self::get_ktc_timezone(); + $date_part = (new DateTime($orig, $tz))->format('Y-m-d'); + $proposed = $date_part . ' ' . sanitize_text_field( $_POST['ktc_proposed_time'] ) . ':00'; + + $wpdb->update( $table_name, [ + 'proposed_timestamp' => $proposed, + 'status' => 'pending_correction', + 'notes' => sanitize_textarea_field( $_POST['ktc_edit_notes'] ) + ], [ 'id' => $log_id ]); + } + } + + private static function handle_admin_actions() { + global $wpdb; + $table_name = $wpdb->prefix . 'ktc_logs'; + $log_id = intval( $_GET['log_id'] ); + + if ( $_GET['ktc_admin_action'] === 'approve' ) { + $wpdb->update( $table_name, [ 'status' => 'approved' ], [ 'id' => $log_id ] ); + } else { + // Reject: Keep approved status but clear the proposed edit + $wpdb->update( $table_name, [ 'status' => 'approved', 'proposed_timestamp' => null ], [ 'id' => $log_id ] ); + } + wp_safe_redirect( home_url('/') ); + exit; + } + + // --- Statistics & Calculations --- + + public static function get_elapsed_time( $user_id ) { + global $wpdb; + $table_name = $wpdb->prefix . 'ktc_logs'; + $last = $wpdb->get_row( $wpdb->prepare( "SELECT timestamp, action_type FROM $table_name WHERE user_id = %d ORDER BY id DESC LIMIT 1", $user_id )); + + if ( ! $last || $last->action_type !== 'clock_in' ) return 0; + + $tz = self::get_ktc_timezone(); + $now = self::get_now(); + $start = new DateTime($last->timestamp, $tz); + + return $now->getTimestamp() - $start->getTimestamp(); + } + + public static function get_weekly_total( $user_id ) { + global $wpdb; + $table_name = $wpdb->prefix . 'ktc_logs'; + $now = self::get_now(); + + $week_start_day = get_option( 'ktc_week_start', 'Monday' ); + $start_of_week_ts = ( $now->format('l') === $week_start_day ) + ? strtotime( "today midnight", $now->getTimestamp() ) + : strtotime( "last $week_start_day midnight", $now->getTimestamp() ); + + $logs = $wpdb->get_results( $wpdb->prepare( + "SELECT timestamp, proposed_timestamp, action_type FROM $table_name + WHERE user_id = %d AND timestamp >= %s AND status = 'approved' ORDER BY timestamp ASC", + $user_id, wp_date( 'Y-m-d H:i:s', $start_of_week_ts ) + )); + + $total_seconds = 0; $temp_in = null; + foreach ( $logs as $log ) { + $ts = !empty($log->proposed_timestamp) ? self::get_local_timestamp($log->proposed_timestamp) : self::get_local_timestamp($log->timestamp); + if ( $log->action_type === 'clock_in' ) { $temp_in = $ts; } + elseif ( $log->action_type === 'clock_out' && $temp_in ) { + $total_seconds += ( $ts - $temp_in ); + $temp_in = null; + } + } + return round( $total_seconds / 3600, 2 ); + } + + public static function get_user_stats( $user_id ) { + global $wpdb; + $table_logs = $wpdb->prefix . 'ktc_logs'; + $table_off = $wpdb->prefix . 'ktc_timeoff'; + + $tz = self::get_ktc_timezone(); + $month_start = (new DateTime('first day of this month midnight', $tz))->format('Y-m-d H:i:s'); + + // Month Total Query + $logs = $wpdb->get_results( $wpdb->prepare( + "SELECT timestamp, proposed_timestamp, action_type FROM $table_logs + WHERE user_id = %d AND timestamp >= %s AND status = 'approved' ORDER BY timestamp ASC", + $user_id, $month_start + )); + + $total_seconds = 0; $temp_in = null; + foreach ( $logs as $log ) { + $ts = !empty($log->proposed_timestamp) ? self::get_local_timestamp($log->proposed_timestamp) : self::get_local_timestamp($log->timestamp); + if ( $log->action_type === 'clock_in' ) { $temp_in = $ts; } + elseif ( $log->action_type === 'clock_out' && $temp_in ) { + $total_seconds += ( $ts - $temp_in ); + $temp_in = null; + } + } + + return [ + 'week' => self::get_weekly_total( $user_id ), + 'month' => round( $total_seconds / 3600, 2 ), + 'pending_tor' => $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table_off WHERE user_id = %d AND status = 'pending'", $user_id ) ) ?: 0 + ]; + } + + /** + * Provides the dashboard stats for the Admin view. + * Restored to fix the Fatal Error in class-admin.php + */ + public static function get_admin_pulse() { + global $wpdb; + $table_logs = $wpdb->prefix . 'ktc_logs'; + $table_off = $wpdb->prefix . 'ktc_timeoff'; + + return [ + 'active_now' => $wpdb->get_var("SELECT COUNT(*) FROM $table_logs WHERE action_type = 'clock_in' AND status = 'approved'"), + 'pending_edits' => $wpdb->get_var("SELECT COUNT(*) FROM $table_logs WHERE status = 'pending_correction'"), + 'total_pending_tor' => $wpdb->get_var("SELECT COUNT(*) FROM $table_off WHERE status = 'pending'") ?: 0 + ]; + } + + /** + * Formats raw seconds into a human-readable Hh Mm Ss format. + * Restored to fix the Fatal Error on line 157 of class-admin.php + */ + public static function format_seconds( $seconds ) { + $hours = floor( $seconds / 3600 ); + $minutes = floor( ( $seconds % 3600 ) / 60 ); + $seconds = $seconds % 60; + + $output = ''; + if ( $hours > 0 ) $output .= "{$hours}h "; + if ( $minutes > 0 ) $output .= "{$minutes}m "; + $output .= "{$seconds}s"; + + return trim( $output ); + } + + /** + * Scans for active shifts exceeding the safety threshold. + * Triggered hourly via wp_cron. + */ + public static function scan_for_zombie_shifts() { + global $wpdb; + $threshold = get_option( 'ktc_alert_threshold', 12 ); + $table = $wpdb->prefix . 'ktc_logs'; + + $active_shifts = $wpdb->get_results(" + SELECT t1.user_id, t1.timestamp + FROM $table t1 + INNER JOIN ( + SELECT user_id, MAX(id) as max_id FROM $table GROUP BY user_id + ) t2 ON t1.id = t2.max_id + WHERE t1.action_type = 'clock_in' + "); + + foreach ( $active_shifts as $shift ) { + $start_time = new DateTime( $shift->timestamp, self::get_ktc_timezone() ); + $diff_hours = (self::get_now()->getTimestamp() - $start_time->getTimestamp()) / 3600; + + if ( $diff_hours > $threshold ) { + self::dispatch_alerts( $shift->user_id, round($diff_hours, 1) ); + } + } + } + + private static function dispatch_alerts( $user_id, $hours ) { + $user = get_userdata( $user_id ); + if ( ! $user ) return; + + // Cooldown check (once per 12 hours) + $last_sent = get_user_meta( $user_id, '_ktc_last_zombie_alert', true ); + if ( $last_sent && ( time() - $last_sent < 43200 ) ) return; + + $subject = "⚠️ Long Shift Alert: " . $user->display_name; + $message = "User is still clocked in after $hours hours."; + + // 1. Email Alert + wp_mail( $user->user_email, $subject, $message ); + + // 2. Teams Alert (if enabled) + if ( get_option( 'ktc_enable_teams' ) && get_option( 'ktc_teams_webhook' ) ) { + self::send_to_teams( $user->display_name, $hours ); + } + + update_user_meta( $user_id, '_ktc_last_zombie_alert', time() ); + } + + private static function send_to_teams( $name, $hours ) { + $webhook_url = get_option( 'ktc_teams_webhook' ); + + $payload = [ + "@type" => "MessageCard", + "@context" => "http://schema.org/extensions", + "themeColor" => "d32f2f", + "summary" => "Zombie Shift Detected", + "sections" => [[ + "activityTitle" => "⚠️ Zombie Shift Alert", + "activitySubtitle" => "Threshold Exceeded", + "facts" => [ + ["name" => "User:", "value" => $name], + ["name" => "Duration:", "value" => $hours . " hours"], + ["name" => "Status:", "value" => "Flagged for Correction"] + ], + "markdown" => true + ]] + ]; + + wp_remote_post( $webhook_url, [ + 'body' => json_encode($payload), + 'headers' => ['Content-Type' => 'application/json'], + ]); + } + +} diff --git a/krista-time-clock.php b/krista-time-clock.php new file mode 100755 index 0000000..00a2a98 --- /dev/null +++ b/krista-time-clock.php @@ -0,0 +1,26 @@ + 0) { + $friendly_name = $info[0]["displayname"][0]; + + // Update WordPress User + wp_update_user(array( + 'ID' => $user_id, + 'display_name' => $friendly_name, + 'nickname' => $friendly_name + )); + + // Force WP to use Display Name publicly + update_user_meta($user_id, 'display_name', $friendly_name); + } + } + ldap_unbind($ds); +}