1419 lines
53 KiB
PHP
1419 lines
53 KiB
PHP
<?php include('include/headscript.php'); ?>
|
|
|
|
<?php
|
|
/*
|
|
* TRFgo Dashboard
|
|
* Draggable user widgets with persistent layout.
|
|
*/
|
|
|
|
function e($value)
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
function jsonResponse(array $payload): void
|
|
{
|
|
header('Content-Type: application/json');
|
|
echo json_encode($payload);
|
|
exit;
|
|
}
|
|
|
|
function getScalar(PDO $db, string $sql, array $params = [])
|
|
{
|
|
try {
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchColumn();
|
|
} catch (Throwable $e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function getRows(PDO $db, string $sql, array $params = [])
|
|
{
|
|
try {
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
$dashboardPage = 'dashboard';
|
|
|
|
$availableWidgets = [
|
|
'kpi_companies',
|
|
'kpi_brands',
|
|
'kpi_departments',
|
|
'kpi_users',
|
|
'chart_structure',
|
|
'setup_progress',
|
|
'quick_actions',
|
|
'recent_companies',
|
|
];
|
|
|
|
$defaultLayout = [
|
|
'kpi_companies',
|
|
'kpi_brands',
|
|
'kpi_departments',
|
|
'kpi_users',
|
|
'chart_structure',
|
|
'setup_progress',
|
|
'quick_actions',
|
|
'recent_companies',
|
|
];
|
|
|
|
/*
|
|
* AJAX: save/reset dashboard layout.
|
|
*/
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|
try {
|
|
if ($_POST['action'] === 'save_dashboard_layout') {
|
|
$layoutRaw = $_POST['layout'] ?? '[]';
|
|
$layout = json_decode($layoutRaw, true);
|
|
|
|
if (!is_array($layout)) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'message' => 'Invalid dashboard layout.'
|
|
]);
|
|
}
|
|
|
|
$cleanLayout = [];
|
|
|
|
foreach ($layout as $widgetKey) {
|
|
if (in_array($widgetKey, $availableWidgets, true) && !in_array($widgetKey, $cleanLayout, true)) {
|
|
$cleanLayout[] = $widgetKey;
|
|
}
|
|
}
|
|
|
|
foreach ($defaultLayout as $widgetKey) {
|
|
if (!in_array($widgetKey, $cleanLayout, true)) {
|
|
$cleanLayout[] = $widgetKey;
|
|
}
|
|
}
|
|
|
|
$layoutJson = json_encode($cleanLayout);
|
|
|
|
$stmt = $db->prepare("
|
|
INSERT INTO user_dashboard_layouts (
|
|
iduser,
|
|
page,
|
|
layout_json,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (
|
|
:iduser,
|
|
:page,
|
|
:layout_json,
|
|
NOW(),
|
|
NOW()
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
layout_json = VALUES(layout_json),
|
|
updated_at = NOW()
|
|
");
|
|
|
|
$stmt->execute([
|
|
':iduser' => $iduserlogin,
|
|
':page' => $dashboardPage,
|
|
':layout_json' => $layoutJson,
|
|
]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'message' => 'Dashboard layout saved.'
|
|
]);
|
|
}
|
|
|
|
if ($_POST['action'] === 'reset_dashboard_layout') {
|
|
$stmt = $db->prepare("
|
|
DELETE FROM user_dashboard_layouts
|
|
WHERE iduser = :iduser
|
|
AND page = :page
|
|
");
|
|
|
|
$stmt->execute([
|
|
':iduser' => $iduserlogin,
|
|
':page' => $dashboardPage,
|
|
]);
|
|
|
|
jsonResponse([
|
|
'success' => true,
|
|
'message' => 'Dashboard layout reset.'
|
|
]);
|
|
}
|
|
|
|
jsonResponse([
|
|
'success' => false,
|
|
'message' => 'Unknown action.'
|
|
]);
|
|
} catch (Throwable $e) {
|
|
jsonResponse([
|
|
'success' => false,
|
|
'message' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Dashboard counters.
|
|
*/
|
|
$totalCompanies = (int) getScalar($db, "SELECT COUNT(*) FROM companies");
|
|
$activeCompanies = (int) getScalar($db, "SELECT COUNT(*) FROM companies WHERE status = 'active'");
|
|
$totalBrands = (int) getScalar($db, "SELECT COUNT(*) FROM brands");
|
|
$activeBrands = (int) getScalar($db, "SELECT COUNT(*) FROM brands WHERE status = 'active'");
|
|
$totalDepartments = (int) getScalar($db, "SELECT COUNT(*) FROM departments");
|
|
$activeDepartments = (int) getScalar($db, "SELECT COUNT(*) FROM departments WHERE status = 'active'");
|
|
$totalCompanyUsers = (int) getScalar($db, "SELECT COUNT(*) FROM company_users");
|
|
$activeCompanyUsers = (int) getScalar($db, "SELECT COUNT(*) FROM company_users WHERE status = 'active'");
|
|
|
|
/*
|
|
* Recent companies.
|
|
*/
|
|
$recentCompanies = getRows($db, "
|
|
SELECT
|
|
c.idcompany,
|
|
c.company_name,
|
|
c.legal_name,
|
|
c.status,
|
|
c.created_at,
|
|
COUNT(DISTINCT b.idbrand) AS brand_count,
|
|
COUNT(DISTINCT d.iddepartment) AS department_count,
|
|
COUNT(DISTINCT cu.idcompanyuser) AS user_count
|
|
FROM companies c
|
|
LEFT JOIN brands b ON b.idcompany = c.idcompany
|
|
LEFT JOIN departments d ON d.idcompany = c.idcompany
|
|
LEFT JOIN company_users cu ON cu.idcompany = c.idcompany
|
|
GROUP BY c.idcompany, c.company_name, c.legal_name, c.status, c.created_at
|
|
ORDER BY c.created_at DESC, c.idcompany DESC
|
|
LIMIT 8
|
|
");
|
|
|
|
/*
|
|
* Chart data.
|
|
*/
|
|
$companyDistribution = getRows($db, "
|
|
SELECT
|
|
c.company_name,
|
|
COUNT(DISTINCT b.idbrand) AS brands,
|
|
COUNT(DISTINCT d.iddepartment) AS departments,
|
|
COUNT(DISTINCT cu.idcompanyuser) AS users
|
|
FROM companies c
|
|
LEFT JOIN brands b ON b.idcompany = c.idcompany
|
|
LEFT JOIN departments d ON d.idcompany = c.idcompany
|
|
LEFT JOIN company_users cu ON cu.idcompany = c.idcompany
|
|
GROUP BY c.idcompany, c.company_name
|
|
ORDER BY c.company_name ASC
|
|
LIMIT 10
|
|
");
|
|
|
|
$chartCompanyLabels = [];
|
|
$chartBrands = [];
|
|
$chartDepartments = [];
|
|
$chartUsers = [];
|
|
|
|
foreach ($companyDistribution as $row) {
|
|
$chartCompanyLabels[] = $row['company_name'];
|
|
$chartBrands[] = (int) $row['brands'];
|
|
$chartDepartments[] = (int) $row['departments'];
|
|
$chartUsers[] = (int) $row['users'];
|
|
}
|
|
|
|
/*
|
|
* Setup progress.
|
|
*/
|
|
$setupItems = [
|
|
[
|
|
'label' => 'Companies',
|
|
'completed' => $totalCompanies > 0,
|
|
'icon' => 'bx bx-buildings',
|
|
],
|
|
[
|
|
'label' => 'Brands',
|
|
'completed' => $totalBrands > 0,
|
|
'icon' => 'bx bx-purchase-tag-alt',
|
|
],
|
|
[
|
|
'label' => 'Departments',
|
|
'completed' => $totalDepartments > 0,
|
|
'icon' => 'bx bx-sitemap',
|
|
],
|
|
[
|
|
'label' => 'User access',
|
|
'completed' => $totalCompanyUsers > 0,
|
|
'icon' => 'bx bx-user-check',
|
|
],
|
|
];
|
|
|
|
$completedSetupItems = count(array_filter($setupItems, function ($item) {
|
|
return $item['completed'];
|
|
}));
|
|
|
|
$setupProgress = count($setupItems) > 0
|
|
? round(($completedSetupItems / count($setupItems)) * 100)
|
|
: 0;
|
|
|
|
/*
|
|
* Load user dashboard layout.
|
|
*/
|
|
$userLayout = $defaultLayout;
|
|
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT layout_json
|
|
FROM user_dashboard_layouts
|
|
WHERE iduser = :iduser
|
|
AND page = :page
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([
|
|
':iduser' => $iduserlogin,
|
|
':page' => $dashboardPage,
|
|
]);
|
|
|
|
$savedLayoutJson = $stmt->fetchColumn();
|
|
|
|
if ($savedLayoutJson) {
|
|
$savedLayout = json_decode($savedLayoutJson, true);
|
|
|
|
if (is_array($savedLayout)) {
|
|
$cleanLayout = [];
|
|
|
|
foreach ($savedLayout as $widgetKey) {
|
|
if (in_array($widgetKey, $availableWidgets, true) && !in_array($widgetKey, $cleanLayout, true)) {
|
|
$cleanLayout[] = $widgetKey;
|
|
}
|
|
}
|
|
|
|
foreach ($defaultLayout as $widgetKey) {
|
|
if (!in_array($widgetKey, $cleanLayout, true)) {
|
|
$cleanLayout[] = $widgetKey;
|
|
}
|
|
}
|
|
|
|
$userLayout = $cleanLayout;
|
|
}
|
|
}
|
|
} catch (Throwable $e) {
|
|
$userLayout = $defaultLayout;
|
|
}
|
|
|
|
$pageTitle = 'TRFgo Dashboard';
|
|
?>
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
|
|
|
<?php include('cssinclude.php'); ?>
|
|
|
|
<title><?= e($pageTitle); ?> - <?= isset($titlewebsite) ? e($titlewebsite) : 'TRFgo'; ?></title>
|
|
|
|
<style>
|
|
:root {
|
|
--trfgo-primary: #2563eb;
|
|
--trfgo-primary-dark: #1e40af;
|
|
--trfgo-secondary: #0f172a;
|
|
--trfgo-soft-blue: #eff6ff;
|
|
--trfgo-soft-green: #ecfdf5;
|
|
--trfgo-soft-orange: #fff7ed;
|
|
--trfgo-soft-purple: #f5f3ff;
|
|
--trfgo-border: #e5e7eb;
|
|
--trfgo-muted: #64748b;
|
|
}
|
|
|
|
.dashboard-hero {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 20px;
|
|
background:
|
|
radial-gradient(circle at top right, rgba(59, 130, 246, 0.35), transparent 32%),
|
|
linear-gradient(135deg, #0f172a 0%, #1d4ed8 58%, #38bdf8 100%);
|
|
color: #fff;
|
|
padding: 28px;
|
|
min-height: 210px;
|
|
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22);
|
|
}
|
|
|
|
.dashboard-hero h1,
|
|
.dashboard-hero h2,
|
|
.dashboard-hero h3,
|
|
.dashboard-hero h4,
|
|
.dashboard-hero h5,
|
|
.dashboard-hero h6 {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.dashboard-hero::before {
|
|
content: "";
|
|
position: absolute;
|
|
width: 260px;
|
|
height: 260px;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.09);
|
|
right: -70px;
|
|
bottom: -100px;
|
|
}
|
|
|
|
.dashboard-hero::after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 130px;
|
|
height: 130px;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.08);
|
|
right: 210px;
|
|
top: -50px;
|
|
}
|
|
|
|
.dashboard-hero-content {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.dashboard-eyebrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 12px;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.16);
|
|
color: rgba(255, 255, 255, 0.92);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
letter-spacing: -0.03em;
|
|
margin-bottom: 8px;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
max-width: 780px;
|
|
color: rgba(255, 255, 255, 0.82);
|
|
font-size: 15px;
|
|
line-height: 1.6;
|
|
margin-bottom: 22px;
|
|
}
|
|
|
|
.hero-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn-hero-light {
|
|
background: #fff;
|
|
color: #1d4ed8;
|
|
border: 0;
|
|
border-radius: 12px;
|
|
padding: 10px 16px;
|
|
font-weight: 700;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.16);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-hero-outline {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
color: #fff;
|
|
border: 1px solid rgba(255, 255, 255, 0.32);
|
|
border-radius: 12px;
|
|
padding: 10px 16px;
|
|
font-weight: 700;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.dashboard-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.dashboard-toolbar-title {
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
font-size: 18px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.dashboard-toolbar-subtitle {
|
|
color: var(--trfgo-muted);
|
|
font-size: 13px;
|
|
margin: 0;
|
|
}
|
|
|
|
.dashboard-widget {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.dashboard-widget.sortable-ghost {
|
|
opacity: 0.35;
|
|
}
|
|
|
|
.dashboard-widget.sortable-chosen .widget-card {
|
|
box-shadow: 0 22px 60px rgba(37, 99, 235, 0.20);
|
|
}
|
|
|
|
.widget-card {
|
|
border: 0;
|
|
border-radius: 18px;
|
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
|
overflow: hidden;
|
|
height: 100%;
|
|
transition: all 0.18s ease;
|
|
}
|
|
|
|
.widget-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.10);
|
|
}
|
|
|
|
.widget-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.widget-title {
|
|
color: #0f172a;
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.widget-subtitle {
|
|
color: var(--trfgo-muted);
|
|
font-size: 13px;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.drag-handle {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 11px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f8fafc;
|
|
border: 1px solid #e5e7eb;
|
|
color: #64748b;
|
|
cursor: grab;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.drag-handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 14px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 25px;
|
|
}
|
|
|
|
.stat-icon.blue {
|
|
background: var(--trfgo-soft-blue);
|
|
color: #2563eb;
|
|
}
|
|
|
|
.stat-icon.green {
|
|
background: var(--trfgo-soft-green);
|
|
color: #059669;
|
|
}
|
|
|
|
.stat-icon.orange {
|
|
background: var(--trfgo-soft-orange);
|
|
color: #ea580c;
|
|
}
|
|
|
|
.stat-icon.purple {
|
|
background: var(--trfgo-soft-purple);
|
|
color: #7c3aed;
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--trfgo-muted);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-value {
|
|
color: #0f172a;
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.stat-caption {
|
|
color: var(--trfgo-muted);
|
|
font-size: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.quick-action {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 15px;
|
|
border: 1px solid var(--trfgo-border);
|
|
border-radius: 16px;
|
|
color: #0f172a;
|
|
text-decoration: none;
|
|
transition: all 0.18s ease;
|
|
background: #fff;
|
|
}
|
|
|
|
.quick-action:hover {
|
|
color: #1d4ed8;
|
|
border-color: rgba(37, 99, 235, 0.25);
|
|
background: #f8fbff;
|
|
transform: translateX(3px);
|
|
}
|
|
|
|
.quick-action-icon {
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 13px;
|
|
background: #eff6ff;
|
|
color: #2563eb;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 22px;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.quick-action-title {
|
|
font-weight: 800;
|
|
font-size: 14px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.quick-action-text {
|
|
color: var(--trfgo-muted);
|
|
font-size: 12px;
|
|
margin: 0;
|
|
}
|
|
|
|
.setup-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 13px 0;
|
|
border-bottom: 1px solid #eef2f7;
|
|
}
|
|
|
|
.setup-item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.setup-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.setup-icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 12px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f1f5f9;
|
|
color: #475569;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.setup-icon.completed {
|
|
background: #ecfdf5;
|
|
color: #059669;
|
|
}
|
|
|
|
.setup-label {
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.badge-soft-success {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
padding: 6px 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.badge-soft-warning {
|
|
background: #ffedd5;
|
|
color: #9a3412;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
padding: 6px 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.badge-soft-muted {
|
|
background: #f1f5f9;
|
|
color: #475569;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
padding: 6px 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.company-table th {
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.company-table td {
|
|
vertical-align: middle;
|
|
border-color: #eef2f7;
|
|
}
|
|
|
|
.company-name {
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.company-legal {
|
|
font-size: 12px;
|
|
color: var(--trfgo-muted);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 38px 20px;
|
|
color: var(--trfgo-muted);
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 46px;
|
|
color: #cbd5e1;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.progress {
|
|
height: 9px;
|
|
border-radius: 999px;
|
|
background: #e2e8f0;
|
|
}
|
|
|
|
.progress-bar {
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.metric-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: #f8fafc;
|
|
border: 1px solid #e5e7eb;
|
|
color: #475569;
|
|
border-radius: 999px;
|
|
padding: 5px 9px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.dashboard-hero {
|
|
padding: 22px;
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 25px;
|
|
}
|
|
|
|
.hero-actions {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn-hero-light,
|
|
.btn-hero-outline {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="wrapper">
|
|
<?php include('include/navbar.php'); ?>
|
|
<?php include('include/topbar.php'); ?>
|
|
|
|
<div class="page-wrapper">
|
|
<div class="page-content">
|
|
|
|
<div class="dashboard-hero mb-4">
|
|
<div class="dashboard-hero-content">
|
|
<div class="dashboard-eyebrow">
|
|
<i class="bx bx-lab"></i>
|
|
TRFgo Platform
|
|
</div>
|
|
|
|
<h1 class="dashboard-title">
|
|
Welcome, <?= e(trim($nameuser . ' ' . $surnameuser)); ?>
|
|
</h1>
|
|
|
|
<p class="dashboard-subtitle">
|
|
Manage companies, brands, departments and user access from one central workspace.
|
|
TRFgo is the customer-side platform for digital test request forms, sample tracking
|
|
and laboratory result exchange.
|
|
</p>
|
|
|
|
<div class="hero-actions">
|
|
<a href="companies.php" class="btn-hero-light">
|
|
<i class="bx bx-buildings"></i>
|
|
Manage Companies
|
|
</a>
|
|
|
|
<a href="brands.php" class="btn-hero-outline">
|
|
<i class="bx bx-purchase-tag-alt"></i>
|
|
Manage Brands
|
|
</a>
|
|
|
|
<a href="departments.php" class="btn-hero-outline">
|
|
<i class="bx bx-sitemap"></i>
|
|
Manage Departments
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-toolbar">
|
|
<div>
|
|
<div class="dashboard-toolbar-title">Your Dashboard</div>
|
|
<p class="dashboard-toolbar-subtitle">
|
|
Drag widgets using the handle. The layout is saved for your user account.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-sm btn-light" id="btnResetLayout">
|
|
<i class="bx bx-reset"></i>
|
|
Reset layout
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-sm btn-primary" id="btnSaveLayout">
|
|
<i class="bx bx-save"></i>
|
|
Save layout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row dashboard-grid" id="dashboardGrid">
|
|
|
|
<?php foreach ($userLayout as $widgetKey): ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_companies'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_companies">
|
|
<div class="card widget-card">
|
|
<div class="card-body">
|
|
<div class="widget-header mb-2">
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Companies</div>
|
|
<div class="stat-value"><?= e($totalCompanies); ?></div>
|
|
<div class="stat-caption"><?= e($activeCompanies); ?> active companies</div>
|
|
</div>
|
|
<div class="stat-icon blue">
|
|
<i class="bx bx-buildings"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_brands'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_brands">
|
|
<div class="card widget-card">
|
|
<div class="card-body">
|
|
<div class="widget-header mb-2">
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Brands</div>
|
|
<div class="stat-value"><?= e($totalBrands); ?></div>
|
|
<div class="stat-caption"><?= e($activeBrands); ?> active brands</div>
|
|
</div>
|
|
<div class="stat-icon green">
|
|
<i class="bx bx-purchase-tag-alt"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_departments'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_departments">
|
|
<div class="card widget-card">
|
|
<div class="card-body">
|
|
<div class="widget-header mb-2">
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Departments</div>
|
|
<div class="stat-value"><?= e($totalDepartments); ?></div>
|
|
<div class="stat-caption"><?= e($activeDepartments); ?> active departments</div>
|
|
</div>
|
|
<div class="stat-icon orange">
|
|
<i class="bx bx-sitemap"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_users'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_users">
|
|
<div class="card widget-card">
|
|
<div class="card-body">
|
|
<div class="widget-header mb-2">
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Company Users</div>
|
|
<div class="stat-value"><?= e($totalCompanyUsers); ?></div>
|
|
<div class="stat-caption"><?= e($activeCompanyUsers); ?> active assignments</div>
|
|
</div>
|
|
<div class="stat-icon purple">
|
|
<i class="bx bx-user-check"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'chart_structure'): ?>
|
|
<div class="col-12 col-xl-8 dashboard-widget" data-widget="chart_structure">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Company Structure Overview</h6>
|
|
<p class="widget-subtitle">Brands, departments and user assignments by company</p>
|
|
</div>
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<?php if (count($companyDistribution) > 0): ?>
|
|
<div id="companyStructureChart" style="min-height: 330px;"></div>
|
|
<?php else: ?>
|
|
<div class="empty-state">
|
|
<i class="bx bx-bar-chart-alt-2"></i>
|
|
<h6>No chart data available</h6>
|
|
<p class="mb-0">Create your first company, brand or department to populate this chart.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'setup_progress'): ?>
|
|
<div class="col-12 col-xl-4 dashboard-widget" data-widget="setup_progress">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">TRFgo Setup</h6>
|
|
<p class="widget-subtitle">Initial configuration progress</p>
|
|
</div>
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<strong><?= e($setupProgress); ?>% completed</strong>
|
|
<span class="badge-soft-muted"><?= e($completedSetupItems); ?>/<?= e(count($setupItems)); ?></span>
|
|
</div>
|
|
|
|
<div class="progress mb-3">
|
|
<div class="progress-bar" role="progressbar" style="width: <?= e($setupProgress); ?>%;" aria-valuenow="<?= e($setupProgress); ?>" aria-valuemin="0" aria-valuemax="100"></div>
|
|
</div>
|
|
|
|
<?php foreach ($setupItems as $item): ?>
|
|
<div class="setup-item">
|
|
<div class="setup-left">
|
|
<div class="setup-icon <?= $item['completed'] ? 'completed' : ''; ?>">
|
|
<i class="<?= e($item['icon']); ?>"></i>
|
|
</div>
|
|
<div class="setup-label"><?= e($item['label']); ?></div>
|
|
</div>
|
|
|
|
<?php if ($item['completed']): ?>
|
|
<span class="badge-soft-success">Ready</span>
|
|
<?php else: ?>
|
|
<span class="badge-soft-warning">Missing</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'quick_actions'): ?>
|
|
<div class="col-12 col-xl-4 dashboard-widget" data-widget="quick_actions">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Quick Actions</h6>
|
|
<p class="widget-subtitle">Start configuring TRFgo</p>
|
|
</div>
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<a href="companies.php" class="quick-action">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-plus-circle"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Create company</span>
|
|
<span class="quick-action-text">Add a customer or laboratory organization</span>
|
|
</span>
|
|
</a>
|
|
|
|
<a href="brands.php" class="quick-action">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-purchase-tag-alt"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Create brand</span>
|
|
<span class="quick-action-text">Organize companies by brand or division</span>
|
|
</span>
|
|
</a>
|
|
|
|
<a href="departments.php" class="quick-action">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-sitemap"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Create department</span>
|
|
<span class="quick-action-text">Define departments such as Bags, Shoes, Apparel</span>
|
|
</span>
|
|
</a>
|
|
|
|
<a href="company-users.php" class="quick-action">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-user-plus"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Assign users</span>
|
|
<span class="quick-action-text">Connect Vanguard users to TRFgo companies</span>
|
|
</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'recent_companies'): ?>
|
|
<div class="col-12 col-xl-8 dashboard-widget" data-widget="recent_companies">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Recent Companies</h6>
|
|
<p class="widget-subtitle">Latest configured organizations in TRFgo</p>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<a href="companies.php" class="btn btn-sm btn-primary">
|
|
<i class="bx bx-list-ul"></i>
|
|
View all
|
|
</a>
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<?php if (count($recentCompanies) > 0): ?>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle company-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Company</th>
|
|
<th>Status</th>
|
|
<th class="text-center">Brands</th>
|
|
<th class="text-center">Departments</th>
|
|
<th class="text-center">Users</th>
|
|
<th>Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($recentCompanies as $company): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="company-name"><?= e($company['company_name']); ?></div>
|
|
<?php if (!empty($company['legal_name'])): ?>
|
|
<div class="company-legal"><?= e($company['legal_name']); ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if ($company['status'] === 'active'): ?>
|
|
<span class="badge-soft-success">Active</span>
|
|
<?php elseif ($company['status'] === 'suspended'): ?>
|
|
<span class="badge-soft-warning">Suspended</span>
|
|
<?php else: ?>
|
|
<span class="badge-soft-muted">Inactive</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="metric-pill">
|
|
<i class="bx bx-purchase-tag-alt"></i>
|
|
<?= e($company['brand_count']); ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="metric-pill">
|
|
<i class="bx bx-sitemap"></i>
|
|
<?= e($company['department_count']); ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="metric-pill">
|
|
<i class="bx bx-user"></i>
|
|
<?= e($company['user_count']); ?>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<?= !empty($company['created_at']) ? e(date('d/m/Y', strtotime($company['created_at']))) : '-'; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="empty-state">
|
|
<i class="bx bx-buildings"></i>
|
|
<h6>No companies configured yet</h6>
|
|
<p>Create your first company to start using TRFgo.</p>
|
|
<a href="companies.php" class="btn btn-primary">
|
|
<i class="bx bx-plus-circle"></i>
|
|
Add first company
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php endforeach; ?>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overlay toggle-icon"></div>
|
|
|
|
<a href="javaScript:;" class="back-to-top">
|
|
<i class='bx bxs-up-arrow-alt'></i>
|
|
</a>
|
|
|
|
<?php include('include/footer.php'); ?>
|
|
</div>
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
|
|
<script src="assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
const chartLabels = <?= json_encode($chartCompanyLabels, JSON_UNESCAPED_UNICODE); ?>;
|
|
const chartBrands = <?= json_encode($chartBrands, JSON_NUMERIC_CHECK); ?>;
|
|
const chartDepartments = <?= json_encode($chartDepartments, JSON_NUMERIC_CHECK); ?>;
|
|
const chartUsers = <?= json_encode($chartUsers, JSON_NUMERIC_CHECK); ?>;
|
|
|
|
let chartInstance = null;
|
|
|
|
function renderCompanyChart() {
|
|
const chartElement = document.querySelector("#companyStructureChart");
|
|
|
|
if (!chartElement) {
|
|
return;
|
|
}
|
|
|
|
if (typeof ApexCharts === "undefined") {
|
|
chartElement.innerHTML =
|
|
'<div class="empty-state"><i class="bx bx-error-circle"></i><h6>ApexCharts not loaded</h6><p class="mb-0">Check apexcharts.min.js path.</p></div>';
|
|
return;
|
|
}
|
|
|
|
if (chartInstance !== null) {
|
|
chartInstance.destroy();
|
|
}
|
|
|
|
const options = {
|
|
series: [{
|
|
name: "Brands",
|
|
data: chartBrands
|
|
},
|
|
{
|
|
name: "Departments",
|
|
data: chartDepartments
|
|
},
|
|
{
|
|
name: "Users",
|
|
data: chartUsers
|
|
}
|
|
],
|
|
chart: {
|
|
type: "bar",
|
|
height: 330,
|
|
toolbar: {
|
|
show: false
|
|
},
|
|
fontFamily: "inherit"
|
|
},
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: false,
|
|
columnWidth: "45%",
|
|
borderRadius: 6
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
stroke: {
|
|
show: true,
|
|
width: 2,
|
|
colors: ["transparent"]
|
|
},
|
|
xaxis: {
|
|
categories: chartLabels,
|
|
labels: {
|
|
rotate: -35,
|
|
trim: true
|
|
}
|
|
},
|
|
yaxis: {
|
|
min: 0,
|
|
forceNiceScale: true
|
|
},
|
|
fill: {
|
|
opacity: 1
|
|
},
|
|
legend: {
|
|
position: "top",
|
|
horizontalAlign: "right"
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value) {
|
|
return value;
|
|
}
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: "#eef2f7"
|
|
}
|
|
};
|
|
|
|
chartInstance = new ApexCharts(chartElement, options);
|
|
chartInstance.render();
|
|
}
|
|
|
|
renderCompanyChart();
|
|
|
|
function getCurrentLayout() {
|
|
const layout = [];
|
|
|
|
document.querySelectorAll("#dashboardGrid .dashboard-widget").forEach(function(widget) {
|
|
const widgetKey = widget.getAttribute("data-widget");
|
|
|
|
if (widgetKey) {
|
|
layout.push(widgetKey);
|
|
}
|
|
});
|
|
|
|
return layout;
|
|
}
|
|
|
|
function showMessage(type, message) {
|
|
if (typeof Swal !== "undefined") {
|
|
Swal.fire({
|
|
icon: type,
|
|
title: type === "success" ? "Done" : "Attention",
|
|
text: message,
|
|
confirmButtonColor: "#2563eb"
|
|
});
|
|
return;
|
|
}
|
|
|
|
alert(message);
|
|
}
|
|
|
|
function saveLayout(showSuccess = true) {
|
|
const layout = getCurrentLayout();
|
|
|
|
$.ajax({
|
|
url: window.location.pathname,
|
|
type: "POST",
|
|
dataType: "json",
|
|
data: {
|
|
action: "save_dashboard_layout",
|
|
layout: JSON.stringify(layout)
|
|
},
|
|
success: function(response) {
|
|
if (!response.success) {
|
|
showMessage("error", response.message || "Unable to save dashboard layout.");
|
|
return;
|
|
}
|
|
|
|
if (showSuccess) {
|
|
showMessage("success", response.message || "Dashboard layout saved.");
|
|
}
|
|
},
|
|
error: function() {
|
|
showMessage("error", "Server error while saving dashboard layout.");
|
|
}
|
|
});
|
|
}
|
|
|
|
if (typeof Sortable !== "undefined") {
|
|
const dashboardGrid = document.getElementById("dashboardGrid");
|
|
|
|
Sortable.create(dashboardGrid, {
|
|
animation: 180,
|
|
handle: ".drag-handle",
|
|
draggable: ".dashboard-widget",
|
|
ghostClass: "sortable-ghost",
|
|
chosenClass: "sortable-chosen",
|
|
onEnd: function() {
|
|
setTimeout(function() {
|
|
renderCompanyChart();
|
|
saveLayout(false);
|
|
}, 250);
|
|
}
|
|
});
|
|
} else {
|
|
console.warn("SortableJS not loaded. Dashboard drag and drop disabled.");
|
|
}
|
|
|
|
$("#btnSaveLayout").on("click", function() {
|
|
saveLayout(true);
|
|
});
|
|
|
|
$("#btnResetLayout").on("click", function() {
|
|
const doReset = function() {
|
|
$.ajax({
|
|
url: window.location.pathname,
|
|
type: "POST",
|
|
dataType: "json",
|
|
data: {
|
|
action: "reset_dashboard_layout"
|
|
},
|
|
success: function(response) {
|
|
if (!response.success) {
|
|
showMessage("error", response.message || "Unable to reset dashboard layout.");
|
|
return;
|
|
}
|
|
|
|
if (typeof Swal !== "undefined") {
|
|
Swal.fire({
|
|
icon: "success",
|
|
title: "Done",
|
|
text: response.message || "Dashboard layout reset.",
|
|
confirmButtonColor: "#2563eb"
|
|
}).then(function() {
|
|
window.location.reload();
|
|
});
|
|
return;
|
|
}
|
|
|
|
alert(response.message || "Dashboard layout reset.");
|
|
window.location.reload();
|
|
},
|
|
error: function() {
|
|
showMessage("error", "Server error while resetting dashboard layout.");
|
|
}
|
|
});
|
|
};
|
|
|
|
if (typeof Swal !== "undefined") {
|
|
Swal.fire({
|
|
icon: "question",
|
|
title: "Reset dashboard layout?",
|
|
text: "Your personal widget order will be restored to the default layout.",
|
|
showCancelButton: true,
|
|
confirmButtonColor: "#2563eb",
|
|
cancelButtonColor: "#64748b",
|
|
confirmButtonText: "Yes, reset"
|
|
}).then(function(result) {
|
|
if (result.isConfirmed) {
|
|
doReset();
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (confirm("Reset dashboard layout?")) {
|
|
doReset();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|