692 lines
26 KiB
PHP
692 lines
26 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Dashboard;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Libraries\AuditLogger;
|
|
use App\Models\ChartModel;
|
|
use App\Models\DashboardModel;
|
|
use App\Models\DashboardWidgetModel;
|
|
use App\Models\QueryVariableModel;
|
|
|
|
class DashboardController extends BaseController
|
|
{
|
|
public function index()
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dashboards = (new DashboardModel())->forWorkspace($workspaceId);
|
|
|
|
return view('dashboard/index', [
|
|
'title' => 'Dashboards | Chart-Board',
|
|
'dashboards' => $dashboards,
|
|
]);
|
|
}
|
|
|
|
public function store()
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$userId = (int) $this->session->get('user_id');
|
|
|
|
$rules = [
|
|
'name' => 'required|min_length[2]|max_length[200]',
|
|
'description' => 'permit_empty|max_length[2000]',
|
|
];
|
|
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$name = strip_tags((string) $this->request->getPost('name'));
|
|
$desc = trim((string) $this->request->getPost('description')) ?: null;
|
|
|
|
$model = new DashboardModel();
|
|
$slug = $model->generateUniqueSlug($workspaceId, $name);
|
|
|
|
$id = (int) $model->insert([
|
|
'workspace_id' => $workspaceId,
|
|
'name' => $name,
|
|
'description' => $desc,
|
|
'slug' => $slug,
|
|
'layout_config' => null,
|
|
'filters_config' => null,
|
|
'refresh_interval' => 0,
|
|
'theme' => 'system',
|
|
'is_public' => 0,
|
|
'public_token' => null,
|
|
'public_password' => null,
|
|
'public_expires_at'=> null,
|
|
'is_pinned' => 0,
|
|
'created_by' => $userId,
|
|
], true);
|
|
|
|
AuditLogger::log(
|
|
'dashboard.created',
|
|
'dashboard',
|
|
$id,
|
|
null,
|
|
['name' => $name],
|
|
$workspaceId,
|
|
$userId
|
|
);
|
|
|
|
return redirect()->to('/dashboard/view/' . $id)->with('success', 'Dashboard created. Add widgets and arrange your layout.');
|
|
}
|
|
|
|
public function viewBoard(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dashModel = new DashboardModel();
|
|
$dashboard = $dashModel->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return redirect()->to('/dashboard')->with('error', 'Dashboard not found.');
|
|
}
|
|
|
|
$widgets = (new DashboardWidgetModel())->forDashboard($id);
|
|
$charts = (new ChartModel())->forWorkspace($workspaceId);
|
|
|
|
return view('dashboard/view', [
|
|
'title' => (string) $dashboard['name'] . ' | Chart-Board',
|
|
'dashboard' => $dashboard,
|
|
'widgets' => $widgets,
|
|
'chartsList' => $charts,
|
|
'dashBoot' => [
|
|
'dashboardId' => $id,
|
|
'dashboardName' => (string) ($dashboard['name'] ?? 'dashboard'),
|
|
'workspaceTheme'=> (string) ($dashboard['theme'] ?? 'system'),
|
|
'refreshSec' => max(0, (int) ($dashboard['refresh_interval'] ?? 0)),
|
|
'widgets' => $this->widgetsBootPayload($widgets),
|
|
'charts' => array_map(static fn (array $c) => [
|
|
'id' => (int) $c['id'],
|
|
'name' => (string) $c['name'],
|
|
'type' => (string) ($c['chart_type'] ?? ''),
|
|
], $charts),
|
|
'urls' => [
|
|
'chartData' => rtrim(base_url(), '/') . '/chart/',
|
|
'chartEdit' => rtrim(base_url(), '/') . '/chart/edit/',
|
|
'chartExportBase' => rtrim(base_url(), '/') . '/chart/',
|
|
'dashboardVariables' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/variables',
|
|
'saveLayout' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/layout',
|
|
'addWidget' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/widget',
|
|
'delWidget' => rtrim(base_url(), '/') . '/dashboard/widget/',
|
|
'updateWidget' => rtrim(base_url(), '/') . '/dashboard/widget/',
|
|
'savedQueryVars' => rtrim(base_url(), '/') . '/chart/saved-query/',
|
|
],
|
|
'csrf' => [
|
|
'name' => csrf_token(),
|
|
'hash' => csrf_hash(),
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $widgets
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function widgetsBootPayload(array $widgets): array
|
|
{
|
|
$out = [];
|
|
foreach ($widgets as $w) {
|
|
$cfg = null;
|
|
if (! empty($w['widget_config'])) {
|
|
$d = json_decode((string) $w['widget_config'], true);
|
|
$cfg = is_array($d) ? $d : null;
|
|
}
|
|
$out[] = [
|
|
'id' => (int) $w['id'],
|
|
'chart_id' => isset($w['chart_id']) ? (int) $w['chart_id'] : null,
|
|
'widget_type' => (string) ($w['widget_type'] ?? 'chart'),
|
|
'title' => $w['title'] !== null && $w['title'] !== '' ? (string) $w['title'] : null,
|
|
'grid_x' => (int) ($w['grid_x'] ?? 0),
|
|
'grid_y' => (int) ($w['grid_y'] ?? 0),
|
|
'grid_w' => (int) ($w['grid_w'] ?? 4),
|
|
'grid_h' => (int) ($w['grid_h'] ?? 3),
|
|
'content' => $w['content'] !== null ? (string) $w['content'] : null,
|
|
'widget_config' => $cfg,
|
|
'chart_name' => isset($w['chart_name']) ? (string) $w['chart_name'] : null,
|
|
'chart_type' => isset($w['chart_type']) ? (string) $w['chart_type'] : null,
|
|
'saved_query_id' => isset($w['saved_query_id']) ? (int) $w['saved_query_id'] : null,
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
public function settings(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dashboard = (new DashboardModel())->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return redirect()->to('/dashboard')->with('error', 'Dashboard not found.');
|
|
}
|
|
|
|
return view('dashboard/settings', [
|
|
'title' => 'Dashboard settings | Chart-Board',
|
|
'dashboard' => $dashboard,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Return merged query variable definitions used by dashboard chart widgets.
|
|
*/
|
|
public function variables(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dashboard = (new DashboardModel())->findForWorkspace($id, $workspaceId);
|
|
if (! $dashboard) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Dashboard not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
$widgets = (new DashboardWidgetModel())->forDashboard($id);
|
|
$queryIds = [];
|
|
foreach ($widgets as $w) {
|
|
if (($w['widget_type'] ?? '') !== 'chart') {
|
|
continue;
|
|
}
|
|
$qid = (int) ($w['saved_query_id'] ?? 0);
|
|
if ($qid > 0) {
|
|
$queryIds[$qid] = true;
|
|
}
|
|
}
|
|
$queryIds = array_keys($queryIds);
|
|
if ($queryIds === []) {
|
|
return $this->response->setJSON(['success' => true, 'variables' => []]);
|
|
}
|
|
|
|
$rows = (new QueryVariableModel())
|
|
->whereIn('saved_query_id', $queryIds)
|
|
->where('workspace_id', $workspaceId)
|
|
->orderBy('saved_query_id', 'ASC')
|
|
->orderBy('sort_order', 'ASC')
|
|
->findAll();
|
|
|
|
$merged = [];
|
|
foreach ($rows as $row) {
|
|
$name = (string) ($row['name'] ?? '');
|
|
if ($name === '') {
|
|
continue;
|
|
}
|
|
$options = [];
|
|
$optsRaw = $row['options_json'] ?? null;
|
|
if ($optsRaw !== null && $optsRaw !== '') {
|
|
$decoded = json_decode((string) $optsRaw, true);
|
|
if (is_array($decoded)) {
|
|
$options = $decoded;
|
|
}
|
|
}
|
|
|
|
if (! isset($merged[$name])) {
|
|
$merged[$name] = [
|
|
'name' => $name,
|
|
'label' => (string) ($row['label'] ?? $name),
|
|
'type' => (string) ($row['type'] ?? 'text'),
|
|
'default_value' => (string) ($row['default_value'] ?? ''),
|
|
'is_required' => ! empty($row['is_required']),
|
|
'options' => $options,
|
|
];
|
|
continue;
|
|
}
|
|
|
|
$merged[$name]['is_required'] = $merged[$name]['is_required'] || ! empty($row['is_required']);
|
|
if ($merged[$name]['default_value'] === '' && (string) ($row['default_value'] ?? '') !== '') {
|
|
$merged[$name]['default_value'] = (string) $row['default_value'];
|
|
}
|
|
if (($merged[$name]['label'] ?? '') === '' && (string) ($row['label'] ?? '') !== '') {
|
|
$merged[$name]['label'] = (string) $row['label'];
|
|
}
|
|
if (($merged[$name]['type'] ?? 'text') === 'text' && (string) ($row['type'] ?? 'text') !== 'text') {
|
|
$merged[$name]['type'] = (string) $row['type'];
|
|
}
|
|
if (is_array($options) && $options !== []) {
|
|
$existing = is_array($merged[$name]['options']) ? $merged[$name]['options'] : [];
|
|
foreach ($options as $opt) {
|
|
if (! in_array($opt, $existing, true)) {
|
|
$existing[] = $opt;
|
|
}
|
|
}
|
|
$merged[$name]['options'] = $existing;
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'variables' => array_values($merged),
|
|
]);
|
|
}
|
|
|
|
public function updateSettings(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DashboardModel();
|
|
$dashboard = $model->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return redirect()->to('/dashboard')->with('error', 'Dashboard not found.');
|
|
}
|
|
|
|
$rules = [
|
|
'name' => 'required|min_length[2]|max_length[200]',
|
|
'description' => 'permit_empty|max_length[2000]',
|
|
'theme' => 'required|in_list[light,dark,system]',
|
|
'refresh_interval' => 'permit_empty|integer',
|
|
];
|
|
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$oldSnap = [
|
|
'name' => $dashboard['name'] ?? null,
|
|
'theme' => $dashboard['theme'] ?? null,
|
|
'refresh_interval' => $dashboard['refresh_interval'] ?? null,
|
|
];
|
|
$model->update($id, [
|
|
'name' => strip_tags((string) $this->request->getPost('name')),
|
|
'description' => trim((string) $this->request->getPost('description')) ?: null,
|
|
'theme' => (string) $this->request->getPost('theme'),
|
|
'refresh_interval' => max(0, (int) $this->request->getPost('refresh_interval')),
|
|
]);
|
|
|
|
AuditLogger::log(
|
|
'dashboard.updated',
|
|
'dashboard',
|
|
$id,
|
|
$oldSnap,
|
|
[
|
|
'name' => strip_tags((string) $this->request->getPost('name')),
|
|
'theme' => (string) $this->request->getPost('theme'),
|
|
'refresh_interval' => max(0, (int) $this->request->getPost('refresh_interval')),
|
|
],
|
|
$workspaceId,
|
|
(int) $this->session->get('user_id')
|
|
);
|
|
|
|
return redirect()->back()->with('success', 'Settings saved.');
|
|
}
|
|
|
|
public function delete(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DashboardModel();
|
|
$dashboard = $model->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return redirect()->to('/dashboard')->with('error', 'Dashboard not found.');
|
|
}
|
|
|
|
$confirm = trim((string) $this->request->getPost('confirm_name'));
|
|
if ($confirm !== (string) $dashboard['name']) {
|
|
return redirect()->back()->with('error', 'Type the dashboard name exactly to confirm deletion.');
|
|
}
|
|
|
|
AuditLogger::log(
|
|
'dashboard.deleted',
|
|
'dashboard',
|
|
$id,
|
|
['name' => $dashboard['name'] ?? null],
|
|
null,
|
|
$workspaceId,
|
|
(int) $this->session->get('user_id')
|
|
);
|
|
|
|
$model->delete($id);
|
|
|
|
return redirect()->to('/dashboard')->with('success', 'Dashboard deleted.');
|
|
}
|
|
|
|
public function pinToggle(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DashboardModel();
|
|
$dashboard = $model->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return redirect()->to('/dashboard')->with('error', 'Dashboard not found.');
|
|
}
|
|
|
|
$pinned = (int) ($dashboard['is_pinned'] ?? 0) === 1;
|
|
$model->update($id, ['is_pinned' => $pinned ? 0 : 1]);
|
|
|
|
return redirect()->back()->with('success', 'Pin updated.');
|
|
}
|
|
|
|
/**
|
|
* Batch-update widget grid positions (JSON body or form).
|
|
*/
|
|
public function saveLayout(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dashModel = new DashboardModel();
|
|
$dashboard = $dashModel->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
// Do not use IncomingRequest::getJSON() here: the front-end posts
|
|
// application/x-www-form-urlencoded (csrf + items_json), and getJSON()
|
|
// decodes the *entire* body as JSON, which throws HTTPException.
|
|
$items = null;
|
|
$ct = strtolower($this->request->getHeaderLine('Content-Type'));
|
|
if (str_contains($ct, 'application/json')) {
|
|
$body = $this->request->getBody();
|
|
if ($body !== null && $body !== '') {
|
|
$decoded = json_decode($body, true);
|
|
if (is_array($decoded)) {
|
|
if (isset($decoded['items']) && is_array($decoded['items'])) {
|
|
$items = $decoded['items'];
|
|
} elseif (array_is_list($decoded)) {
|
|
$items = $decoded;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (! is_array($items)) {
|
|
$items = json_decode((string) $this->request->getPost('items_json'), true);
|
|
}
|
|
if (! is_array($items)) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Invalid layout payload.'])->setStatusCode(422);
|
|
}
|
|
|
|
$widgetModel = new DashboardWidgetModel();
|
|
$db = $widgetModel->db;
|
|
$db->transStart();
|
|
|
|
foreach ($items as $row) {
|
|
if (! is_array($row)) {
|
|
continue;
|
|
}
|
|
$wid = (int) ($row['id'] ?? 0);
|
|
if ($wid <= 0) {
|
|
continue;
|
|
}
|
|
$w = $widgetModel->find($wid);
|
|
if (! $w || (int) $w['dashboard_id'] !== $id) {
|
|
continue;
|
|
}
|
|
|
|
$widgetModel->update($wid, [
|
|
'grid_x' => max(0, min(255, (int) ($row['x'] ?? 0))),
|
|
'grid_y' => max(0, min(255, (int) ($row['y'] ?? 0))),
|
|
'grid_w' => max(1, min(12, (int) ($row['w'] ?? 4))),
|
|
'grid_h' => max(1, min(24, (int) ($row['h'] ?? 3))),
|
|
]);
|
|
}
|
|
|
|
// Keep DB row order aligned with visual layout (top-to-bottom, left-to-right) so PHP render matches the grid.
|
|
$posById = [];
|
|
foreach ($items as $row) {
|
|
if (! is_array($row)) {
|
|
continue;
|
|
}
|
|
$wid = (int) ($row['id'] ?? 0);
|
|
if ($wid <= 0) {
|
|
continue;
|
|
}
|
|
$posById[$wid] = [
|
|
'y' => (int) ($row['y'] ?? 0),
|
|
'x' => (int) ($row['x'] ?? 0),
|
|
];
|
|
}
|
|
$allWidgets = $widgetModel->where('dashboard_id', $id)->findAll();
|
|
usort($allWidgets, static function (array $a, array $b) use ($posById): int {
|
|
$ida = (int) $a['id'];
|
|
$idb = (int) $b['id'];
|
|
$pa = $posById[$ida] ?? null;
|
|
$pb = $posById[$idb] ?? null;
|
|
if ($pa !== null && $pb !== null) {
|
|
if ($pa['y'] !== $pb['y']) {
|
|
return $pa['y'] <=> $pb['y'];
|
|
}
|
|
if ($pa['x'] !== $pb['x']) {
|
|
return $pa['x'] <=> $pb['x'];
|
|
}
|
|
|
|
return $ida <=> $idb;
|
|
}
|
|
if ($pa !== null) {
|
|
return -1;
|
|
}
|
|
if ($pb !== null) {
|
|
return 1;
|
|
}
|
|
|
|
$ya = (int) ($a['grid_y'] ?? 0);
|
|
$yb = (int) ($b['grid_y'] ?? 0);
|
|
if ($ya !== $yb) {
|
|
return $ya <=> $yb;
|
|
}
|
|
$xa = (int) ($a['grid_x'] ?? 0);
|
|
$xb = (int) ($b['grid_x'] ?? 0);
|
|
if ($xa !== $xb) {
|
|
return $xa <=> $xb;
|
|
}
|
|
|
|
return $ida <=> $idb;
|
|
});
|
|
$order = 1;
|
|
foreach ($allWidgets as $wRow) {
|
|
$widgetModel->update((int) $wRow['id'], ['sort_order' => $order]);
|
|
$order++;
|
|
}
|
|
|
|
$db->transComplete();
|
|
|
|
if (! $db->transStatus()) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Could not save layout.'])->setStatusCode(500);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'csrf' => ['name' => csrf_token(), 'hash' => csrf_hash()],
|
|
]);
|
|
}
|
|
|
|
public function addWidget(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dashModel = new DashboardModel();
|
|
$dashboard = $dashModel->findForWorkspace($id, $workspaceId);
|
|
|
|
if (! $dashboard) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
$type = (string) $this->request->getPost('widget_type');
|
|
if (! in_array($type, ['chart', 'text', 'image', 'filter_date', 'filter_dropdown'], true)) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Invalid widget type.'])->setStatusCode(422);
|
|
}
|
|
|
|
$chartId = null;
|
|
if ($type === 'chart') {
|
|
$chartId = (int) $this->request->getPost('chart_id');
|
|
$chart = (new ChartModel())->where('workspace_id', $workspaceId)->find($chartId);
|
|
if (! $chart) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Chart not found.'])->setStatusCode(404);
|
|
}
|
|
}
|
|
|
|
$title = trim((string) $this->request->getPost('title')) ?: null;
|
|
$content = trim((string) $this->request->getPost('content')) ?: null;
|
|
$widgetConfig = $this->request->getPost('widget_config');
|
|
|
|
if ($type === 'text' && ($content === null || $content === '')) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Text widgets need content.'])->setStatusCode(422);
|
|
}
|
|
|
|
if ($type === 'image' && ($content === null || $content === '')) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Image widgets need a URL.'])->setStatusCode(422);
|
|
}
|
|
|
|
if ($type === 'image' && ! $this->isSafeImageUrl((string) $content)) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Only http(s) image URLs are allowed.'])->setStatusCode(422);
|
|
}
|
|
|
|
if ($type === 'image') {
|
|
$fit = (string) $this->request->getPost('image_fit');
|
|
if (! in_array($fit, ['cover', 'contain', 'fill', 'scale-down'], true)) {
|
|
$fit = 'cover';
|
|
}
|
|
$widgetConfig = json_encode(['object_fit' => $fit], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
if ($type === 'filter_date') {
|
|
$sv = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $this->request->getPost('filter_start_var')) ?: 'date_from';
|
|
$ev = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $this->request->getPost('filter_end_var')) ?: 'date_to';
|
|
$widgetConfig = json_encode(['start_var' => $sv, 'end_var' => $ev], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
if ($type === 'filter_dropdown') {
|
|
$vn = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $this->request->getPost('filter_var_name')) ?: 'region';
|
|
$optRaw = (string) $this->request->getPost('filter_options');
|
|
$options = array_values(array_filter(array_map('trim', explode(',', $optRaw)), static fn ($s) => $s !== ''));
|
|
$widgetConfig = json_encode(['var_name' => $vn, 'options' => $options], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
$configJson = $this->normalizeWidgetConfig($widgetConfig, $type);
|
|
|
|
$widgetModel = new DashboardWidgetModel();
|
|
$maxRow = $widgetModel->selectMax('sort_order')->where('dashboard_id', $id)->first();
|
|
$maxOrder = (int) ($maxRow['sort_order'] ?? 0);
|
|
|
|
$newId = (int) $widgetModel->insert([
|
|
'dashboard_id' => $id,
|
|
'chart_id' => $chartId,
|
|
'widget_type' => $type,
|
|
'title' => $title,
|
|
'grid_x' => 0,
|
|
'grid_y' => 0,
|
|
'grid_w' => $type === 'chart' ? 6 : 4,
|
|
'grid_h' => $type === 'chart' ? 4 : 2,
|
|
'content' => $type === 'chart' ? null : $content,
|
|
'widget_config' => $configJson,
|
|
'sort_order' => $maxOrder + 1,
|
|
], true);
|
|
|
|
$row = $widgetModel->forDashboard($id);
|
|
$created = null;
|
|
foreach ($row as $r) {
|
|
if ((int) $r['id'] === $newId) {
|
|
$created = $r;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'widget' => $created,
|
|
'csrf' => ['name' => csrf_token(), 'hash' => csrf_hash()],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update markdown content for a text widget (AJAX).
|
|
*/
|
|
public function updateWidget(int $widgetId)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$widgetModel = new DashboardWidgetModel();
|
|
$w = $widgetModel->find($widgetId);
|
|
|
|
if (! $w) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Widget not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
$dashboard = (new DashboardModel())->findForWorkspace((int) $w['dashboard_id'], $workspaceId);
|
|
if (! $dashboard) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
if (($w['widget_type'] ?? '') !== 'text') {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Only text widgets can be updated here.'])->setStatusCode(422);
|
|
}
|
|
|
|
$content = trim((string) $this->request->getPost('content'));
|
|
if ($content === '') {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Content is required.'])->setStatusCode(422);
|
|
}
|
|
|
|
$widgetModel->update($widgetId, [
|
|
'content' => $content,
|
|
]);
|
|
|
|
$fresh = $widgetModel->find($widgetId);
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'widget' => [
|
|
'id' => (int) ($fresh['id'] ?? $widgetId),
|
|
'content' => (string) ($fresh['content'] ?? ''),
|
|
],
|
|
'csrf' => ['name' => csrf_token(), 'hash' => csrf_hash()],
|
|
]);
|
|
}
|
|
|
|
public function removeWidget(int $widgetId)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$widgetModel = new DashboardWidgetModel();
|
|
$w = $widgetModel->find($widgetId);
|
|
|
|
if (! $w) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Widget not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
$dashboard = (new DashboardModel())->findForWorkspace((int) $w['dashboard_id'], $workspaceId);
|
|
if (! $dashboard) {
|
|
return $this->response->setJSON(['success' => false, 'message' => 'Not found.'])->setStatusCode(404);
|
|
}
|
|
|
|
$widgetModel->delete($widgetId);
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'csrf' => ['name' => csrf_token(), 'hash' => csrf_hash()],
|
|
]);
|
|
}
|
|
|
|
private function isSafeImageUrl(string $url): bool
|
|
{
|
|
$url = trim($url);
|
|
if ($url === '') {
|
|
return false;
|
|
}
|
|
if (preg_match('#^https?://#i', $url) !== 1) {
|
|
return false;
|
|
}
|
|
|
|
return filter_var($url, FILTER_VALIDATE_URL) !== false;
|
|
}
|
|
|
|
private function normalizeWidgetConfig(mixed $raw, string $type): ?string
|
|
{
|
|
if ($raw === null || $raw === '') {
|
|
if ($type === 'filter_dropdown') {
|
|
return json_encode(['var_name' => 'filter', 'options' => []], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
if ($type === 'filter_date') {
|
|
return json_encode(['start_var' => 'date_from', 'end_var' => 'date_to'], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
if ($type === 'image') {
|
|
return json_encode(['object_fit' => 'cover'], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (is_string($raw)) {
|
|
$d = json_decode($raw, true);
|
|
|
|
return is_array($d) ? json_encode($d, JSON_UNESCAPED_UNICODE) : null;
|
|
}
|
|
|
|
if (is_array($raw)) {
|
|
return json_encode($raw, JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|