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
= e($companyName); ?>
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): ?>
>
= e($companyOption['company_name']); ?>
No company available.
This user is not linked to any TRFgo company yet.