mirror of
https://github.com/tips-of-mine/GLPI-Plugin-SOC-Case-Management.git
synced 2026-05-27 14:28:57 +02:00
Start repository
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"template": "bolt-vite-react-ts"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
|
||||
|
||||
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
|
||||
|
||||
Use icons from lucide-react for logos.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
@@ -0,0 +1,43 @@
|
||||
# GLPI SOC Case Management Plugin
|
||||
|
||||
## Overview
|
||||
This plugin provides a specialized case management system for Security Operations Center (SOC) teams working with GLPI. It enables efficient creation and tracking of security cases with seamless integration into GLPI's change and request management workflows.
|
||||
|
||||
## Features
|
||||
- Case creation and management dashboard
|
||||
- Integration with GLPI's ticketing system
|
||||
- Case timeline view
|
||||
- Severity classification system
|
||||
- Attachment handling for evidence
|
||||
- SOC-specific KPI dashboard and reporting
|
||||
- Role-based access control
|
||||
- Search and filtering capabilities
|
||||
|
||||
## Requirements
|
||||
- GLPI >= 10.0.0
|
||||
- PHP >= 7.4.0
|
||||
|
||||
## Installation
|
||||
1. Download the ZIP file
|
||||
2. Extract it in your GLPI plugins directory (`glpi/plugins/`)
|
||||
3. Rename the directory to "soc" if needed
|
||||
4. Navigate to Setup > Plugins in your GLPI web interface
|
||||
5. Install and activate the plugin
|
||||
|
||||
## Configuration
|
||||
After activation, you can configure the plugin by:
|
||||
1. Setting up access rights in Administration > Profiles
|
||||
2. Customizing the dashboard widgets if needed
|
||||
|
||||
## Usage
|
||||
- Access the SOC dashboard from the Management menu
|
||||
- Create new security cases with appropriate severity levels
|
||||
- Link cases to tickets and changes for comprehensive incident management
|
||||
- Track case progress through the timeline view
|
||||
- Generate reports on SOC team performance
|
||||
|
||||
## License
|
||||
GPL-3.0+
|
||||
|
||||
## Author
|
||||
Your Organization
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/* Main SOC plugin styles */
|
||||
.soc-dashboard {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.soc-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.soc-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.soc-card-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.soc-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.soc-card-content {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Status and severity indicators */
|
||||
.soc-status, .soc-severity {
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
.soc-status-new {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.soc-status-assigned {
|
||||
background-color: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
.soc-status-in-progress {
|
||||
background-color: #fffde7;
|
||||
color: #fbc02d;
|
||||
}
|
||||
|
||||
.soc-status-pending {
|
||||
background-color: #fff8e1;
|
||||
color: #ff8f00;
|
||||
}
|
||||
|
||||
.soc-status-resolved {
|
||||
background-color: #e8eaf6;
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.soc-status-closed {
|
||||
background-color: #eceff1;
|
||||
color: #607d8b;
|
||||
}
|
||||
|
||||
/* Severity colors */
|
||||
.soc-severity-critical {
|
||||
background-color: #ffebee;
|
||||
color: #d32f2f;
|
||||
border-left: 4px solid #d32f2f;
|
||||
}
|
||||
|
||||
.soc-severity-high {
|
||||
background-color: #fff3e0;
|
||||
color: #f57c00;
|
||||
border-left: 4px solid #f57c00;
|
||||
}
|
||||
|
||||
.soc-severity-medium {
|
||||
background-color: #fffde7;
|
||||
color: #fbc02d;
|
||||
border-left: 4px solid #fbc02d;
|
||||
}
|
||||
|
||||
.soc-severity-low {
|
||||
background-color: #e8f5e9;
|
||||
color: #4caf50;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.soc-timeline {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.soc-timeline:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.soc-timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.soc-timeline-item:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
top: 5px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #0056b3;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.soc-timeline-item-content {
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.soc-timeline-item-time {
|
||||
color: #757575;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Animation for status changes */
|
||||
@keyframes statusChange {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.soc-status-changed {
|
||||
animation: statusChange 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* KPI Dashboard */
|
||||
.soc-kpi-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.soc-kpi-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.soc-kpi-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.soc-kpi-label {
|
||||
color: #757575;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.soc-dashboard {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.soc-kpi-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.soc-kpi-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
include ("../../../inc/includes.php");
|
||||
|
||||
Session::checkRight("plugin_soc_case", READ);
|
||||
|
||||
// Check if plugin is activated
|
||||
if (!Plugin::isPluginActive("soc")) {
|
||||
Html::displayNotFoundError();
|
||||
}
|
||||
|
||||
if (isset($_POST["add"])) {
|
||||
Session::checkRight("plugin_soc_case", CREATE);
|
||||
$case = new PluginSocCase();
|
||||
$case->check(-1, CREATE, $_POST);
|
||||
$case->add($_POST);
|
||||
Html::back();
|
||||
} else if (isset($_POST["update"])) {
|
||||
Session::checkRight("plugin_soc_case", UPDATE);
|
||||
$case = new PluginSocCase();
|
||||
$case->check($_POST['id'], UPDATE);
|
||||
$case->update($_POST);
|
||||
Html::back();
|
||||
} else if (isset($_POST["delete"])) {
|
||||
Session::checkRight("plugin_soc_case", DELETE);
|
||||
$case = new PluginSocCase();
|
||||
$case->check($_POST['id'], DELETE);
|
||||
$case->delete($_POST);
|
||||
Html::redirect(Plugin::getWebDir("soc")."/front/case.php");
|
||||
} else if (isset($_POST["restore"])) {
|
||||
Session::checkRight("plugin_soc_case", DELETE);
|
||||
$case = new PluginSocCase();
|
||||
$case->check($_POST['id'], DELETE);
|
||||
$case->restore($_POST);
|
||||
Html::back();
|
||||
} else if (isset($_POST["purge"])) {
|
||||
Session::checkRight("plugin_soc_case", PURGE);
|
||||
$case = new PluginSocCase();
|
||||
$case->check($_POST['id'], PURGE);
|
||||
$case->delete($_POST, 1);
|
||||
Html::redirect(Plugin::getWebDir("soc")."/front/case.php");
|
||||
} else if (isset($_POST["add_ticket"])) {
|
||||
Session::checkRight("plugin_soc_case", UPDATE);
|
||||
$case = new PluginSocCase();
|
||||
$case->getFromDB($_POST['plugin_soc_cases_id']);
|
||||
$tickets_id = $case->createTicket($_POST);
|
||||
Html::back();
|
||||
} else if (isset($_POST["add_change"])) {
|
||||
Session::checkRight("plugin_soc_case", UPDATE);
|
||||
$case = new PluginSocCase();
|
||||
$case->getFromDB($_POST['plugin_soc_cases_id']);
|
||||
$changes_id = $case->createChange($_POST);
|
||||
Html::back();
|
||||
} else {
|
||||
$id = "";
|
||||
if (isset($_GET["id"])) {
|
||||
$id = $_GET["id"];
|
||||
}
|
||||
$case = new PluginSocCase();
|
||||
|
||||
Html::header(PluginSocCase::getTypeName(Session::getPluralNumber()), '', "management", "pluginsoccase");
|
||||
|
||||
$case->display(['id' => $id]);
|
||||
|
||||
Html::footer();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
include ("../../../inc/includes.php");
|
||||
|
||||
Session::checkRight("plugin_soc_case", READ);
|
||||
|
||||
// Check if plugin is activated
|
||||
if (!Plugin::isPluginActive("soc")) {
|
||||
Html::displayNotFoundError();
|
||||
}
|
||||
|
||||
Html::header(PluginSocCase::getTypeName(Session::getPluralNumber()), '', "management", "pluginsoccase");
|
||||
|
||||
Search::show('PluginSocCase');
|
||||
|
||||
Html::footer();
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
include ("../../../inc/includes.php");
|
||||
|
||||
Session::checkRight("plugin_soc_case", READ);
|
||||
|
||||
// Check if plugin is activated
|
||||
if (!Plugin::isPluginActive("soc")) {
|
||||
Html::displayNotFoundError();
|
||||
}
|
||||
|
||||
Html::header(__('SOC Dashboard', 'soc'), '', "management", "pluginsoccase");
|
||||
|
||||
// Get counts
|
||||
$case = new PluginSocCase();
|
||||
$total_cases = countElementsInTable($case->getTable());
|
||||
$new_cases = countElementsInTable($case->getTable(), ['status' => PluginSocCase::STATUS_NEW]);
|
||||
$critical_cases = countElementsInTable($case->getTable(), ['severity' => PluginSocCase::SEVERITY_CRITICAL]);
|
||||
|
||||
// Display dashboard
|
||||
?>
|
||||
<div class="soc-dashboard">
|
||||
<h2><?php echo __('SOC Case Management Dashboard', 'soc'); ?></h2>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="soc-kpi-container">
|
||||
<div class="soc-kpi-card">
|
||||
<div class="soc-kpi-label"><?php echo __('Total Cases', 'soc'); ?></div>
|
||||
<div class="soc-kpi-value" data-value="<?php echo $total_cases; ?>"><?php echo $total_cases; ?></div>
|
||||
</div>
|
||||
|
||||
<div class="soc-kpi-card">
|
||||
<div class="soc-kpi-label"><?php echo __('New Cases', 'soc'); ?></div>
|
||||
<div class="soc-kpi-value" data-value="<?php echo $new_cases; ?>"><?php echo $new_cases; ?></div>
|
||||
</div>
|
||||
|
||||
<div class="soc-kpi-card">
|
||||
<div class="soc-kpi-label"><?php echo __('Critical Cases', 'soc'); ?></div>
|
||||
<div class="soc-kpi-value" data-value="<?php echo $critical_cases; ?>"><?php echo $critical_cases; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Cases -->
|
||||
<div class="soc-card" style="flex: 1 1 100%;">
|
||||
<div class="soc-card-header">
|
||||
<h3 class="soc-card-title"><?php echo __('Recent Cases', 'soc'); ?></h3>
|
||||
</div>
|
||||
<div class="soc-card-content">
|
||||
<?php
|
||||
$cases = $DB->request([
|
||||
'FROM' => $case->getTable(),
|
||||
'ORDER' => ['date_creation DESC'],
|
||||
'LIMIT' => 5
|
||||
]);
|
||||
|
||||
if (count($cases) > 0) {
|
||||
echo '<table class="tab_cadre_fixehov">';
|
||||
echo '<tr>';
|
||||
echo '<th>' . __('Name') . '</th>';
|
||||
echo '<th>' . __('Status') . '</th>';
|
||||
echo '<th>' . __('Severity', 'soc') . '</th>';
|
||||
echo '<th>' . __('Creation date') . '</th>';
|
||||
echo '</tr>';
|
||||
|
||||
foreach ($cases as $data) {
|
||||
echo '<tr class="tab_bg_1">';
|
||||
echo '<td><a href="case.form.php?id=' . $data['id'] . '">' . $data['name'] . '</a></td>';
|
||||
echo '<td><span class="soc-status soc-status-' . $data['status'] . '">' . $case->getStatusOptions()[$data['status']] . '</span></td>';
|
||||
echo '<td><span class="soc-severity soc-severity-' . $data['severity'] . '">' . $case->getSeverityOptions()[$data['severity']] . '</span></td>';
|
||||
echo '<td>' . Html::convDateTime($data['date_creation']) . '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
echo '</table>';
|
||||
} else {
|
||||
echo '<p>' . __('No cases found.', 'soc') . '</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="center" style="margin-top: 20px;">
|
||||
<a href="case.php" class="submit"><?php echo __('View all cases', 'soc'); ?></a>
|
||||
<a href="case.form.php" class="submit"><?php echo __('Create new case', 'soc'); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
Html::footer();
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
include ("../../../inc/includes.php");
|
||||
|
||||
// Check for AJAX request
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
|
||||
// Check if plugin is activated
|
||||
if (!Plugin::isPluginActive("soc")) {
|
||||
http_response_code(404);
|
||||
die(json_encode(['error' => 'Plugin not active']));
|
||||
}
|
||||
|
||||
Session::checkLoginUser();
|
||||
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
http_response_code(400);
|
||||
die(json_encode(['error' => 'Invalid ID']));
|
||||
}
|
||||
|
||||
$case_id = intval($_GET['id']);
|
||||
|
||||
// Check permission
|
||||
if (!Session::haveRight("plugin_soc_case", READ)) {
|
||||
http_response_code(403);
|
||||
die(json_encode(['error' => 'Permission denied']));
|
||||
}
|
||||
|
||||
$case = new PluginSocCase();
|
||||
|
||||
if (!$case->getFromDB($case_id)) {
|
||||
http_response_code(404);
|
||||
die(json_encode(['error' => 'Case not found']));
|
||||
}
|
||||
|
||||
// Get timeline events
|
||||
$timeline = [];
|
||||
|
||||
// Get case creation
|
||||
$timeline[] = [
|
||||
'date' => $case->fields['date_creation'],
|
||||
'type' => 'creation',
|
||||
'content' => __('Case created', 'soc')
|
||||
];
|
||||
|
||||
// Get case updates from history
|
||||
$log = new Log();
|
||||
$logs = $log->getHistoryData($case, 0, 0, ['date_mod' => 'DESC']);
|
||||
|
||||
foreach ($logs as $entry) {
|
||||
$timeline[] = [
|
||||
'date' => $entry['date_mod'],
|
||||
'type' => 'update',
|
||||
'content' => sprintf(
|
||||
__('%s updated %s from %s to %s', 'soc'),
|
||||
$entry['user_name'],
|
||||
$entry['field'],
|
||||
$entry['old_value'],
|
||||
$entry['new_value']
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
// Get related tickets
|
||||
$case_ticket = new PluginSocCaseTicket();
|
||||
$tickets = PluginSocCaseTicket::getTicketsForCase($case_id);
|
||||
|
||||
foreach ($tickets as $ticket_data) {
|
||||
$timeline[] = [
|
||||
'date' => $ticket_data['date_creation'],
|
||||
'type' => 'ticket',
|
||||
'content' => sprintf(
|
||||
__('Ticket %s created: %s', 'soc'),
|
||||
$ticket_data['id'],
|
||||
$ticket_data['name']
|
||||
),
|
||||
'ticket_id' => $ticket_data['id']
|
||||
];
|
||||
}
|
||||
|
||||
// Get related changes
|
||||
$case_change = new PluginSocCaseChange();
|
||||
$changes = PluginSocCaseChange::getChangesForCase($case_id);
|
||||
|
||||
foreach ($changes as $change_data) {
|
||||
$timeline[] = [
|
||||
'date' => $change_data['date_creation'],
|
||||
'type' => 'change',
|
||||
'content' => sprintf(
|
||||
__('Change %s created: %s', 'soc'),
|
||||
$change_data['id'],
|
||||
$change_data['name']
|
||||
),
|
||||
'change_id' => $change_data['id']
|
||||
];
|
||||
}
|
||||
|
||||
// Sort timeline by date (newest first)
|
||||
usort($timeline, function($a, $b) {
|
||||
return strtotime($b['date']) - strtotime($a['date']);
|
||||
});
|
||||
|
||||
echo json_encode(['timeline' => $timeline]);
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* SOC Case Class
|
||||
*/
|
||||
class PluginSocCase extends CommonDBTM {
|
||||
|
||||
static $rightname = 'plugin_soc_case';
|
||||
|
||||
// Severity levels
|
||||
const SEVERITY_CRITICAL = 'critical';
|
||||
const SEVERITY_HIGH = 'high';
|
||||
const SEVERITY_MEDIUM = 'medium';
|
||||
const SEVERITY_LOW = 'low';
|
||||
|
||||
// Case statuses
|
||||
const STATUS_NEW = 'new';
|
||||
const STATUS_ASSIGNED = 'assigned';
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_RESOLVED = 'resolved';
|
||||
const STATUS_CLOSED = 'closed';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
static function getTypeName($nb = 0) {
|
||||
return _n('SOC Case', 'SOC Cases', $nb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define tabs to display
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
function defineTabs($options = []) {
|
||||
$tabs = [];
|
||||
$tabs[1] = __('Main');
|
||||
$tabs[2] = __('Related Tickets');
|
||||
$tabs[3] = __('Related Changes');
|
||||
$tabs[4] = __('Timeline');
|
||||
$tabs[5] = __('Documents');
|
||||
$tabs[6] = __('Notes');
|
||||
$tabs[7] = __('History');
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get severity options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static function getSeverityOptions() {
|
||||
return [
|
||||
self::SEVERITY_CRITICAL => __('Critical', 'soc'),
|
||||
self::SEVERITY_HIGH => __('High', 'soc'),
|
||||
self::SEVERITY_MEDIUM => __('Medium', 'soc'),
|
||||
self::SEVERITY_MEDIUM => __('Low', 'soc')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static function getStatusOptions() {
|
||||
return [
|
||||
self::STATUS_NEW => __('New', 'soc'),
|
||||
self::STATUS_ASSIGNED => __('Assigned', 'soc'),
|
||||
self::STATUS_IN_PROGRESS => __('In Progress', 'soc'),
|
||||
self::STATUS_PENDING => __('Pending', 'soc'),
|
||||
self::STATUS_RESOLVED => __('Resolved', 'soc'),
|
||||
self::STATUS_CLOSED => __('Closed', 'soc')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form
|
||||
*
|
||||
* @param integer $ID
|
||||
* @param array $options
|
||||
* @return boolean
|
||||
*/
|
||||
function showForm($ID, $options = []) {
|
||||
global $CFG_GLPI;
|
||||
|
||||
$this->initForm($ID, $options);
|
||||
$this->showFormHeader($options);
|
||||
|
||||
echo "<tr class='tab_bg_1'>";
|
||||
|
||||
echo "<td>" . __('Name') . "</td>";
|
||||
echo "<td>";
|
||||
echo Html::input('name', ['value' => $this->fields['name']]);
|
||||
echo "</td>";
|
||||
|
||||
echo "<td>" . __('Status') . "</td>";
|
||||
echo "<td>";
|
||||
Dropdown::showFromArray('status', self::getStatusOptions(), ['value' => $this->fields['status']]);
|
||||
echo "</td>";
|
||||
|
||||
echo "</tr>";
|
||||
|
||||
echo "<tr class='tab_bg_1'>";
|
||||
|
||||
echo "<td>" . __('Severity', 'soc') . "</td>";
|
||||
echo "<td>";
|
||||
Dropdown::showFromArray('severity', self::getSeverityOptions(), ['value' => $this->fields['severity']]);
|
||||
echo "</td>";
|
||||
|
||||
echo "<td>" . __('Technician') . "</td>";
|
||||
echo "<td>";
|
||||
User::dropdown(['name' => 'users_id_tech',
|
||||
'value' => $this->fields['users_id_tech'],
|
||||
'entity' => $this->fields['entities_id'],
|
||||
'right' => 'interface']);
|
||||
echo "</td>";
|
||||
|
||||
echo "</tr>";
|
||||
|
||||
echo "<tr class='tab_bg_1'>";
|
||||
echo "<td>" . __('Description') . "</td>";
|
||||
echo "<td colspan='3'>";
|
||||
Html::textarea([
|
||||
'name' => 'description',
|
||||
'value' => $this->fields['description'],
|
||||
'cols' => 125,
|
||||
'rows' => 5
|
||||
]);
|
||||
echo "</td>";
|
||||
echo "</tr>";
|
||||
|
||||
$this->showFormButtons($options);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function rawSearchOptions() {
|
||||
$tab = [];
|
||||
|
||||
$tab[] = [
|
||||
'id' => 'common',
|
||||
'name' => self::getTypeName(2)
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '1',
|
||||
'table' => $this->getTable(),
|
||||
'field' => 'name',
|
||||
'name' => __('Name'),
|
||||
'datatype' => 'itemlink',
|
||||
'massiveaction' => false
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '2',
|
||||
'table' => $this->getTable(),
|
||||
'field' => 'id',
|
||||
'name' => __('ID'),
|
||||
'massiveaction' => false,
|
||||
'datatype' => 'number'
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '3',
|
||||
'table' => $this->getTable(),
|
||||
'field' => 'severity',
|
||||
'name' => __('Severity', 'soc'),
|
||||
'datatype' => 'specific',
|
||||
'searchtype' => ['equals', 'notequals']
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '4',
|
||||
'table' => $this->getTable(),
|
||||
'field' => 'status',
|
||||
'name' => __('Status'),
|
||||
'datatype' => 'specific',
|
||||
'searchtype' => ['equals', 'notequals']
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '5',
|
||||
'table' => $this->getTable(),
|
||||
'field' => 'date_creation',
|
||||
'name' => __('Creation date'),
|
||||
'datatype' => 'datetime',
|
||||
'massiveaction' => false
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '6',
|
||||
'table' => $this->getTable(),
|
||||
'field' => 'date_mod',
|
||||
'name' => __('Last update'),
|
||||
'datatype' => 'datetime',
|
||||
'massiveaction' => false
|
||||
];
|
||||
|
||||
$tab[] = [
|
||||
'id' => '7',
|
||||
'table' => 'glpi_users',
|
||||
'field' => 'name',
|
||||
'linkfield' => 'users_id_tech',
|
||||
'name' => __('Technician'),
|
||||
'datatype' => 'dropdown'
|
||||
];
|
||||
|
||||
return $tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ticket from this case
|
||||
*
|
||||
* @param array $input
|
||||
* @return integer|boolean
|
||||
*/
|
||||
function createTicket($input) {
|
||||
$ticket = new Ticket();
|
||||
|
||||
$ticket_input = [
|
||||
'name' => sprintf(__('[SOC Case #%s] %s', 'soc'), $this->fields['id'], $this->fields['name']),
|
||||
'content' => $this->fields['description'],
|
||||
'entities_id' => $this->fields['entities_id'],
|
||||
'urgency' => self::mapSeverityToUrgency($this->fields['severity']),
|
||||
'users_id_recipient' => Session::getLoginUserID()
|
||||
];
|
||||
|
||||
$tickets_id = $ticket->add($ticket_input);
|
||||
if ($tickets_id) {
|
||||
$this->addTicket($tickets_id);
|
||||
return $tickets_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a change from this case
|
||||
*
|
||||
* @param array $input
|
||||
* @return integer|boolean
|
||||
*/
|
||||
function createChange($input) {
|
||||
$change = new Change();
|
||||
|
||||
$change_input = [
|
||||
'name' => sprintf(__('[SOC Case #%s] %s', 'soc'), $this->fields['id'], $this->fields['name']),
|
||||
'content' => $this->fields['description'],
|
||||
'entities_id' => $this->fields['entities_id'],
|
||||
'urgency' => self::mapSeverityToUrgency($this->fields['severity']),
|
||||
'users_id_recipient' => Session::getLoginUserID()
|
||||
];
|
||||
|
||||
$changes_id = $change->add($change_input);
|
||||
if ($changes_id) {
|
||||
$this->addChange($changes_id);
|
||||
return $changes_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map SOC severity to GLPI urgency
|
||||
*
|
||||
* @param string $severity
|
||||
* @return integer
|
||||
*/
|
||||
static function mapSeverityToUrgency($severity) {
|
||||
switch ($severity) {
|
||||
case self::SEVERITY_CRITICAL:
|
||||
return 5; // Very high
|
||||
case self::SEVERITY_HIGH:
|
||||
return 4; // High
|
||||
case self::SEVERITY_MEDIUM:
|
||||
return 3; // Medium
|
||||
case self::SEVERITY_LOW:
|
||||
return 2; // Low
|
||||
default:
|
||||
return 3; // Medium by default
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a ticket to this case
|
||||
*
|
||||
* @param integer $tickets_id
|
||||
* @return boolean
|
||||
*/
|
||||
function addTicket($tickets_id) {
|
||||
global $DB;
|
||||
|
||||
$case_ticket = new PluginSocCaseTicket();
|
||||
return $case_ticket->add([
|
||||
'plugin_soc_cases_id' => $this->fields['id'],
|
||||
'tickets_id' => $tickets_id,
|
||||
'date_creation' => $_SESSION["glpi_currenttime"]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a change to this case
|
||||
*
|
||||
* @param integer $changes_id
|
||||
* @return boolean
|
||||
*/
|
||||
function addChange($changes_id) {
|
||||
global $DB;
|
||||
|
||||
$case_change = new PluginSocCaseChange();
|
||||
return $case_change->add([
|
||||
'plugin_soc_cases_id' => $this->fields['id'],
|
||||
'changes_id' => $changes_id,
|
||||
'date_creation' => $_SESSION["glpi_currenttime"]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* SOC Case-Change relation class
|
||||
*/
|
||||
class PluginSocCaseChange extends CommonDBRelation {
|
||||
|
||||
// From CommonDBRelation
|
||||
static public $itemtype_1 = 'PluginSocCase';
|
||||
static public $items_id_1 = 'plugin_soc_cases_id';
|
||||
static public $itemtype_2 = 'Change';
|
||||
static public $items_id_2 = 'changes_id';
|
||||
|
||||
/**
|
||||
* Get changes for a case
|
||||
*
|
||||
* @param integer $cases_id
|
||||
* @return DBmysqlIterator
|
||||
*/
|
||||
static function getChangesForCase($cases_id) {
|
||||
global $DB;
|
||||
|
||||
$iterator = $DB->request([
|
||||
'SELECT' => [
|
||||
'glpi_changes.*',
|
||||
'glpi_plugin_soc_case_changes.id AS link_id'
|
||||
],
|
||||
'FROM' => 'glpi_plugin_soc_case_changes',
|
||||
'LEFT JOIN' => [
|
||||
'glpi_changes' => [
|
||||
'FKEY' => [
|
||||
'glpi_plugin_soc_case_changes' => 'changes_id',
|
||||
'glpi_changes' => 'id'
|
||||
]
|
||||
]
|
||||
],
|
||||
'WHERE' => [
|
||||
'glpi_plugin_soc_case_changes.plugin_soc_cases_id' => $cases_id
|
||||
],
|
||||
'ORDER' => [
|
||||
'glpi_changes.date_creation DESC'
|
||||
]
|
||||
]);
|
||||
|
||||
return $iterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cases for a change
|
||||
*
|
||||
* @param integer $changes_id
|
||||
* @return DBmysqlIterator
|
||||
*/
|
||||
static function getCasesForChange($changes_id) {
|
||||
global $DB;
|
||||
|
||||
$iterator = $DB->request([
|
||||
'SELECT' => [
|
||||
'glpi_plugin_soc_cases.*',
|
||||
'glpi_plugin_soc_case_changes.id AS link_id'
|
||||
],
|
||||
'FROM' => 'glpi_plugin_soc_case_changes',
|
||||
'LEFT JOIN' => [
|
||||
'glpi_plugin_soc_cases' => [
|
||||
'FKEY' => [
|
||||
'glpi_plugin_soc_case_changes' => 'plugin_soc_cases_id',
|
||||
'glpi_plugin_soc_cases' => 'id'
|
||||
]
|
||||
]
|
||||
],
|
||||
'WHERE' => [
|
||||
'glpi_plugin_soc_case_changes.changes_id' => $changes_id
|
||||
],
|
||||
'ORDER' => [
|
||||
'glpi_plugin_soc_cases.date_creation DESC'
|
||||
]
|
||||
]);
|
||||
|
||||
return $iterator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* SOC Case-Ticket relation class
|
||||
*/
|
||||
class PluginSocCaseTicket extends CommonDBRelation {
|
||||
|
||||
// From CommonDBRelation
|
||||
static public $itemtype_1 = 'PluginSocCase';
|
||||
static public $items_id_1 = 'plugin_soc_cases_id';
|
||||
static public $itemtype_2 = 'Ticket';
|
||||
static public $items_id_2 = 'tickets_id';
|
||||
|
||||
/**
|
||||
* Get tickets for a case
|
||||
*
|
||||
* @param integer $cases_id
|
||||
* @return DBmysqlIterator
|
||||
*/
|
||||
static function getTicketsForCase($cases_id) {
|
||||
global $DB;
|
||||
|
||||
$iterator = $DB->request([
|
||||
'SELECT' => [
|
||||
'glpi_tickets.*',
|
||||
'glpi_plugin_soc_case_tickets.id AS link_id'
|
||||
],
|
||||
'FROM' => 'glpi_plugin_soc_case_tickets',
|
||||
'LEFT JOIN' => [
|
||||
'glpi_tickets' => [
|
||||
'FKEY' => [
|
||||
'glpi_plugin_soc_case_tickets' => 'tickets_id',
|
||||
'glpi_tickets' => 'id'
|
||||
]
|
||||
]
|
||||
],
|
||||
'WHERE' => [
|
||||
'glpi_plugin_soc_case_tickets.plugin_soc_cases_id' => $cases_id
|
||||
],
|
||||
'ORDER' => [
|
||||
'glpi_tickets.date_creation DESC'
|
||||
]
|
||||
]);
|
||||
|
||||
return $iterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cases for a ticket
|
||||
*
|
||||
* @param integer $tickets_id
|
||||
* @return DBmysqlIterator
|
||||
*/
|
||||
static function getCasesForTicket($tickets_id) {
|
||||
global $DB;
|
||||
|
||||
$iterator = $DB->request([
|
||||
'SELECT' => [
|
||||
'glpi_plugin_soc_cases.*',
|
||||
'glpi_plugin_soc_case_tickets.id AS link_id'
|
||||
],
|
||||
'FROM' => 'glpi_plugin_soc_case_tickets',
|
||||
'LEFT JOIN' => [
|
||||
'glpi_plugin_soc_cases' => [
|
||||
'FKEY' => [
|
||||
'glpi_plugin_soc_case_tickets' => 'plugin_soc_cases_id',
|
||||
'glpi_plugin_soc_cases' => 'id'
|
||||
]
|
||||
]
|
||||
],
|
||||
'WHERE' => [
|
||||
'glpi_plugin_soc_case_tickets.tickets_id' => $tickets_id
|
||||
],
|
||||
'ORDER' => [
|
||||
'glpi_plugin_soc_cases.date_creation DESC'
|
||||
]
|
||||
]);
|
||||
|
||||
return $iterator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* SOC Profile class
|
||||
*/
|
||||
class PluginSocProfile extends Profile {
|
||||
|
||||
static $rightname = "profile";
|
||||
|
||||
/**
|
||||
* @param string $right
|
||||
* @return integer
|
||||
*/
|
||||
function getRight($right) {
|
||||
return $this->fields[$right];
|
||||
}
|
||||
|
||||
/**
|
||||
* Init profiles
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
static function initProfile() {
|
||||
global $DB;
|
||||
|
||||
$profile = new self();
|
||||
|
||||
// Add new rights in glpi_profilerights table
|
||||
foreach ($profile->getAllRights() as $right) {
|
||||
if (!countElementsInTable("glpi_profilerights", ['name' => $right['field']])) {
|
||||
ProfileRight::addProfileRights([$right['field']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all rights
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static function getAllRights() {
|
||||
$rights = [
|
||||
[
|
||||
'field' => 'plugin_soc_case',
|
||||
'name' => __('SOC Case', 'soc'),
|
||||
'rights' => [
|
||||
CREATE => __('Create'),
|
||||
READ => __('Read'),
|
||||
UPDATE => __('Update'),
|
||||
DELETE => __('Delete'),
|
||||
PURGE => __('Purge')
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
return $rights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create first access for super-admin user
|
||||
*
|
||||
* @param integer $ID
|
||||
* @return void
|
||||
*/
|
||||
static function createFirstAccess($ID) {
|
||||
$profile = new self();
|
||||
|
||||
foreach ($profile->getAllRights() as $right) {
|
||||
self::addDefaultProfileInfos($ID, [$right['field'] => ALLSTANDARDRIGHT]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show profile form
|
||||
*
|
||||
* @param integer $profiles_id
|
||||
* @param boolean $openform
|
||||
* @param boolean $closeform
|
||||
* @return void
|
||||
*/
|
||||
function showForm($profiles_id = 0, $openform = true, $closeform = true) {
|
||||
|
||||
echo "<div class='firstbloc'>";
|
||||
|
||||
if (($canedit = Session::haveRightsOr(self::$rightname, [CREATE, UPDATE, DELETE, PURGE]))
|
||||
&& $openform) {
|
||||
$profile = new Profile();
|
||||
echo "<form method='post' action='".$profile->getFormURL()."'>";
|
||||
}
|
||||
|
||||
$profile = new Profile();
|
||||
$profile->getFromDB($profiles_id);
|
||||
|
||||
$rights = $this->getAllRights();
|
||||
$profile->displayRightsChoiceMatrix($rights, ['canedit' => $canedit,
|
||||
'default_class' => 'tab_bg_2',
|
||||
'title' => __('General')]);
|
||||
|
||||
if ($canedit && $closeform) {
|
||||
echo "<div class='center'>";
|
||||
echo Html::hidden('id', ['value' => $profiles_id]);
|
||||
echo Html::submit(_sx('button', 'Save'), ['name' => 'update']);
|
||||
echo "</div>\n";
|
||||
Html::closeForm();
|
||||
}
|
||||
|
||||
echo "</div>";
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GLPI SOC Case Management Plugin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* GLPI SOC Case Management Plugin JavaScript
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize SOC plugin components
|
||||
initializeSocPlugin();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize SOC plugin components
|
||||
*/
|
||||
function initializeSocPlugin() {
|
||||
// Initialize case creation form
|
||||
initializeCaseForm();
|
||||
|
||||
// Initialize timeline
|
||||
initializeTimeline();
|
||||
|
||||
// Initialize KPI dashboard
|
||||
initializeKpiDashboard();
|
||||
|
||||
// Initialize related items tabs
|
||||
initializeRelatedItemsTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize case creation/edit form
|
||||
*/
|
||||
function initializeCaseForm() {
|
||||
const severityField = document.querySelector('select[name="severity"]');
|
||||
|
||||
if (severityField) {
|
||||
// Update visual indicators when severity changes
|
||||
severityField.addEventListener('change', function() {
|
||||
updateSeverityIndicator(this.value);
|
||||
});
|
||||
|
||||
// Initialize with current value
|
||||
updateSeverityIndicator(severityField.value);
|
||||
}
|
||||
|
||||
// Handle case creation button
|
||||
const createCaseButton = document.querySelector('.js-soc-create-case');
|
||||
if (createCaseButton) {
|
||||
createCaseButton.addEventListener('click', function(e) {
|
||||
// Show creation form if not visible
|
||||
const caseForm = document.querySelector('.js-soc-case-form');
|
||||
if (caseForm && caseForm.classList.contains('d-none')) {
|
||||
e.preventDefault();
|
||||
caseForm.classList.remove('d-none');
|
||||
this.textContent = 'Cancel';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update severity visual indicator
|
||||
* @param {string} severity - Severity value
|
||||
*/
|
||||
function updateSeverityIndicator(severity) {
|
||||
const indicator = document.querySelector('.js-soc-severity-indicator');
|
||||
|
||||
if (!indicator) return;
|
||||
|
||||
// Remove existing classes
|
||||
indicator.classList.remove(
|
||||
'soc-severity-critical',
|
||||
'soc-severity-high',
|
||||
'soc-severity-medium',
|
||||
'soc-severity-low'
|
||||
);
|
||||
|
||||
// Add appropriate class
|
||||
indicator.classList.add(`soc-severity-${severity}`);
|
||||
|
||||
// Update text
|
||||
const severityText = {
|
||||
'critical': 'Critical',
|
||||
'high': 'High',
|
||||
'medium': 'Medium',
|
||||
'low': 'Low'
|
||||
};
|
||||
|
||||
indicator.textContent = severityText[severity] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize timeline visualization
|
||||
*/
|
||||
function initializeTimeline() {
|
||||
// Animate timeline items when they enter viewport
|
||||
const timelineItems = document.querySelectorAll('.soc-timeline-item');
|
||||
|
||||
if (timelineItems.length === 0) return;
|
||||
|
||||
// Simple animation for timeline items
|
||||
timelineItems.forEach(function(item, index) {
|
||||
// Stagger the animation
|
||||
setTimeout(function() {
|
||||
item.style.opacity = '1';
|
||||
item.style.transform = 'translateY(0)';
|
||||
}, index * 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize KPI dashboard
|
||||
*/
|
||||
function initializeKpiDashboard() {
|
||||
const kpiCards = document.querySelectorAll('.soc-kpi-card');
|
||||
|
||||
if (kpiCards.length === 0) return;
|
||||
|
||||
// Add animation to KPI numbers
|
||||
kpiCards.forEach(function(card) {
|
||||
const valueEl = card.querySelector('.soc-kpi-value');
|
||||
|
||||
if (valueEl) {
|
||||
const finalValue = parseInt(valueEl.getAttribute('data-value'), 10);
|
||||
let currentValue = 0;
|
||||
|
||||
// Simple animation
|
||||
const interval = setInterval(function() {
|
||||
currentValue += Math.ceil(finalValue / 20);
|
||||
|
||||
if (currentValue >= finalValue) {
|
||||
clearInterval(interval);
|
||||
currentValue = finalValue;
|
||||
}
|
||||
|
||||
valueEl.textContent = currentValue;
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize related items tabs (tickets, changes)
|
||||
*/
|
||||
function initializeRelatedItemsTabs() {
|
||||
const tabLinks = document.querySelectorAll('.js-soc-tab');
|
||||
|
||||
if (tabLinks.length === 0) return;
|
||||
|
||||
tabLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove active class from all tabs
|
||||
tabLinks.forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked tab
|
||||
this.classList.add('active');
|
||||
|
||||
// Hide all tab contents
|
||||
const tabContents = document.querySelectorAll('.js-soc-tab-content');
|
||||
tabContents.forEach(content => content.classList.add('d-none'));
|
||||
|
||||
// Show the target tab content
|
||||
const targetId = this.getAttribute('data-target');
|
||||
const targetContent = document.getElementById(targetId);
|
||||
|
||||
if (targetContent) {
|
||||
targetContent.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ticket from case
|
||||
* @param {number} caseId - Case ID
|
||||
*/
|
||||
function createTicketFromCase(caseId) {
|
||||
// Show confirmation dialog
|
||||
if (confirm('Create a new ticket from this case?')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = 'case.form.php';
|
||||
|
||||
const caseIdInput = document.createElement('input');
|
||||
caseIdInput.type = 'hidden';
|
||||
caseIdInput.name = 'plugin_soc_cases_id';
|
||||
caseIdInput.value = caseId;
|
||||
|
||||
const submitInput = document.createElement('input');
|
||||
submitInput.type = 'hidden';
|
||||
submitInput.name = 'add_ticket';
|
||||
submitInput.value = '1';
|
||||
|
||||
form.appendChild(caseIdInput);
|
||||
form.appendChild(submitInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create change from case
|
||||
* @param {number} caseId - Case ID
|
||||
*/
|
||||
function createChangeFromCase(caseId) {
|
||||
// Show confirmation dialog
|
||||
if (confirm('Create a new change from this case?')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = 'case.form.php';
|
||||
|
||||
const caseIdInput = document.createElement('input');
|
||||
caseIdInput.type = 'hidden';
|
||||
caseIdInput.name = 'plugin_soc_cases_id';
|
||||
caseIdInput.value = caseId;
|
||||
|
||||
const submitInput = document.createElement('input');
|
||||
submitInput.type = 'hidden';
|
||||
submitInput.name = 'add_change';
|
||||
submitInput.value = '1';
|
||||
|
||||
form.appendChild(caseIdInput);
|
||||
form.appendChild(submitInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
Generated
+4051
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/*
|
||||
* @version 1.0.0
|
||||
* @license GPL-3.0+
|
||||
* @brief GLPI SOC Case Management Plugin
|
||||
* @copyright 2025 Your Organization
|
||||
*/
|
||||
|
||||
define('PLUGIN_SOC_VERSION', '1.0.0');
|
||||
define('PLUGIN_SOC_MIN_GLPI', '10.0.0');
|
||||
define('PLUGIN_SOC_MAX_GLPI', '10.1.0');
|
||||
|
||||
/**
|
||||
* Plugin description
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function plugin_version_soc() {
|
||||
return [
|
||||
'name' => 'SOC Case Management',
|
||||
'version' => PLUGIN_SOC_VERSION,
|
||||
'author' => 'Your Organization',
|
||||
'license' => 'GPL-3.0+',
|
||||
'homepage' => 'https://yourorganization.com',
|
||||
'requirements' => [
|
||||
'glpi' => [
|
||||
'min' => PLUGIN_SOC_MIN_GLPI,
|
||||
'max' => PLUGIN_SOC_MAX_GLPI,
|
||||
],
|
||||
'php' => [
|
||||
'min' => '7.4.0',
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check plugin prerequisites before installation
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function plugin_soc_check_prerequisites() {
|
||||
if (version_compare(GLPI_VERSION, PLUGIN_SOC_MIN_GLPI, 'lt') || version_compare(GLPI_VERSION, PLUGIN_SOC_MAX_GLPI, 'gt')) {
|
||||
echo "This plugin requires GLPI >= " . PLUGIN_SOC_MIN_GLPI . " and < " . PLUGIN_SOC_MAX_GLPI;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plugin configuration is compatible with current GLPI status
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function plugin_soc_check_config() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin initialization
|
||||
*
|
||||
* @global array $PLUGIN_HOOKS
|
||||
* @return void
|
||||
*/
|
||||
function plugin_init_soc() {
|
||||
global $PLUGIN_HOOKS;
|
||||
|
||||
$PLUGIN_HOOKS['csrf_compliant']['soc'] = true;
|
||||
$PLUGIN_HOOKS['menu_toadd']['soc'] = ['management' => 'PluginSocCase'];
|
||||
$PLUGIN_HOOKS['javascript']['soc'] = ['/plugins/soc/js/soc.js'];
|
||||
$PLUGIN_HOOKS['add_css']['soc'] = ['/plugins/soc/css/soc.css'];
|
||||
|
||||
if (Session::haveRight('plugin_soc_case', READ)) {
|
||||
$PLUGIN_HOOKS['menu_toadd']['soc'] = ['management' => 'PluginSocCase'];
|
||||
}
|
||||
|
||||
// Add a tab to Changes
|
||||
if (Session::haveRight('change', READ)) {
|
||||
Plugin::registerClass('PluginSocCase', [
|
||||
'addtabtypes' => ['Change']
|
||||
]);
|
||||
}
|
||||
|
||||
// Add a tab to Tickets
|
||||
if (Session::haveRight('ticket', READ)) {
|
||||
Plugin::registerClass('PluginSocCase', [
|
||||
'addtabtypes' => ['Ticket']
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Init the hooks of the plugin
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function plugin_soc_install() {
|
||||
global $DB;
|
||||
|
||||
if (!$DB->tableExists('glpi_plugin_soc_cases')) {
|
||||
$query = "CREATE TABLE `glpi_plugin_soc_cases` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
||||
`entities_id` int(11) NOT NULL DEFAULT '0',
|
||||
`is_recursive` tinyint(1) NOT NULL DEFAULT '0',
|
||||
`severity` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
||||
`status` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
|
||||
`date_creation` timestamp NULL DEFAULT NULL,
|
||||
`date_mod` timestamp NULL DEFAULT NULL,
|
||||
`description` text COLLATE utf8_unicode_ci,
|
||||
`users_id_tech` int(11) NOT NULL DEFAULT '0',
|
||||
`groups_id_tech` int(11) NOT NULL DEFAULT '0',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `name` (`name`),
|
||||
KEY `entities_id` (`entities_id`),
|
||||
KEY `is_recursive` (`is_recursive`),
|
||||
KEY `severity` (`severity`),
|
||||
KEY `status` (`status`),
|
||||
KEY `users_id_tech` (`users_id_tech`),
|
||||
KEY `groups_id_tech` (`groups_id_tech`),
|
||||
KEY `is_deleted` (`is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
|
||||
$DB->query($query) or die("Error creating glpi_plugin_soc_cases table " . $DB->error());
|
||||
}
|
||||
|
||||
if (!$DB->tableExists('glpi_plugin_soc_case_tickets')) {
|
||||
$query = "CREATE TABLE `glpi_plugin_soc_case_tickets` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`plugin_soc_cases_id` int(11) NOT NULL DEFAULT '0',
|
||||
`tickets_id` int(11) NOT NULL DEFAULT '0',
|
||||
`date_creation` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unicity` (`plugin_soc_cases_id`,`tickets_id`),
|
||||
KEY `tickets_id` (`tickets_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
|
||||
$DB->query($query) or die("Error creating glpi_plugin_soc_case_tickets table" . $DB->error());
|
||||
}
|
||||
|
||||
if (!$DB->tableExists('glpi_plugin_soc_case_changes')) {
|
||||
$query = "CREATE TABLE `glpi_plugin_soc_case_changes` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`plugin_soc_cases_id` int(11) NOT NULL DEFAULT '0',
|
||||
`changes_id` int(11) NOT NULL DEFAULT '0',
|
||||
`date_creation` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unicity` (`plugin_soc_cases_id`,`changes_id`),
|
||||
KEY `changes_id` (`changes_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
|
||||
$DB->query($query) or die("Error creating glpi_plugin_soc_case_changes table" . $DB->error());
|
||||
}
|
||||
|
||||
// Create profiles rights
|
||||
PluginSocProfile::initProfile();
|
||||
PluginSocProfile::createFirstAccess($_SESSION['glpiactiveprofile']['id']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall the plugin
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function plugin_soc_uninstall() {
|
||||
global $DB;
|
||||
|
||||
// Delete plugin tables
|
||||
$tables = [
|
||||
'glpi_plugin_soc_cases',
|
||||
'glpi_plugin_soc_case_tickets',
|
||||
'glpi_plugin_soc_case_changes',
|
||||
'glpi_plugin_soc_profiles'
|
||||
];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$query = "DROP TABLE IF EXISTS `$table`";
|
||||
$DB->query($query) or die("Error dropping $table table");
|
||||
}
|
||||
|
||||
// Delete plugin rights from profiles table
|
||||
$query = "DELETE FROM `glpi_profilerights` WHERE `name` LIKE 'plugin_soc_%'";
|
||||
$DB->query($query) or die("Error deleting plugin_soc rights");
|
||||
|
||||
// Delete plugin display preferences
|
||||
$query = "DELETE FROM `glpi_displaypreferences` WHERE `itemtype` LIKE 'PluginSoc%'";
|
||||
$DB->query($query) or die("Error deleting plugin_soc display preferences");
|
||||
|
||||
return true;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<p>Start prompting (or editing) to see magic happen :)</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user