<?php
// Plugin Check / PHPCS: category-level exclusions with reasons.
// Line-level findings in this file are handled individually below.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- PaintOps keeps its records in its own custom tables (built by paintops_table()); $wpdb is the supported API for plugin-owned tables, there is no core CRUD alternative, and results are request-scoped.
// phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date -- date() here is deliberately paired with current_time() timestamps: crews are scheduled on the site's local calendar day, and gmdate() would shift jobs across midnight UTC.
if (!defined('ABSPATH')) exit;

// Wrapped in a named function (2026-07-27, wp.org hosted Plugin Check run):
// this file only ever runs via require from inside paintops_fetch_csjobs()
// in manage-jobs.php, so every variable below is function-local at runtime.
// PHPCS reads the file in isolation, saw code at file scope, and warned that
// every variable here was an unprefixed global. The wrapper makes the real
// scope explicit. function_exists() guard because the file is require'd (not
// require_once) and some SEO plugins run do_shortcode() early, which can pull
// it in twice in one request.
if (!function_exists('paintops_render_timeoff_screen')) {
function paintops_render_timeoff_screen() {
global $wpdb;


// This file is only ever require()'d from inside paintops_fetch_csjobs() in
// manage-jobs.php, which already checks paintops_user_can_manage() before
// including it. Kept here too as defense in depth in case that changes.
if (!paintops_user_can_manage()) {
    echo '<div class="paintops-login-required"><p>Please log in to access PaintOps.</p></div>';
    return;
}

$thisMonth = (int)gmdate("m", current_time('timestamp'));
$thisYear = (int)gmdate("Y", current_time('timestamp'));
// ===================================================
// Handle DB delete
// ===================================================
if (!empty($_GET['delTid'])) {
	$time_id = intval($_GET['delTid']);
	$valid_request = paintops_user_can_manage()
		&& wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'paintops_delete_timeoff_' . $time_id);

	if (!$valid_request) {
		echo "<div style=\"color:red\">That delete link is invalid or expired.</div>";
	} else {
		$deleted = $wpdb->delete(paintops_table('timeoff'), [ 'id' => $time_id ], [ '%d' ]);
		if ($deleted) {
			echo wp_kses("<div style=\"color:green\">Time off entry #$time_id deleted.</div>", paintops_kses_allowed_html(), paintops_kses_protocols());
		}
	}
}
// ===================================================
// Handle DB add
// ===================================================
if (isset($_POST['addTimeOff'])
	&& wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['paintops_timeoff_nonce'] ?? '')), 'paintops_save_timeoff')
	&& (!function_exists('paintops_once_check') || paintops_once_check())
	&& paintops_user_can_manage()) {
	$reqWorkersArr = array_map('absint', (array) wp_unslash($_POST['reqWorkers'] ?? []));
	$reqWorkers = '';

	if (is_array($reqWorkersArr)) {
		foreach ($reqWorkersArr as $reqWorker) {
			$reqWorkers .= '#' . intval($reqWorker) . '#';
		}
	}

	$required_fields = [
		'startDay'   => 'Day of Month',
		'startMonth' => 'Month',
		'startYear'  => 'Year',
		'daysOff'    => '# Days Off',
	];

	$missing_fields = [];
	$clean_input    = [];
	$formErr        = '';

	// Manually validate reqWorkers
	if (empty($reqWorkersArr) || !is_array($reqWorkersArr)) {
		$missing_fields[] = 'No Worker Selected';
	}

	// Validate other fields
	foreach ($required_fields as $field => $label) {
		$value = sanitize_text_field(wp_unslash($_POST[$field] ?? ''));
		if (trim($value) === '') {
			$missing_fields[] = $label;
		} else {
			$clean_input[$field] = sanitize_text_field($value);
		}
	}

	if ($missing_fields) {
		echo '<p style="color:red"><strong>Missing required fields:</strong> ' . esc_html(implode(', ', $missing_fields)) . '</p>';
	}
	else{
        // Create variables for fields (explicit loop, not extract(),
        // 2026-07-22, Plugin Check flags extract).
        foreach ($clean_input as $paintops_ci_key => $paintops_ci_val) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- keys come from the fixed $clean_input field whitelist built above, never user-chosen names, so no global (incl. $wpdb) can be overwritten.
            ${$paintops_ci_key} = $paintops_ci_val;
        }
        // A checkbox stored as %d: sanitize by casting to a 1/0 flag.
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- value is cast to a 1/0 flag, sanitization not applicable
        $isRepeat = !empty($_POST['isRepeat']) ? 1 : 0;
		$job_data = [
			'user_id'  => $reqWorkers,
			'day'      => $startDay,
			'month'    => $startMonth,
			'year'     => $startYear,
			'daysOff'  => $daysOff,
			'isRepeat' => $isRepeat,
		];

		$job_formats = [
			'user_id'    => '%s',
			'day'        => '%d',
			'month'      => '%d',
			'year' 		 => '%d',
			'daysOff'    => '%d',
			'isRepeat'   => '%d',
		];
		
		$result = $wpdb->insert(
		paintops_table('timeoff'),
		$job_data,
		array_values($job_formats) // extract formats as numeric array
		);
		
		if ($result === false) {
			echo "<div style=\"color:red\">Failed with error: " . esc_html($wpdb->last_error) . "</div>";

		} else {
				// (int) casts (wp.org review 2026-07-20): sanitize_text_field
				// keeps quote characters, so raw POST values could break out
				// of the href attribute; month/year are numbers, cast them.
				echo "<div style=\"color:green\">Time off was added to the schedule. Confirm by <a href=\"" . paintops_href(['calendar' => 1, 'cmonth' => (int) $startMonth, 'cyear' => (int) $startYear]) . "\">viewing the calendar</a>.</div>";
			
		}
	}
}


