Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Granular permission tables and seeder on activation.
- "Powered by Escalated" badge with admin toggle.
- Workflow `delay` action — pauses a workflow run for N seconds and resumes the remaining actions via a per-minute WP-Cron sweep. Backed by a new `escalated_deferred_workflow_jobs` table with a composite `(status, run_at)` index for efficient polling. Existing installs need to reactivate the plugin to pick up the new table.
- Users management admin page (Escalated → Users) — list WordPress users with their Escalated admin/agent roles, search by name/email, paginated 20 per page. Toggle the `escalated_admin` / `escalated_agent` WP roles per user with the same self-demote and admin→agent cascade rules as the Laravel reference (escalated-laravel #94). Gated by the `escalated_user_manage` capability (held by `escalated_admin` and `administrator` roles).

### Changed
- License changed from GPL-2.0-or-later to MIT.
Expand Down
4 changes: 2 additions & 2 deletions escalated.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Plugin Name: Escalated
* Plugin URI: https://github.com/escalated-dev/escalated-wordpress
* Description: A full-featured helpdesk and ticketing system with multi-role support, SLA tracking, escalation rules, inbound email, macros, and REST API.
* Version: 1.2.0
* Version: 1.2.1
* Author: Escalated
* Author URI: https://escalated.dev
* License: MIT
Expand All @@ -18,7 +18,7 @@
exit;
}

define('ESCALATED_VERSION', '1.2.0');
define('ESCALATED_VERSION', '1.2.1');
define('ESCALATED_PLUGIN_FILE', __FILE__);
define('ESCALATED_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('ESCALATED_PLUGIN_URL', plugin_dir_url(__FILE__));
Expand Down
2 changes: 2 additions & 0 deletions includes/Admin/class-admin-menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ public function add_menus(): void
add_submenu_page('escalated', __('Automations', 'escalated'), __('Automations', 'escalated'), 'escalated_automation_view', 'escalated-automations', [new Admin_Automations, 'render']);
add_submenu_page('escalated', __('Escalation Rules', 'escalated'), __('Escalation Rules', 'escalated'), 'escalated_escalation_view', 'escalated-escalation-rules', [new Admin_Escalation_Rules, 'render']);
add_submenu_page('escalated', __('Tags', 'escalated'), __('Tags', 'escalated'), 'escalated_tag_view', 'escalated-tags', [new Admin_Tags, 'render']);
add_submenu_page('escalated', __('Skills', 'escalated'), __('Skills', 'escalated'), 'escalated_skill_manage', 'escalated-skills', [new Admin_Skills, 'render']);
add_submenu_page('escalated', __('Canned Responses', 'escalated'), __('Canned Responses', 'escalated'), 'escalated_macro_manage', 'escalated-canned-responses', [new Admin_Canned_Responses, 'render']);
add_submenu_page('escalated', __('Macros', 'escalated'), __('Macros', 'escalated'), 'escalated_macro_view', 'escalated-macros', [new Admin_Macros, 'render']);
add_submenu_page('escalated', __('Reports', 'escalated'), __('Reports', 'escalated'), 'escalated_report_view', 'escalated-reports', [new Admin_Reports, 'render']);
add_submenu_page('escalated', __('Users', 'escalated'), __('Users', 'escalated'), 'escalated_user_manage', 'escalated-users', [new Admin_Users, 'render']);
add_submenu_page('escalated', __('API Tokens', 'escalated'), __('API Tokens', 'escalated'), 'escalated_api_token_view', 'escalated-api-tokens', [new Admin_Api_Tokens, 'render']);
add_submenu_page('escalated', __('Settings', 'escalated'), __('Settings', 'escalated'), 'escalated_settings_view', 'escalated-settings', [new Admin_Settings, 'render']);
}
Expand Down
142 changes: 142 additions & 0 deletions includes/Admin/class-admin-skills.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

namespace Escalated\Admin;

use Escalated\Services\SkillService;

