mirror of
https://github.com/tips-of-mine/GLPI-Plugin-SOC-Case-Management.git
synced 2025-06-27 13:18:42 +02:00
Start repository
This commit is contained in:
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"template": "bolt-vite-react-ts"
|
||||
}
|
5
.bolt/prompt
Normal file
5
.bolt/prompt
Normal file
@ -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
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -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
|
43
README.md
Normal file
43
README.md
Normal file
@ -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
css/soc.css
Normal file
205
css/soc.css
Normal file
@ -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%;
|
||||
}
|
||||
}
|
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
65
front/case.form.php
Normal file
65
front/case.form.php
Normal file
@ -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();
|
||||
}
|
15
front/case.php
Normal file
15
front/case.php
Normal file
@ -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();
|
88
front/dashboard.php
Normal file
88
front/dashboard.php
Normal file
@ -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();
|
102
front/timeline.ajax.php
Normal file
102
front/timeline.ajax.php
Normal file
@ -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]);
|
325
inc/case.class.php
Normal file
325
inc/case.class.php
Normal file
@ -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"]
|
||||
]);
|
||||
}
|
||||
}
|
80
inc/casechange.class.php
Normal file
80
inc/casechange.class.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
80
inc/caseticket.class.php
Normal file
80
inc/caseticket.class.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
108
inc/profile.class.php
Normal file
108
inc/profile.class.php
Normal file
@ -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
index.html
Normal file
13
index.html
Normal file
@ -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>
|
227
js/soc.js
Normal file
227
js/soc.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
4051
package-lock.json
generated
Normal file
4051
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@ -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
plugin.php
Normal file
90
plugin.php
Normal file
@ -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']
|
||||
]);
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
100
setup.php
Normal file
100
setup.php
Normal file
@ -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
src/App.tsx
Normal file
11
src/App.tsx
Normal file
@ -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;
|
3
src/index.css
Normal file
3
src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -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>
|
||||
);
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
24
tsconfig.app.json
Normal file
24
tsconfig.app.json
Normal file
@ -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"]
|
||||
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal file
@ -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"]
|
||||
}
|
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@ -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