2002 lines
78 KiB
PHP
2002 lines
78 KiB
PHP
<?php include('include/headscript.php'); ?>
|
|
|
|
<?php
|
|
/*
|
|
* TRFgo Customer Dashboard
|
|
* Operational dashboard for company/customer users.
|
|
* Widgets are draggable and layout is persisted per Vanguard user.
|
|
*/
|
|
|
|
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 tableExists(PDO $db, string $tableName): bool
|
|
{
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT COUNT(*)
|
|
FROM information_schema.tables
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = :table_name
|
|
");
|
|
$stmt->execute([':table_name' => $tableName]);
|
|
|
|
return (int) $stmt->fetchColumn() > 0;
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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_customer';
|
|
|
|
/*
|
|
* Widget registry.
|
|
*/
|
|
$availableWidgets = [
|
|
'customer_company_profile',
|
|
'kpi_business_partners',
|
|
'kpi_samples',
|
|
'kpi_sample_parts',
|
|
'kpi_documents',
|
|
'kpi_trf_requests',
|
|
'kpi_pending_requests',
|
|
'kpi_reports_received',
|
|
'master_data_readiness',
|
|
'chart_trf_status',
|
|
'recent_samples',
|
|
'recent_trf_requests',
|
|
'document_repository',
|
|
'quick_actions',
|
|
];
|
|
|
|
$defaultLayout = [
|
|
'customer_company_profile',
|
|
'kpi_business_partners',
|
|
'kpi_samples',
|
|
'kpi_sample_parts',
|
|
'kpi_documents',
|
|
'master_data_readiness',
|
|
'recent_samples',
|
|
'quick_actions',
|
|
'kpi_trf_requests',
|
|
'kpi_pending_requests',
|
|
'kpi_reports_received',
|
|
'chart_trf_status',
|
|
'recent_trf_requests',
|
|
'document_repository',
|
|
];
|
|
|
|
/*
|
|
* Find customer company scope for current user.
|
|
*
|
|
* SaaS:
|
|
* - user can be linked to one or more companies through company_users.
|
|
*
|
|
* On-premise:
|
|
* - if no assignment exists, fallback to TRFGO_DEFAULT_COMPANY_ID or first company.
|
|
*/
|
|
$userCompanies = getRows($db, "
|
|
SELECT DISTINCT
|
|
c.idcompany,
|
|
c.company_name,
|
|
c.legal_name,
|
|
c.status,
|
|
c.email,
|
|
c.phone,
|
|
c.city,
|
|
c.external_code,
|
|
cu.company_role,
|
|
cu.user_scope
|
|
FROM company_users cu
|
|
INNER JOIN companies c ON c.idcompany = cu.idcompany
|
|
WHERE cu.iduser = :iduser
|
|
AND cu.status = 'active'
|
|
ORDER BY c.company_name ASC
|
|
", [
|
|
':iduser' => $iduserlogin,
|
|
]);
|
|
|
|
$selectedCompanyId = 0;
|
|
|
|
if (!empty($_GET['idcompany'])) {
|
|
$selectedCompanyId = (int) $_GET['idcompany'];
|
|
}
|
|
|
|
if ($selectedCompanyId <= 0 && count($userCompanies) > 0) {
|
|
$selectedCompanyId = (int) $userCompanies[0]['idcompany'];
|
|
}
|
|
|
|
/*
|
|
* On-premise fallback.
|
|
*/
|
|
if ($selectedCompanyId <= 0) {
|
|
$defaultCompanyId = isset($_ENV['TRFGO_DEFAULT_COMPANY_ID']) ? (int) $_ENV['TRFGO_DEFAULT_COMPANY_ID'] : 0;
|
|
|
|
if ($defaultCompanyId > 0) {
|
|
$selectedCompanyId = $defaultCompanyId;
|
|
} else {
|
|
$selectedCompanyId = (int) getScalar($db, "
|
|
SELECT idcompany
|
|
FROM companies
|
|
ORDER BY idcompany ASC
|
|
LIMIT 1
|
|
");
|
|
}
|
|
}
|
|
|
|
$selectedCompany = null;
|
|
|
|
if ($selectedCompanyId > 0) {
|
|
$rows = getRows($db, "
|
|
SELECT *
|
|
FROM companies
|
|
WHERE idcompany = :idcompany
|
|
LIMIT 1
|
|
", [
|
|
':idcompany' => $selectedCompanyId,
|
|
]);
|
|
|
|
$selectedCompany = $rows[0] ?? null;
|
|
}
|
|
|
|
$companyParams = [
|
|
':idcompany' => $selectedCompanyId,
|
|
];
|
|
|
|
/*
|
|
* Existing setup data.
|
|
*/
|
|
$totalBrands = $selectedCompanyId > 0
|
|
? (int) getScalar($db, "SELECT COUNT(*) FROM brands WHERE idcompany = :idcompany", $companyParams)
|
|
: 0;
|
|
|
|
$totalDepartments = $selectedCompanyId > 0
|
|
? (int) getScalar($db, "SELECT COUNT(*) FROM departments WHERE idcompany = :idcompany", $companyParams)
|
|
: 0;
|
|
|
|
$totalCompanyUsers = $selectedCompanyId > 0
|
|
? (int) getScalar($db, "SELECT COUNT(*) FROM company_users WHERE idcompany = :idcompany AND status = 'active'", $companyParams)
|
|
: 0;
|
|
|
|
/*
|
|
* Future operational tables.
|
|
* These names are provisional and will be created later with Phinx.
|
|
*/
|
|
$hasSamplesTable = tableExists($db, 'samples');
|
|
$hasTrfRequestsTable = tableExists($db, 'trf_requests');
|
|
$hasLabReportsTable = tableExists($db, 'lab_reports');
|
|
$hasDocumentsTable = tableExists($db, 'documents') || tableExists($db, 'trf_documents');
|
|
$hasBusinessPartnersTable = tableExists($db, 'business_partners');
|
|
$hasSamplePartsTable = tableExists($db, 'sample_parts');
|
|
$hasSamplePhotosTable = tableExists($db, 'sample_photos');
|
|
/*
|
|
* KPI counters.
|
|
*/
|
|
$totalSamples = 0;
|
|
$totalTrfRequests = 0;
|
|
$pendingTrfRequests = 0;
|
|
$reportsReceived = 0;
|
|
$totalDocuments = 0;
|
|
$totalBusinessPartners = 0;
|
|
$activeBusinessPartners = 0;
|
|
$totalSampleParts = 0;
|
|
$totalSamplePhotos = 0;
|
|
$recentSamples = [];
|
|
|
|
if ($selectedCompanyId > 0 && $hasSamplesTable) {
|
|
$totalSamples = (int) getScalar($db, "
|
|
SELECT COUNT(*)
|
|
FROM samples
|
|
WHERE idcompany = :idcompany
|
|
", $companyParams);
|
|
}
|
|
if ($selectedCompanyId > 0 && $hasBusinessPartnersTable) {
|
|
$totalBusinessPartners = (int) getScalar($db, "
|
|
SELECT COUNT(*)
|
|
FROM business_partners
|
|
WHERE idcompany = :idcompany
|
|
", $companyParams);
|
|
|
|
$activeBusinessPartners = (int) getScalar($db, "
|
|
SELECT COUNT(*)
|
|
FROM business_partners
|
|
WHERE idcompany = :idcompany
|
|
AND status = 'active'
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && $hasSamplePartsTable && $hasSamplesTable) {
|
|
$totalSampleParts = (int) getScalar($db, "
|
|
SELECT COUNT(sp.idpart)
|
|
FROM sample_parts sp
|
|
INNER JOIN samples s ON s.idsample = sp.idsample
|
|
WHERE s.idcompany = :idcompany
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && $hasSamplePhotosTable && $hasSamplesTable) {
|
|
$totalSamplePhotos = (int) getScalar($db, "
|
|
SELECT COUNT(sph.idsamplephoto)
|
|
FROM sample_photos sph
|
|
INNER JOIN samples s ON s.idsample = sph.idsample
|
|
WHERE s.idcompany = :idcompany
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && $hasSamplesTable) {
|
|
$recentSamples = getRows($db, "
|
|
SELECT
|
|
s.idsample,
|
|
s.sample_code,
|
|
s.external_sample_id,
|
|
s.article_no,
|
|
s.po_no,
|
|
s.season,
|
|
s.sample_description,
|
|
s.color,
|
|
s.production_stage,
|
|
s.status,
|
|
s.created_at,
|
|
b.brand_name,
|
|
d.department_name,
|
|
bp1.partner_name AS producer_name,
|
|
bp2.partner_name AS supplier_name,
|
|
COUNT(DISTINCT sp.idpart) AS parts_count,
|
|
COUNT(DISTINCT sph.idsamplephoto) AS photos_count,
|
|
COUNT(DISTINCT sd.idsampledocument) AS documents_count
|
|
FROM samples s
|
|
LEFT JOIN brands b ON b.idbrand = s.idbrand
|
|
LEFT JOIN departments d ON d.iddepartment = s.iddepartment
|
|
LEFT JOIN business_partners bp1 ON bp1.idpartner = s.idproducer
|
|
LEFT JOIN business_partners bp2 ON bp2.idpartner = s.idsupplier
|
|
LEFT JOIN sample_parts sp ON sp.idsample = s.idsample
|
|
LEFT JOIN sample_photos sph ON sph.idsample = s.idsample
|
|
LEFT JOIN sample_documents sd ON sd.idsample = s.idsample
|
|
WHERE s.idcompany = :idcompany
|
|
GROUP BY
|
|
s.idsample,
|
|
s.sample_code,
|
|
s.external_sample_id,
|
|
s.article_no,
|
|
s.po_no,
|
|
s.season,
|
|
s.sample_description,
|
|
s.color,
|
|
s.production_stage,
|
|
s.status,
|
|
s.created_at,
|
|
b.brand_name,
|
|
d.department_name,
|
|
bp1.partner_name,
|
|
bp2.partner_name
|
|
ORDER BY s.created_at DESC, s.idsample DESC
|
|
LIMIT 8
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && $hasTrfRequestsTable) {
|
|
$totalTrfRequests = (int) getScalar($db, "
|
|
SELECT COUNT(*)
|
|
FROM trf_requests
|
|
WHERE idcompany = :idcompany
|
|
", $companyParams);
|
|
|
|
$pendingTrfRequests = (int) getScalar($db, "
|
|
SELECT COUNT(*)
|
|
FROM trf_requests
|
|
WHERE idcompany = :idcompany
|
|
AND status IN ('draft', 'submitted', 'available_for_lab', 'pulled_by_lab', 'in_lims', 'testing')
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && $hasLabReportsTable) {
|
|
$reportsReceived = (int) getScalar($db, "
|
|
SELECT COUNT(lr.idlabreport)
|
|
FROM lab_reports lr
|
|
INNER JOIN trf_requests trf ON trf.idtrf = lr.idtrf
|
|
WHERE trf.idcompany = :idcompany
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && tableExists($db, 'documents')) {
|
|
$totalDocuments = (int) getScalar($db, "
|
|
SELECT COUNT(*)
|
|
FROM documents
|
|
WHERE idcompany = :idcompany
|
|
", $companyParams);
|
|
}
|
|
|
|
if ($selectedCompanyId > 0 && !$totalDocuments && tableExists($db, 'trf_documents')) {
|
|
$totalDocuments = (int) getScalar($db, "
|
|
SELECT COUNT(td.iddocument)
|
|
FROM trf_documents td
|
|
INNER JOIN trf_requests trf ON trf.idtrf = td.idtrf
|
|
WHERE trf.idcompany = :idcompany
|
|
", $companyParams);
|
|
}
|
|
|
|
/*
|
|
* TRF status chart data.
|
|
*/
|
|
$trfStatusRows = [];
|
|
|
|
if ($selectedCompanyId > 0 && $hasTrfRequestsTable) {
|
|
$trfStatusRows = getRows($db, "
|
|
SELECT status, COUNT(*) AS total
|
|
FROM trf_requests
|
|
WHERE idcompany = :idcompany
|
|
GROUP BY status
|
|
ORDER BY status ASC
|
|
", $companyParams);
|
|
}
|
|
|
|
$trfStatusLabels = [];
|
|
$trfStatusValues = [];
|
|
|
|
foreach ($trfStatusRows as $row) {
|
|
$trfStatusLabels[] = ucfirst(str_replace('_', ' ', $row['status']));
|
|
$trfStatusValues[] = (int) $row['total'];
|
|
}
|
|
|
|
/*
|
|
* Recent TRF requests.
|
|
*/
|
|
$recentTrfRequests = [];
|
|
|
|
if ($selectedCompanyId > 0 && $hasTrfRequestsTable) {
|
|
$recentTrfRequests = getRows($db, "
|
|
SELECT
|
|
idtrf,
|
|
trf_code,
|
|
external_trf_id,
|
|
trf_type,
|
|
service_required,
|
|
status,
|
|
created_at
|
|
FROM trf_requests
|
|
WHERE idcompany = :idcompany
|
|
ORDER BY created_at DESC, idtrf DESC
|
|
LIMIT 8
|
|
", $companyParams);
|
|
}
|
|
|
|
/*
|
|
* Pending actions.
|
|
*/
|
|
$pendingActions = [
|
|
[
|
|
'title' => 'Create business partners',
|
|
'text' => $hasBusinessPartnersTable ? 'Add producers, suppliers, vendors and factories.' : 'Business partner module is not active yet.',
|
|
'icon' => 'bx bx-network-chart',
|
|
'completed' => $hasBusinessPartnersTable && $totalBusinessPartners > 0,
|
|
'link' => $hasBusinessPartnersTable ? 'business-partners.php' : '#',
|
|
],
|
|
[
|
|
'title' => 'Create or import samples',
|
|
'text' => $hasSamplesTable ? 'Start building product identity cards.' : 'Sample module is not active yet.',
|
|
'icon' => 'bx bx-package',
|
|
'completed' => $hasSamplesTable && $totalSamples > 0,
|
|
'link' => $hasSamplesTable ? 'samples.php' : '#',
|
|
],
|
|
[
|
|
'title' => 'Add BOM / parts',
|
|
'text' => $hasSamplePartsTable ? 'Complete sample identity with materials and components.' : 'BOM module is not active yet.',
|
|
'icon' => 'bx bx-git-branch',
|
|
'completed' => $hasSamplePartsTable && $totalSampleParts > 0,
|
|
'link' => $hasSamplesTable ? 'samples.php' : '#',
|
|
],
|
|
[
|
|
'title' => 'Attach documents',
|
|
'text' => $hasDocumentsTable ? 'Attach technical sheets, certificates and declarations.' : 'Document repository is not active yet.',
|
|
'icon' => 'bx bx-folder',
|
|
'completed' => $hasDocumentsTable && $totalDocuments > 0,
|
|
'link' => $hasDocumentsTable ? 'documents.php' : '#',
|
|
],
|
|
[
|
|
'title' => 'Prepare TRF requests',
|
|
'text' => $hasTrfRequestsTable ? 'Create test requests from one or more samples.' : 'TRF request module is not active yet.',
|
|
'icon' => 'bx bx-file',
|
|
'completed' => $hasTrfRequestsTable && $totalTrfRequests > 0,
|
|
'link' => $hasTrfRequestsTable ? 'trf-requests.php' : '#',
|
|
],
|
|
];
|
|
|
|
$completedPendingActions = count(array_filter($pendingActions, function ($item) {
|
|
return $item['completed'];
|
|
}));
|
|
|
|
$operationalProgress = count($pendingActions) > 0
|
|
? round(($completedPendingActions / count($pendingActions)) * 100)
|
|
: 0;
|
|
|
|
/*
|
|
* 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()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Load user 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 = 'Customer Dashboard';
|
|
$companyName = $selectedCompany['company_name'] ?? 'Your Company';
|
|
$companyLegalName = $selectedCompany['legal_name'] ?? '';
|
|
?>
|
|
|
|
<!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-soft-red: #fef2f2;
|
|
--trfgo-border: #e5e7eb;
|
|
--trfgo-muted: #64748b;
|
|
}
|
|
|
|
.customer-hero {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 20px;
|
|
background:
|
|
radial-gradient(circle at top right, rgba(45, 212, 191, 0.32), transparent 34%),
|
|
linear-gradient(135deg, #0f172a 0%, #1d4ed8 54%, #0f766e 100%);
|
|
color: #fff;
|
|
padding: 28px;
|
|
min-height: 220px;
|
|
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22);
|
|
}
|
|
|
|
.customer-hero h1,
|
|
.customer-hero h2,
|
|
.customer-hero h3,
|
|
.customer-hero h4,
|
|
.customer-hero h5,
|
|
.customer-hero h6 {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.customer-hero::before {
|
|
content: "";
|
|
position: absolute;
|
|
width: 280px;
|
|
height: 280px;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.09);
|
|
right: -90px;
|
|
bottom: -120px;
|
|
}
|
|
|
|
.customer-hero-content {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.customer-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;
|
|
}
|
|
|
|
.customer-title {
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
letter-spacing: -0.03em;
|
|
margin-bottom: 8px;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.customer-subtitle {
|
|
max-width: 820px;
|
|
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;
|
|
}
|
|
|
|
.company-switcher {
|
|
max-width: 360px;
|
|
}
|
|
|
|
.company-switcher .form-select {
|
|
border-radius: 12px;
|
|
border: 0;
|
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.16);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.coming-soon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: #f1f5f9;
|
|
color: #475569;
|
|
border-radius: 999px;
|
|
padding: 5px 9px;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.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.disabled-action {
|
|
pointer-events: none;
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.action-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 13px 0;
|
|
border-bottom: 1px solid #eef2f7;
|
|
}
|
|
|
|
.action-item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.action-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.action-icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 12px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f1f5f9;
|
|
color: #475569;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.action-icon.completed {
|
|
background: #ecfdf5;
|
|
color: #059669;
|
|
}
|
|
|
|
.action-title {
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.action-text {
|
|
color: var(--trfgo-muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.badge-soft-primary {
|
|
background: #dbeafe;
|
|
color: #1d4ed8;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
padding: 6px 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid #eef2f7;
|
|
}
|
|
|
|
.info-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.info-label {
|
|
color: var(--trfgo-muted);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.info-value {
|
|
color: #0f172a;
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
text-align: right;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.trf-table th {
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.trf-table td {
|
|
vertical-align: middle;
|
|
border-color: #eef2f7;
|
|
}
|
|
|
|
.trf-code {
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.trf-sub {
|
|
font-size: 12px;
|
|
color: var(--trfgo-muted);
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.customer-hero {
|
|
padding: 22px;
|
|
}
|
|
|
|
.customer-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="customer-hero mb-4">
|
|
<div class="customer-hero-content">
|
|
<div class="d-flex align-items-start justify-content-between flex-wrap gap-3">
|
|
<div>
|
|
<div class="customer-eyebrow">
|
|
<i class="bx bx-home-circle"></i>
|
|
Customer Workspace
|
|
</div>
|
|
|
|
<h1 class="customer-title">
|
|
<?= e($companyName); ?>
|
|
</h1>
|
|
|
|
<p class="customer-subtitle">
|
|
Manage your samples, digital test request forms, laboratory reports and technical documents from one operational dashboard.
|
|
This workspace is designed for customer users and on-premise installations.
|
|
</p>
|
|
|
|
<div class="hero-actions">
|
|
<a href="<?= $hasSamplesTable ? 'samples.php' : '#'; ?>" class="btn-hero-light <?= !$hasSamplesTable ? 'disabled-action' : ''; ?>">
|
|
<i class="bx bx-package"></i>
|
|
Add Sample
|
|
</a>
|
|
|
|
<a href="<?= $hasTrfRequestsTable ? 'trf-requests.php' : '#'; ?>" class="btn-hero-outline <?= !$hasTrfRequestsTable ? 'disabled-action' : ''; ?>">
|
|
<i class="bx bx-file"></i>
|
|
New TRF Request
|
|
</a>
|
|
|
|
<a href="<?= $hasDocumentsTable ? 'documents.php' : '#'; ?>" class="btn-hero-outline <?= !$hasDocumentsTable ? 'disabled-action' : ''; ?>">
|
|
<i class="bx bx-folder"></i>
|
|
Documents
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (count($userCompanies) > 1): ?>
|
|
<div class="company-switcher">
|
|
<select class="form-select" id="companySwitcher">
|
|
<?php foreach ($userCompanies as $companyOption): ?>
|
|
<option value="<?= e($companyOption['idcompany']); ?>" <?= (int) $companyOption['idcompany'] === $selectedCompanyId ? 'selected' : ''; ?>>
|
|
<?= e($companyOption['company_name']); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!$selectedCompany): ?>
|
|
<div class="alert alert-warning">
|
|
<strong>No company available.</strong>
|
|
This user is not linked to any TRFgo company yet.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="dashboard-toolbar">
|
|
<div>
|
|
<div class="dashboard-toolbar-title">Operational 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 === 'customer_company_profile'): ?>
|
|
<div class="col-12 col-xl-4 dashboard-widget" data-widget="customer_company_profile">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Company Profile</h6>
|
|
<p class="widget-subtitle">Current customer workspace</p>
|
|
</div>
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="info-row">
|
|
<div class="info-label">Company</div>
|
|
<div class="info-value"><?= e($companyName); ?></div>
|
|
</div>
|
|
|
|
<div class="info-row">
|
|
<div class="info-label">Legal Name</div>
|
|
<div class="info-value"><?= $companyLegalName ? e($companyLegalName) : '-'; ?></div>
|
|
</div>
|
|
|
|
<div class="info-row">
|
|
<div class="info-label">Brands</div>
|
|
<div class="info-value"><?= e($totalBrands); ?></div>
|
|
</div>
|
|
|
|
<div class="info-row">
|
|
<div class="info-label">Departments</div>
|
|
<div class="info-value"><?= e($totalDepartments); ?></div>
|
|
</div>
|
|
|
|
<div class="info-row">
|
|
<div class="info-label">Active Users</div>
|
|
<div class="info-value"><?= e($totalCompanyUsers); ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_business_partners'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_business_partners">
|
|
<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>
|
|
<?php if (!$hasBusinessPartnersTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Business Partners</div>
|
|
<div class="stat-value"><?= e($totalBusinessPartners); ?></div>
|
|
<div class="stat-caption"><?= e($activeBusinessPartners); ?> active partners</div>
|
|
</div>
|
|
<div class="stat-icon green">
|
|
<i class="bx bx-network-chart"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_samples'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_samples">
|
|
<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>
|
|
<?php if (!$hasSamplesTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Samples</div>
|
|
<div class="stat-value"><?= e($totalSamples); ?></div>
|
|
<div class="stat-caption">Products or samples in archive</div>
|
|
</div>
|
|
<div class="stat-icon blue">
|
|
<i class="bx bx-package"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_sample_parts'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_sample_parts">
|
|
<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>
|
|
<?php if (!$hasSamplePartsTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">BOM / Parts</div>
|
|
<div class="stat-value"><?= e($totalSampleParts); ?></div>
|
|
<div class="stat-caption"><?= e($totalSamplePhotos); ?> sample photos uploaded</div>
|
|
</div>
|
|
<div class="stat-icon orange">
|
|
<i class="bx bx-git-branch"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_trf_requests'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_trf_requests">
|
|
<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>
|
|
<?php if (!$hasTrfRequestsTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">TRF Requests</div>
|
|
<div class="stat-value"><?= e($totalTrfRequests); ?></div>
|
|
<div class="stat-caption">Digital test request forms</div>
|
|
</div>
|
|
<div class="stat-icon green">
|
|
<i class="bx bx-file"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_documents'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_documents">
|
|
<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>
|
|
<?php if (!$hasDocumentsTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Documents</div>
|
|
<div class="stat-value"><?= e($totalDocuments); ?></div>
|
|
<div class="stat-caption">Technical files and certificates</div>
|
|
</div>
|
|
<div class="stat-icon purple">
|
|
<i class="bx bx-folder"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_pending_requests'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_pending_requests">
|
|
<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>
|
|
<?php if (!$hasTrfRequestsTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Pending</div>
|
|
<div class="stat-value"><?= e($pendingTrfRequests); ?></div>
|
|
<div class="stat-caption">Requests not completed yet</div>
|
|
</div>
|
|
<div class="stat-icon orange">
|
|
<i class="bx bx-time-five"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'kpi_reports_received'): ?>
|
|
<div class="col-12 col-md-6 col-xl-3 dashboard-widget" data-widget="kpi_reports_received">
|
|
<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>
|
|
<?php if (!$hasLabReportsTable): ?>
|
|
<span class="coming-soon">Coming soon</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<div class="stat-label">Reports</div>
|
|
<div class="stat-value"><?= e($reportsReceived); ?></div>
|
|
<div class="stat-caption">Laboratory reports received</div>
|
|
</div>
|
|
<div class="stat-icon purple">
|
|
<i class="bx bx-test-tube"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'chart_trf_status'): ?>
|
|
<div class="col-12 col-xl-8 dashboard-widget" data-widget="chart_trf_status">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">TRF Status Overview</h6>
|
|
<p class="widget-subtitle">Distribution of digital test request forms by status</p>
|
|
</div>
|
|
<div class="drag-handle"><i class="bx bx-move"></i></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<?php if ($hasTrfRequestsTable && count($trfStatusRows) > 0): ?>
|
|
<div id="trfStatusChart" style="min-height: 330px;"></div>
|
|
<?php else: ?>
|
|
<div class="empty-state">
|
|
<i class="bx bx-pie-chart-alt-2"></i>
|
|
<h6>TRF chart not available yet</h6>
|
|
<p class="mb-0">The chart will be populated when TRF requests are created.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'master_data_readiness'): ?>
|
|
<div class="col-12 col-xl-4 dashboard-widget" data-widget="master_data_readiness"">
|
|
<div class=" card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Master Data Readiness</h6>
|
|
<p class="widget-subtitle">Product identity card setup 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($operationalProgress); ?>% ready</strong>
|
|
<span class="badge-soft-muted"><?= e($completedPendingActions); ?>/<?= e(count($pendingActions)); ?></span>
|
|
</div>
|
|
|
|
<div class="progress mb-3">
|
|
<div class="progress-bar" role="progressbar" style="width: <?= e($operationalProgress); ?>%;" aria-valuenow="<?= e($operationalProgress); ?>" aria-valuemin="0" aria-valuemax="100"></div>
|
|
</div>
|
|
|
|
<?php foreach ($pendingActions as $item): ?>
|
|
<div class="action-item">
|
|
<div class="action-left">
|
|
<div class="action-icon <?= $item['completed'] ? 'completed' : ''; ?>">
|
|
<i class="<?= e($item['icon']); ?>"></i>
|
|
</div>
|
|
<div>
|
|
<div class="action-title"><?= e($item['title']); ?></div>
|
|
<div class="action-text"><?= e($item['text']); ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($item['completed']): ?>
|
|
<span class="badge-soft-success">Ready</span>
|
|
<?php else: ?>
|
|
<span class="badge-soft-warning">Todo</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'recent_samples'): ?>
|
|
<div class="col-12 col-xl-8 dashboard-widget" data-widget="recent_samples">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Recent Samples</h6>
|
|
<p class="widget-subtitle">Latest product identity cards created by the customer</p>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<a href="<?= $hasSamplesTable ? 'samples.php' : '#'; ?>" class="btn btn-sm btn-primary <?= !$hasSamplesTable ? 'disabled-action' : ''; ?>">
|
|
<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 ($hasSamplesTable && count($recentSamples) > 0): ?>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle trf-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Sample</th>
|
|
<th>Brand / Dept.</th>
|
|
<th>Producer / Supplier</th>
|
|
<th class="text-center">BOM</th>
|
|
<th class="text-center">Files</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($recentSamples as $sample): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="trf-code"><?= e($sample['sample_code']); ?></div>
|
|
<div class="trf-sub">
|
|
<?= e($sample['sample_description']); ?>
|
|
<?php if (!empty($sample['article_no'])): ?>
|
|
| Article: <?= e($sample['article_no']); ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
|
|
<td>
|
|
<div><?= e($sample['brand_name'] ?: '-'); ?></div>
|
|
<div class="trf-sub"><?= e($sample['department_name'] ?: '-'); ?></div>
|
|
</td>
|
|
|
|
<td>
|
|
<div><?= e($sample['producer_name'] ?: '-'); ?></div>
|
|
<div class="trf-sub"><?= e($sample['supplier_name'] ?: '-'); ?></div>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<span class="badge-soft-muted"><?= e($sample['parts_count']); ?> parts</span>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<span class="badge-soft-muted">
|
|
<?= e(((int) $sample['photos_count']) + ((int) $sample['documents_count'])); ?> files
|
|
</span>
|
|
</td>
|
|
|
|
<td>
|
|
<span class="badge-soft-primary">
|
|
<?= e(ucfirst(str_replace('_', ' ', $sample['status']))); ?>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="empty-state">
|
|
<i class="bx bx-package"></i>
|
|
<h6>No samples yet</h6>
|
|
<p>The latest product identity cards will appear here.</p>
|
|
<a href="<?= $hasSamplesTable ? 'samples.php' : '#'; ?>" class="btn btn-primary <?= !$hasSamplesTable ? 'disabled-action' : ''; ?>">
|
|
<i class="bx bx-plus-circle"></i>
|
|
Add sample
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'recent_trf_requests'): ?>
|
|
<div class="col-12 col-xl-8 dashboard-widget" data-widget="recent_trf_requests">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Recent TRF Requests</h6>
|
|
<p class="widget-subtitle">Latest customer test request forms</p>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<a href="<?= $hasTrfRequestsTable ? 'trf-requests.php' : '#'; ?>" class="btn btn-sm btn-primary <?= !$hasTrfRequestsTable ? 'disabled-action' : ''; ?>">
|
|
<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 ($hasTrfRequestsTable && count($recentTrfRequests) > 0): ?>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle trf-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>TRF Code</th>
|
|
<th>Type</th>
|
|
<th>Service</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($recentTrfRequests as $trf): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="trf-code"><?= e($trf['trf_code']); ?></div>
|
|
<?php if (!empty($trf['external_trf_id'])): ?>
|
|
<div class="trf-sub"><?= e($trf['external_trf_id']); ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= e($trf['trf_type'] ?: '-'); ?></td>
|
|
<td><?= e($trf['service_required'] ?: '-'); ?></td>
|
|
<td>
|
|
<span class="badge-soft-primary">
|
|
<?= e(ucfirst(str_replace('_', ' ', $trf['status']))); ?>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<?= !empty($trf['created_at']) ? e(date('d/m/Y', strtotime($trf['created_at']))) : '-'; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="empty-state">
|
|
<i class="bx bx-file"></i>
|
|
<h6>No TRF requests yet</h6>
|
|
<p>The latest digital test request forms will appear here.</p>
|
|
<a href="<?= $hasTrfRequestsTable ? 'trf-requests.php' : '#'; ?>" class="btn btn-primary <?= !$hasTrfRequestsTable ? 'disabled-action' : ''; ?>">
|
|
<i class="bx bx-plus-circle"></i>
|
|
New TRF request
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($widgetKey === 'document_repository'): ?>
|
|
<div class="col-12 col-xl-4 dashboard-widget" data-widget="document_repository">
|
|
<div class="card widget-card">
|
|
<div class="card-header bg-transparent">
|
|
<div class="widget-header">
|
|
<div>
|
|
<h6 class="widget-title mb-0">Document Repository</h6>
|
|
<p class="widget-subtitle">Technical files and report archive</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-3">
|
|
<div>
|
|
<div class="stat-label">Documents</div>
|
|
<div class="stat-value"><?= e($totalDocuments); ?></div>
|
|
<div class="stat-caption">Linked technical and laboratory files</div>
|
|
</div>
|
|
<div class="stat-icon blue">
|
|
<i class="bx bx-folder"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!$hasDocumentsTable): ?>
|
|
<div class="alert alert-light border mb-0">
|
|
<strong>Coming soon.</strong><br>
|
|
The document repository will be activated with the sample/TRF modules.
|
|
</div>
|
|
<?php else: ?>
|
|
<a href="documents.php" class="btn btn-primary w-100">
|
|
<i class="bx bx-folder-open"></i>
|
|
Open Repository
|
|
</a>
|
|
<?php endif; ?>
|
|
</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">Common customer operations</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="<?= $hasBusinessPartnersTable ? 'business-partners.php' : '#'; ?>" class="quick-action <?= !$hasBusinessPartnersTable ? 'disabled-action' : ''; ?>">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-network-chart"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Add business partner</span>
|
|
<span class="quick-action-text">Create producer, supplier, vendor or factory</span>
|
|
</span>
|
|
</a>
|
|
<a href="<?= $hasSamplesTable ? 'samples.php' : '#'; ?>" class="quick-action <?= !$hasSamplesTable ? 'disabled-action' : ''; ?>">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-package"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Create sample identity card</span>
|
|
<span class="quick-action-text">Create product data, producer, supplier and technical details</span>
|
|
</span>
|
|
</a>
|
|
|
|
<a href="<?= $hasTrfRequestsTable ? 'trf-requests.php' : '#'; ?>" class="quick-action <?= !$hasTrfRequestsTable ? 'disabled-action' : ''; ?>">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-file"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Create TRF request</span>
|
|
<span class="quick-action-text">Prepare a new digital test request form</span>
|
|
</span>
|
|
</a>
|
|
|
|
<a href="<?= $hasLabReportsTable ? 'lab-results.php' : '#'; ?>" class="quick-action <?= !$hasLabReportsTable ? 'disabled-action' : ''; ?>">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-test-tube"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">View lab results</span>
|
|
<span class="quick-action-text">Check received reports and structured results</span>
|
|
</span>
|
|
</a>
|
|
|
|
<a href="<?= $hasDocumentsTable ? 'documents.php' : '#'; ?>" class="quick-action <?= !$hasDocumentsTable ? 'disabled-action' : ''; ?>">
|
|
<span class="quick-action-icon">
|
|
<i class="bx bx-folder"></i>
|
|
</span>
|
|
<span>
|
|
<span class="quick-action-title d-block">Open documents</span>
|
|
<span class="quick-action-text">Browse the technical document repository</span>
|
|
</span>
|
|
</a>
|
|
</div>
|
|
</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 trfStatusLabels = <?= json_encode($trfStatusLabels, JSON_UNESCAPED_UNICODE); ?>;
|
|
const trfStatusValues = <?= json_encode($trfStatusValues, JSON_NUMERIC_CHECK); ?>;
|
|
|
|
let trfChartInstance = null;
|
|
|
|
function renderTrfStatusChart() {
|
|
const chartElement = document.querySelector("#trfStatusChart");
|
|
|
|
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 (trfChartInstance !== null) {
|
|
trfChartInstance.destroy();
|
|
}
|
|
|
|
const options = {
|
|
series: trfStatusValues,
|
|
chart: {
|
|
type: "donut",
|
|
height: 330,
|
|
toolbar: {
|
|
show: false
|
|
},
|
|
fontFamily: "inherit"
|
|
},
|
|
labels: trfStatusLabels,
|
|
legend: {
|
|
position: "bottom"
|
|
},
|
|
dataLabels: {
|
|
enabled: true
|
|
},
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: "68%",
|
|
labels: {
|
|
show: true,
|
|
total: {
|
|
show: true,
|
|
label: "TRF",
|
|
formatter: function(w) {
|
|
return w.globals.seriesTotals.reduce((a, b) => a + b, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
stroke: {
|
|
width: 2
|
|
}
|
|
};
|
|
|
|
trfChartInstance = new ApexCharts(chartElement, options);
|
|
trfChartInstance.render();
|
|
}
|
|
|
|
renderTrfStatusChart();
|
|
|
|
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() {
|
|
renderTrfStatusChart();
|
|
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();
|
|
}
|
|
});
|
|
|
|
$("#companySwitcher").on("change", function() {
|
|
const idcompany = $(this).val();
|
|
window.location.href = window.location.pathname + "?idcompany=" + encodeURIComponent(idcompany);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|