/**
* WordPress admin UI for skills (parity with shared Escalated/Admin/Skills/* Vue pages).
*
* When the host mounts the shared Inertia bundle, use the same props shape as:
* - Escalated/Admin/Skills/Index.vue (route escalated.admin.skills.index)
* - Escalated/Admin/Skills/Form.vue (create/edit — escalated.admin.skills.create / .edit)
*
* REST endpoints under /wp-json/escalated/v1/admin/skills supply those props for embedded panels.
*/
class Admin_Skills
{
public function __construct()
{
add_action('admin_init', [$this, 'handle_actions']);
}

public function render(): void
{
$skills = SkillService::list_for_admin();
$ctx = SkillService::get_form_context();
$edit_skill = null;

if (isset($_GET['action'], $_GET['id']) && $_GET['action'] === 'edit') {
$edit_skill = SkillService::find_for_edit(absint($_GET['id']));
}

$message = isset($_GET['message']) ? sanitize_text_field(wp_unslash($_GET['message'])) : '';
$error = isset($_GET['error']) ? sanitize_text_field(wp_unslash($_GET['error'])) : '';

include ESCALATED_PLUGIN_DIR.'templates/admin/skills.php';
}

public function handle_actions(): void
{
if (! isset($_POST['escalated_skill_action'])) {
return;
}

if (! current_user_can('escalated_skill_manage')) {
wp_die(esc_html__('Permission denied.', 'escalated'));
}

$action = sanitize_text_field(wp_unslash($_POST['escalated_skill_action']));
$redirect = admin_url('admin.php?page=escalated-skills');

switch ($action) {
case 'create':
case 'update':
if ($action === 'create') {
if (! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_escalated_nonce'] ?? '')), 'escalated_skill_create')) {
wp_die(esc_html__('Security check failed.', 'escalated'));
}
} else {
$id = absint($_POST['skill_id'] ?? 0);
if (! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_escalated_nonce'] ?? '')), 'escalated_skill_update_'.$id)) {
wp_die(esc_html__('Security check failed.', 'escalated'));
}
}

$payload = [
'name' => sanitize_text_field(wp_unslash($_POST['name'] ?? '')),
'routing_tag_ids' => isset($_POST['routing_tag_ids']) && is_array($_POST['routing_tag_ids'])
? array_map('absint', wp_unslash($_POST['routing_tag_ids']))
: [],
'routing_department_ids' => isset($_POST['routing_department_ids']) && is_array($_POST['routing_department_ids'])
? array_map('absint', wp_unslash($_POST['routing_department_ids']))
: [],
'agents' => self::parse_agents_from_post(),
];

if ($action === 'create') {
$result = SkillService::create($payload);
if (is_wp_error($result)) {
$redirect = add_query_arg('error', rawurlencode($result->get_error_message()), $redirect);
} else {
$redirect = add_query_arg('message', 'created', $redirect);
}
} else {
$id = absint($_POST['skill_id'] ?? 0);
$result = SkillService::update($id, $payload);
if (is_wp_error($result)) {
$redirect = add_query_arg('error', rawurlencode($result->get_error_message()), $redirect);
} else {
$redirect = add_query_arg(['message' => 'updated', 'action' => 'edit', 'id' => $id], $redirect);
}
}
break;

case 'delete':
$id = absint($_POST['skill_id'] ?? 0);
if (! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_escalated_nonce'] ?? '')), 'escalated_skill_delete_'.$id)) {
wp_die(esc_html__('Security check failed.', 'escalated'));
}
$result = SkillService::delete($id);
if (is_wp_error($result)) {
$redirect = add_query_arg('error', rawurlencode($result->get_error_message()), $redirect);
} else {
$redirect = add_query_arg('message', 'deleted', $redirect);
}
break;
}

wp_safe_redirect($redirect);
exit;
}

/**
* @return array<int, array{user_id: int, proficiency: int}>
*/
private static function parse_agents_from_post(): array
{
$enabled = isset($_POST['enabled_agent_ids']) && is_array($_POST['enabled_agent_ids'])
? array_map('absint', wp_unslash($_POST['enabled_agent_ids']))
: [];
$prof_map = isset($_POST['agent_proficiency']) && is_array($_POST['agent_proficiency'])
? wp_unslash($_POST['agent_proficiency'])
: [];

$out = [];
foreach ($enabled as $uid) {
if ($uid <= 0) {
continue;
}
$p = isset($prof_map[$uid]) ? absint($prof_map[$uid]) : 3;
if ($p < 1) {
$p = 1;
}
if ($p > 5) {
$p = 5;
}
$out[] = ['user_id' => $uid, 'proficiency' => $p];
}

return $out;
}
}
Loading