echo "Schedule holidays, vacations, or time you need to not be placed on the schedule. Dates are shown as dd/mm/yyyy<br><br>";


echo "<h3>Add Time Off</h3>";
// Get all workers, Administrators/Editors plus anyone with the
// Painter/Helper role (see paintops_get_assignable_workers()).
$results = paintops_get_assignable_workers();

echo "<form method=\"POST\">";
if ($results) {
	foreach ($results as $row) {
		// SECURITY (2026-07-19, audit P1): display_name is user-controlled
		// (a worker can edit their own WP profile name), and this staff-
		// facing form echoed it raw, stored XSS against a manager. The
		// list further down this page already escapes it; now both do.
		$display_name = esc_html($row->display_name);
		$uid = (int) $row->ID;
		$po_tpl_html = "<div style='display: inline-flex; align-items: center; margin-bottom: 8px;'>
		<input 
			style='width: 28px; height: 28px; margin: 0 8px 0 0;' 
			type='checkbox' 
			id='worker_$uid' 
			value='$uid' 
			name='reqWorkers[]'>
		<label for='worker_$uid' style='margin: 0; line-height: 28px;'>$display_name</label>
		</div><br>";
		echo wp_kses($po_tpl_html, paintops_kses_allowed_html(), paintops_kses_protocols());
	}
}
// Date-fields grid CSS through the styles API (wp.org review 2026-07);
// was a literal <style> block here (assets/css/timeoff-form.css).
// NOTE (2026-07-28): .addr-row is ALSO defined 4-column in paintops-app.css
// (Address|City|State|Zip); which file wins depends on enqueue order, which
// differs between sites (that is how creditwww got a 4-column Time Off row
// while Coastal rendered 3). The row below carries its intended 3-column
// template INLINE, the same order-proof pattern customers.php and
// job-followups.php already use for their addr-row variants.
paintops_enqueue_style('timeoff-form');
?>
<div class="addr-row" style="grid-template-columns: 2fr 1fr 0.8fr;">
	<div>
    <label for="pyear">Year</label>
    <input type="number" step="1" min="2025" max="2099" id="pyear" name="startYear" value="<?php echo esc_attr(gmdate('Y', current_time('timestamp'))); ?>">
    </div>

	<div>
    <label for="pmonth">Month</label>
    <input type="number" step="1" min="1" max="12" id="pmonth" name="startMonth" value="">
    </div>

	<div>
    <label for="pday">Day</label>
    <input type="number" step="1" min="1" max="31" id="pday" name="startDay" value="">
    </div>

