diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fbae131..adfae84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ This file documents all notable changes made to ITFlow. +## [26.02.1] Maint Release +### Bug Fixes +- Credentials: Fix Password Generator. +- Calendar: Restrict Events for client restricted agents. +- Ticket Merge: Fix. +- Asset Transfer: Fix. +- Ticket Listing: Restrict Tickets presented in ticket list view from client restricted agents. +- Ticket Details: Deny access to client restricted agents to view tickets without client_id in uri. +- Tickets: Allow agents with restricted client access to view and edit tickets without a client. +- Ticket Change client: Limit selection for agents with restricted client access. +- Ticket Details: Don't display Updated at when null. + +### New Features & Updates +- Report: Added Client Detail Auditing. +- API: Added Endpoint to retrieve time worked by agent. +- ajax-modal: Revert to previous JS implementation before 26.02 release. +- Ticket: Move Subject from Ticket main ticket header to ticket details card header. + ## [26.02] Stable Release ### Bug Fixes - Mail Parser - Do not automatically send new ticket notifications to noreply/donotreply addresses. diff --git a/agent/ajax.php b/agent/ajax.php index 8079da84..beb25738 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -319,7 +319,7 @@ if (isset($_GET['get_client_assets'])) { LEFT JOIN contacts ON contact_id = asset_contact_id WHERE assets.asset_archived_at IS NULL AND asset_client_id = $client_id $access_permission_query - ORDER BY asset_important DESC, asset_name" + ORDER BY asset_favorite DESC, asset_name" ); while ($row = mysqli_fetch_assoc($asset_sql)) { diff --git a/agent/calendar.php b/agent/calendar.php index 04783c46..2a16120f 100644 --- a/agent/calendar.php +++ b/agent/calendar.php @@ -178,8 +178,8 @@ while ($row = mysqli_fetch_assoc($sql)) { echo "{ id: $event_id, title: $event_title, start: $event_start, end: $event_end, color: $calendar_color },"; } - //Invoices Created - $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN invoices ON client_id = invoice_client_id $client_query"); + // Invoices Created + $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN invoices ON client_id = invoice_client_id $client_query $access_permission_query"); while ($row = mysqli_fetch_assoc($sql)) { $event_id = intval($row['invoice_id']); $scope = strval($row['invoice_scope']); @@ -193,8 +193,8 @@ while ($row = mysqli_fetch_assoc($sql)) { echo "{ id: $event_id, title: $event_title, start: $event_start, display: 'list-item', color: 'blue', url: 'invoice.php?invoice_id=$event_id$client_url' },"; } - //Quotes Created - $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN quotes ON client_id = quote_client_id $client_query"); + // Quotes Created + $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN quotes ON client_id = quote_client_id $client_query $access_permission_query"); while ($row = mysqli_fetch_assoc($sql)) { $event_id = intval($row['quote_id']); $event_title = json_encode($row['quote_prefix'] . $row['quote_number'] . " " . $row['quote_scope']); @@ -203,12 +203,12 @@ while ($row = mysqli_fetch_assoc($sql)) { echo "{ id: $event_id, title: $event_title, start: $event_start, display: 'list-item', color: 'purple', url: 'quote.php?quote_id=$event_id$client_url' },"; } - //Tickets Created + // Tickets Created $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN tickets ON client_id = ticket_client_id LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id LEFT JOIN users ON ticket_assigned_to = user_id - $client_query" + $client_query $access_permission_query" ); while ($row = mysqli_fetch_assoc($sql)) { $event_id = intval($row['ticket_id']); @@ -242,7 +242,7 @@ while ($row = mysqli_fetch_assoc($sql)) { $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN recurring_tickets ON client_id = recurring_ticket_client_id LEFT JOIN users ON recurring_ticket_assigned_to = user_id - $client_query" + $client_query $access_permission_query" ); while ($row = mysqli_fetch_assoc($sql)) { $event_id = intval($row['recurring_ticket_id']); @@ -262,12 +262,12 @@ while ($row = mysqli_fetch_assoc($sql)) { echo "{ id: $event_id, title: $event_title, start: $event_start, color: '$event_color', url: 'recurring_tickets.php?client_id=$client_id$client_url' },"; } - //Tickets Scheduled + // Tickets Scheduled $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN tickets ON client_id = ticket_client_id LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id LEFT JOIN users ON ticket_assigned_to = user_id - $client_query AND ticket_schedule IS NOT NULL" + $client_query $access_permission_query AND ticket_schedule IS NOT NULL" ); while ($row = mysqli_fetch_assoc($sql)) { $event_id = intval($row['ticket_id']); @@ -297,8 +297,8 @@ while ($row = mysqli_fetch_assoc($sql)) { echo "{ id: $event_id, title: $event_title, start: $event_start, color: '$event_color', url: 'ticket.php?ticket_id=$event_id$client_url' },"; } - //Vendors Added Created - $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN vendors ON client_id = vendor_client_id $client_query"); + // Vendors Added Created + $sql = mysqli_query($mysqli, "SELECT * FROM clients LEFT JOIN vendors ON client_id = vendor_client_id $client_query $access_permission_query"); while ($row = mysqli_fetch_assoc($sql)) { $event_id = intval($row['vendor_id']); $client_id = intval($row['client_id']); diff --git a/agent/modals/client/client_edit.php b/agent/modals/client/client_edit.php index 06106856..b9220139 100644 --- a/agent/modals/client/client_edit.php +++ b/agent/modals/client/client_edit.php @@ -4,7 +4,7 @@ require_once '../../../includes/modal_header.php'; $client_id = intval($_GET['id']); -$sql = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_id = $client_id LIMIT 1"); +$sql = mysqli_query($mysqli, "SELECT * FROM clients WHERE client_id = $client_id $access_permission_query LIMIT 1"); $row = mysqli_fetch_assoc($sql); $client_name = nullable_htmlentities($row['client_name']); diff --git a/agent/modals/credential/credential_add.php b/agent/modals/credential/credential_add.php index 46710e89..4f5eaeca 100644 --- a/agent/modals/credential/credential_add.php +++ b/agent/modals/credential/credential_add.php @@ -260,6 +260,8 @@ ob_start(); + + - + - Select a Ticket - - + @@ -47,7 +47,7 @@ ob_start(); - + - Select a Ticket - 24h by using hours > 24) + */ +function secondsToHmsString($seconds) { + $seconds = (int) max(0, $seconds); + $hours = intdiv($seconds, 3600); + $minutes = intdiv($seconds % 3600, 60); + $secs = $seconds % 60; + return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs); +} + +/** + * 15-minute round up, return decimal hours in 0.25 increments + * NOTE: In this report, billed hours are calculated per TICKET total + * (sum of reply time within range, then rounded up to nearest 15 minutes). + */ +function secondsToQuarterHourDecimal($seconds) { + $seconds = (int) max(0, $seconds); + if ($seconds === 0) return 0.00; + + $quarters = (int) ceil($seconds / 900); // 900 seconds = 15 minutes + return $quarters * 0.25; +} + +/** + * Validate YYYY-MM-DD + */ +function isValidDateYmd($s) { + return is_string($s) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $s); +} + +// Default range: current month +$from = isset($_GET['from']) ? $_GET['from'] : date('Y-m-01'); +$to = isset($_GET['to']) ? $_GET['to'] : date('Y-m-t'); + +if (!isValidDateYmd($from)) $from = date('Y-m-01'); +if (!isValidDateYmd($to)) $to = date('Y-m-t'); + +// Inclusive datetime bounds +$from_dt = $from . " 00:00:00"; +$to_dt = $to . " 23:59:59"; + +$billable_only = (isset($_GET['billable_only']) && (int)$_GET['billable_only'] === 1) ? 1 : 0; + +// Ticket-level billable flag (same as your original report) +$billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : ""; + +/** + * Query returns ONLY replies that have time_worked and are within date range. + * Reply content column = tr.ticket_reply + */ +$stmt = $mysqli->prepare(" + SELECT + c.client_id, + c.client_name, + t.ticket_id, + t.ticket_prefix, + t.ticket_number, + t.ticket_subject, + + tr.ticket_reply_id, + tr.ticket_reply_created_at, + tr.ticket_reply_time_worked, + TIME_TO_SEC(tr.ticket_reply_time_worked) AS reply_time_seconds, + tr.ticket_reply AS reply_content + + FROM tickets t + INNER JOIN clients c + ON c.client_id = t.ticket_client_id + + INNER JOIN ticket_replies tr + ON tr.ticket_reply_ticket_id = t.ticket_id + AND tr.ticket_reply_time_worked IS NOT NULL + AND TIME_TO_SEC(tr.ticket_reply_time_worked) > 0 + AND tr.ticket_reply_created_at BETWEEN ? AND ? + + WHERE c.client_archived_at IS NULL + $billable_sql + + ORDER BY c.client_name ASC, + t.ticket_number ASC, + t.ticket_id ASC, + tr.ticket_reply_created_at ASC +"); +$stmt->bind_param("ss", $from_dt, $to_dt); +$stmt->execute(); +$result = $stmt->get_result(); + +?> + + + + + Client Time Detail Audit Report ( to ) + + Billable Only + + + + + Print + + + + + + + + + + From + + + + + To + + + + + + + > + Billable tickets only + + + + + + Apply + + + + + + + + + + + + + Ticket / Replies with Time + Time Worked + Billable (hrs) + + + + + + + Ticket Total for + + + + '; + } + + // Client subtotal + ?> + + + Total for ( tickets) + + + + + + + + + + '; + + // Reset ticket accumulator + $ticket_time_seconds = 0; + $current_ticket_id = null; + $current_ticket_label_html = null; + } + + // Ticket header (first row for this ticket) + if ($ticket_id !== $current_ticket_id) { + $current_ticket_id = $ticket_id; + $current_ticket_label_html = $ticket_label_html; + + $client_ticket_count++; + $grand_ticket_count++; + + ?> + + + + + + + + + + + + + + + + + + + + + No ticket replies with time worked found for this date range. + + + '; + } + + // Close last client subtotal + ?> + + + Total for ( tickets) + + + + + + + + + + + Grand Total ( tickets) + + + + + + + + + + This report shows only ticket replies with time worked within the selected date range. + Ticket “Billable (hrs)” totals are calculated by summing reply time per ticket within the range, + then rounding that ticket total up to the nearest 15 minutes (0.25 hours). + + Reply content is displayed under each reply timestamp. + + + + +Unbilled Tickets + + "> + + Client Time Detail Audit + + diff --git a/agent/reports/ticket_by_client_v2.php b/agent/reports/ticket_by_client_v2.php deleted file mode 100644 index 25e5c864..00000000 --- a/agent/reports/ticket_by_client_v2.php +++ /dev/null @@ -1,279 +0,0 @@ - 24h by using hours > 24) - */ -function secondsToHmsString($seconds) { - $seconds = (int) max(0, $seconds); - $hours = intdiv($seconds, 3600); - $minutes = intdiv($seconds % 3600, 60); - $secs = $seconds % 60; - return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs); -} - -/** - * 15-minute round up, return decimal hours in 0.25 increments - * Examples: - * 1 min => 0.25 - * 16 min => 0.50 - * 61 min => 1.25 - */ -function secondsToQuarterHourDecimal($seconds) { - $seconds = (int) max(0, $seconds); - if ($seconds === 0) return 0.00; - - $quarters = (int) ceil($seconds / 900); // 900 seconds = 15 minutes - return $quarters * 0.25; -} - -$year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y'); -$month = isset($_GET['month']) ? (int) $_GET['month'] : (int) date('m'); -if ($month < 1 || $month > 12) $month = (int) date('m'); - -$billable_only = (isset($_GET['billable_only']) && (int) $_GET['billable_only'] === 1) ? 1 : 0; - -// Used for Year dropdown -$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC"); - -// Billable filter (adjust field name if yours differs) -$billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : ""; - -/** - * IMPORTANT: - * This sums time worked ONLY for replies within the selected month/year - * by filtering on tr.ticket_reply_created_at. - * If your column name differs, replace ticket_reply_created_at accordingly. - */ -$stmt = $mysqli->prepare(" - SELECT - c.client_id, - c.client_name, - t.ticket_id, - t.ticket_prefix, - t.ticket_number, - t.ticket_subject, - SEC_TO_TIME(COALESCE(SUM(TIME_TO_SEC(tr.ticket_reply_time_worked)), 0)) AS ticket_time_hms, - COALESCE(SUM(TIME_TO_SEC(tr.ticket_reply_time_worked)), 0) AS ticket_time_seconds - FROM tickets t - INNER JOIN clients c - ON c.client_id = t.ticket_client_id - LEFT JOIN ticket_replies tr - ON tr.ticket_reply_ticket_id = t.ticket_id - AND tr.ticket_reply_time_worked IS NOT NULL - AND YEAR(tr.ticket_reply_created_at) = ? - AND MONTH(tr.ticket_reply_created_at) = ? - WHERE c.client_archived_at IS NULL - $billable_sql - GROUP BY t.ticket_id - HAVING ticket_time_seconds > 0 - ORDER BY c.client_name ASC, t.ticket_number ASC, t.ticket_id ASC -"); -$stmt->bind_param("ii", $year, $month); -$stmt->execute(); -$result = $stmt->get_result(); - -?> - - - - - - Ticket By Client () - - Billable Only - - - - - Print - - - - - - - - - - - Year - - - value=""> - - - - - - - - Month - - - value=""> - - - - - - - - - - > - Billable tickets only - - - - - - Apply - - - - - - - - - - - - - Ticket - Time Worked - Billable (hrs) - - - - - - - Total for ( tickets) - - - - - - - - - - - - = "$display_ticket - $ticket_subject" ?> - - - - - - No tickets with time worked found for this month. - - - - - Total for ( tickets) - - - - - - - - - - - Grand Total ( tickets) - - - - - - - - - - Billed hours are calculated per ticket by rounding that ticket’s worked time up to the nearest 15 minutes (0.25 hours). - - - - - Ticket = "$ticket_prefix$ticket_number" ?> - + = $ticket_status_name ?> - = $ticket_subject ?> @@ -439,10 +445,11 @@ if (isset($_GET['ticket_id'])) { - + + Updated: = date('M d, Y • g:i A', strtotime($ticket_updated_at)) . " ($ticket_updated_at_ago)" ?> - + @@ -535,9 +542,7 @@ if (isset($_GET['ticket_id'])) { - - Description / Comments - + = $ticket_subject ?> diff --git a/agent/ticket_kanban.php b/agent/ticket_kanban.php index 846f9379..493e18f0 100644 --- a/agent/ticket_kanban.php +++ b/agent/ticket_kanban.php @@ -64,7 +64,7 @@ $sql = mysqli_query( ticket_vendor_ticket_number LIKE '%$q%' ) $ticket_project_snippet - $ticket_permission_snippet + $access_permission_query_overide $ordering_snippet" ); diff --git a/agent/tickets.php b/agent/tickets.php index 0123dd96..0cf77b8a 100644 --- a/agent/tickets.php +++ b/agent/tickets.php @@ -88,15 +88,14 @@ if (isset($_GET['project']) & !empty($_GET['project']) && $_GET['project'] > '0' $ticket_project_filter_id = intval($_GET['project']); } -// Ticket client access snippet -$ticket_permission_snippet = ''; -if (!empty($client_access_string)) { - $ticket_permission_snippet = "AND ticket_client_id IN ($client_access_string)"; +// Ticket client access overide - This is the only way to show tickets without a client to agents with restricted client access +$access_permission_query_overide = ''; +if ($client_access_string) { + $access_permission_query_overide = "AND ticket_client_id IN (0,$client_access_string)"; } // Main ticket query: -$sql = mysqli_query( - $mysqli, +$query = "SELECT SQL_CALC_FOUND_ROWS * FROM tickets LEFT JOIN clients ON ticket_client_id = client_id LEFT JOIN contacts ON ticket_contact_id = contact_id @@ -112,7 +111,7 @@ $sql = mysqli_query( AND (CONCAT(ticket_prefix,ticket_number) LIKE '%$q%' OR client_name LIKE '%$q%' OR ticket_subject LIKE '%$q%' OR ticket_status_name LIKE '%$q%' OR ticket_priority LIKE '%$q%' OR user_name LIKE '%$q%' OR contact_name LIKE '%$q%' OR asset_name LIKE '%$q%' OR vendor_name LIKE '%$q%' OR ticket_vendor_ticket_number LIKE '%q%') $ticket_billable_snippet $ticket_project_snippet - $ticket_permission_snippet + $access_permission_query_overide $client_query ORDER BY CASE @@ -126,28 +125,29 @@ $sql = mysqli_query( ELSE NULL END $order, $sort $order -- Apply normal sorting by $sort and $order - LIMIT $record_from, $record_to" -); + LIMIT $record_from, $record_to"; + +$sql = mysqli_query($mysqli,$query); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); //Get Total tickets open -$sql_total_tickets_open = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_open FROM tickets WHERE ticket_resolved_at IS NULL $client_query $ticket_permission_snippet"); +$sql_total_tickets_open = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_open FROM tickets WHERE ticket_resolved_at IS NULL $client_query $access_permission_query_overide"); $row = mysqli_fetch_assoc($sql_total_tickets_open); $total_tickets_open = intval($row['total_tickets_open']); //Get Total tickets closed -$sql_total_tickets_closed = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_closed FROM tickets WHERE ticket_resolved_at IS NOT NULL $client_query $ticket_permission_snippet"); +$sql_total_tickets_closed = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_closed FROM tickets WHERE ticket_resolved_at IS NOT NULL $client_query $access_permission_query_overide"); $row = mysqli_fetch_assoc($sql_total_tickets_closed); $total_tickets_closed = intval($row['total_tickets_closed']); //Get Unassigned tickets -$sql_total_tickets_unassigned = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_unassigned FROM tickets WHERE ticket_assigned_to = '0' AND ticket_resolved_at IS NULL $client_query $ticket_permission_snippet"); +$sql_total_tickets_unassigned = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_unassigned FROM tickets WHERE ticket_assigned_to = '0' AND ticket_resolved_at IS NULL $client_query $access_permission_query_overide"); $row = mysqli_fetch_assoc($sql_total_tickets_unassigned); $total_tickets_unassigned = intval($row['total_tickets_unassigned']); //Get Total tickets assigned to me -$sql_total_tickets_assigned = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_assigned FROM tickets WHERE ticket_assigned_to = $session_user_id AND ticket_resolved_at IS NULL $client_query $ticket_permission_snippet"); +$sql_total_tickets_assigned = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS total_tickets_assigned FROM tickets WHERE ticket_assigned_to = $session_user_id AND ticket_resolved_at IS NULL $client_query $access_permission_query_overide"); $row = mysqli_fetch_assoc($sql_total_tickets_assigned); $user_active_assigned_tickets = intval($row['total_tickets_assigned']); @@ -159,8 +159,6 @@ $sql_categories_filter = mysqli_query( ORDER BY category_name" ); - - ?>
Client Time Detail Audit