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'] ?? ''; ?> <?= e($pageTitle); ?> - <?= isset($titlewebsite) ? e($titlewebsite) : 'TRFgo'; ?>
Customer Workspace

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.

1): ?>
No company available. This user is not linked to any TRFgo company yet.
Operational Dashboard

Drag widgets using the handle. The layout is saved for your user account.

Company Profile

Current customer workspace

Company
Legal Name
Brands
Departments
Active Users
Coming soon
Business Partners
active partners
Coming soon
Samples
Products or samples in archive
Coming soon
BOM / Parts
sample photos uploaded
Coming soon
TRF Requests
Digital test request forms
Coming soon
Documents
Technical files and certificates
Coming soon
Pending
Requests not completed yet
Coming soon
Reports
Laboratory reports received
TRF Status Overview

Distribution of digital test request forms by status

0): ?>
TRF chart not available yet

The chart will be populated when TRF requests are created.

Master Data Readiness

Product identity card setup progress

% ready /
Ready Todo
Recent Samples

Latest product identity cards created by the customer

0): ?>
Sample Brand / Dept. Producer / Supplier BOM Files Status
| Article:
parts files
No samples yet

The latest product identity cards will appear here.

Add sample
Recent TRF Requests

Latest customer test request forms

0): ?>
TRF Code Type Service Status Created
No TRF requests yet

The latest digital test request forms will appear here.

New TRF request
Document Repository

Technical files and report archive

Documents
Linked technical and laboratory files
Coming soon.
The document repository will be activated with the sample/TRF modules.
Open Repository