<div>
    <label for="daysOff">Days Off</label>
    <input type="number" step="1" min="1" max="365" id="daysOff" name="daysOff" value="">
    
<label for="isrepeat">Repeat Each Year?</label>
	<input 
		style='width: 28px; height: 28px; margin: 0 8px 0 0;' 
		type='checkbox' 
		id='isRepeat' 
		value='1' 
		name='isRepeat'>
</div>
</div>
<br>
<input type="hidden" name="addTimeOff" value="1">
<?php wp_nonce_field('paintops_save_timeoff', 'paintops_timeoff_nonce'); ?>
<?php echo wp_kses(function_exists('paintops_once_field') ? paintops_once_field() : '', paintops_kses_allowed_html(), paintops_kses_protocols()); ?>
<div class="paintops-sticky-save">
    <button type="submit" class="button button-primary">Add Time Off</button>
</div>
</form>
<?php
// View the list of time off
echo "<br><br><h3>Scheduled Time Off</h3>";
$results = $wpdb->get_results($wpdb->prepare("SELECT * FROM %i WHERE year >= %d ORDER BY year asc, month asc, day asc", paintops_table('timeoff'), (int) $thisYear));
if ($results) {
	$optionPrefills = "";
	
	foreach ($results as $row) {
		$workerList = "";
		if ($row->isRepeat==1){
			$isRepeat = "[<b>repeating annually</b>]";
		}
		else{
			$isRepeat = "";
		}
		
		if ($row->daysOff==1){
			$days = "day";
		}
		else{
			$days = "days";
		}

		// Extract User IDs and get each user
		// ex $row->user_id = '#5##6#';
		$userIds = paintops_parse_worker_ids($row->user_id);

		foreach ($userIds as $userId) {
			$user = get_userdata($userId);
			// get_userdata() returns false for a deleted/stale user ID,
			// guard against that (a worker removed from the site shouldn't
			// break this list), and esc_html() the display name since a
			// worker can set this themselves on their own WP profile.
			if (!$user) {
				continue;
			}
			$userName = esc_html($user->display_name);
			$workerList .= " [$userName] ";
		}
		
		$day = $row->day;
		$month = $row->month;
		$year = $row->year;

		$dateString = "$year-$month-$day"; // Format to ISO (YYYY-MM-DD)
		$date = DateTime::createFromFormat('Y-m-d', $dateString);

		// Format it however you want
		$fullDate = $date->format('d M Y'); // "15 Sep 2025"
		$delete_url = wp_nonce_url(
			add_query_arg(['timeOff' => 1, 'delTid' => $row->id], ''),
			'paintops_delete_timeoff_' . $row->id
		);
		echo wp_kses("$workerList $isRepeat starting on $fullDate  for ".$row->daysOff." $days [<a href=\"" . esc_url($delete_url) . "\" onclick=\"return confirm('Delete this time?');\">Delete</a>]<br>", paintops_kses_allowed_html(), paintops_kses_protocols());
	}
} else {
	// Empty state (Mike 2026-07-22): a bare heading with nothing under it
	// read like the page was broken.
	echo '<p style="color:var(--po-text-muted,#777);">No time off scheduled yet. Entries you add above will be listed here and blocked out on the calendar.</p>';
}
// Reserve space so the fixed "Add Time Off" bar (.paintops-sticky-save,
// position:fixed at the bottom) never covers the last row of the scheduled
// list above, same reservation the job/quote forms get from .job-form's
// padding-bottom.
echo '<div style="height:90px" aria-hidden="true"></div>';
?>

<?php
} // end paintops_render_timeoff_screen()
} // end function_exists() guard
paintops_render_timeoff_screen();
