format('Ymd_His')
. '_EuropeRome_'
. $cleanOriginalName
. '.' . $extension;
}
function resizeAndConvertImageToJpeg(string $sourcePath, string $targetPath, int $maxSize = 1600, int $quality = 82): bool
{
$imageInfo = getimagesize($sourcePath);
if ($imageInfo === false) {
return false;
}
$mimeType = $imageInfo['mime'];
$sourceImage = null;
switch ($mimeType) {
case 'image/jpeg':
$sourceImage = imagecreatefromjpeg($sourcePath);
break;
case 'image/png':
$sourceImage = imagecreatefrompng($sourcePath);
break;
case 'image/webp':
if (!function_exists('imagecreatefromwebp')) {
return false;
}
$sourceImage = imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
if (!$sourceImage) {
return false;
}
$originalWidth = imagesx($sourceImage);
$originalHeight = imagesy($sourceImage);
if ($originalWidth <= 0 || $originalHeight <= 0) {
imagedestroy($sourceImage);
return false;
}
$ratio = min($maxSize / $originalWidth, $maxSize / $originalHeight, 1);
$newWidth = (int) round($originalWidth * $ratio);
$newHeight = (int) round($originalHeight * $ratio);
$targetImage = imagecreatetruecolor($newWidth, $newHeight);
// White background avoids black areas when converting transparent PNG/WEBP to JPG.
$white = imagecolorallocate($targetImage, 255, 255, 255);
imagefill($targetImage, 0, 0, $white);
imagecopyresampled(
$targetImage,
$sourceImage,
0,
0,
0,
0,
$newWidth,
$newHeight,
$originalWidth,
$originalHeight
);
$result = imagejpeg($targetImage, $targetPath, $quality);
imagedestroy($sourceImage);
imagedestroy($targetImage);
return $result;
}
$idsample = isset($_GET['idsample']) ? (int) $_GET['idsample'] : 0;
if ($idsample <= 0) {
die('Invalid sample id.');
}
$partRiskLevels = [
'unknown' => 'Unknown',
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
'critical' => 'Critical',
];
$photoTypes = [
'main' => 'Main',
'product' => 'Product',
'label' => 'Label',
'packaging' => 'Packaging',
'warning' => 'Warning',
'detail' => 'Detail',
'other' => 'Other',
];
$documentTypes = [
'technical_sheet' => 'Technical Sheet',
'declaration' => 'Declaration',
'bom' => 'BOM',
'photo' => 'Photo',
'certificate' => 'Certificate',
'test_report' => 'Test Report',
'supplier_document' => 'Supplier Document',
'invoice' => 'Invoice',
'manual' => 'Manual',
'other' => 'Other',
];
/*
* Load sample.
*/
$stmt = $db->prepare("
SELECT
s.*,
c.company_name,
c.legal_name AS company_legal_name,
b.brand_name,
d.department_name,
bp1.partner_name AS producer_name,
bp2.partner_name AS supplier_name,
ac.name AS country_of_origin_name,
u.email AS created_by_email,
u.first_name AS created_by_first_name,
u.last_name AS created_by_last_name
FROM samples s
INNER JOIN companies c ON c.idcompany = s.idcompany
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 auth_countries ac ON ac.id = s.country_of_origin
LEFT JOIN auth_users u ON u.id = s.created_by
WHERE s.idsample = :idsample
LIMIT 1
");
$stmt->execute([':idsample' => $idsample]);
$sample = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$sample) {
die('Sample not found.');
}
$idcompany = (int) $sample['idcompany'];
/*
* Upload folders.
* Current file is inside public/userarea.
* Files will be stored inside public/userarea/uploads/trfgo.
*/
$uploadBaseDir = __DIR__ . '/uploads/trfgo';
$uploadBaseWebPath = 'uploads/trfgo/';
$samplePhotoRelativeDir = 'company_' . $idcompany . '/samples/' . $idsample . '/photos';
$documentRelativeDir = 'company_' . $idcompany . '/samples/' . $idsample . '/documents';
$samplePhotoDir = $uploadBaseDir . '/' . $samplePhotoRelativeDir;
$documentDir = $uploadBaseDir . '/' . $documentRelativeDir;
ensureDirectory($samplePhotoDir);
ensureDirectory($documentDir);
$photoWebPath = $uploadBaseWebPath;
$documentWebPath = $uploadBaseWebPath;
/*
* AJAX actions.
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
try {
if ($action === 'save_part') {
$idpart = isset($_POST['idpart']) ? (int) $_POST['idpart'] : 0;
$parentIdpart = !empty($_POST['parent_idpart']) ? (int) $_POST['parent_idpart'] : null;
$partCode = trim($_POST['part_code'] ?? '');
$partName = trim($_POST['part_name'] ?? '');
$partDescription = trim($_POST['part_description'] ?? '');
$material = trim($_POST['material'] ?? '');
$color = trim($_POST['color'] ?? '');
$quantity = trim($_POST['quantity'] ?? '');
$unit = trim($_POST['unit'] ?? '');
$supplierId = !empty($_POST['supplier_id']) ? (int) $_POST['supplier_id'] : null;
$producerId = !empty($_POST['producer_id']) ? (int) $_POST['producer_id'] : null;
$position = trim($_POST['position'] ?? '');
$riskLevel = $_POST['risk_level'] ?? 'unknown';
$notes = trim($_POST['notes'] ?? '');
$sortOrder = isset($_POST['sort_order']) ? (int) $_POST['sort_order'] : 0;
if ($partName === '') {
jsonResponse([
'success' => false,
'message' => 'Part name is required.'
]);
}
if (!array_key_exists($riskLevel, $partRiskLevels)) {
$riskLevel = 'unknown';
}
/*
* Validate parent part belongs to the same sample.
*/
if ($parentIdpart !== null) {
$stmt = $db->prepare("
SELECT COUNT(*)
FROM sample_parts
WHERE idpart = :idpart
AND idsample = :idsample
");
$stmt->execute([
':idpart' => $parentIdpart,
':idsample' => $idsample,
]);
if ((int) $stmt->fetchColumn() === 0) {
jsonResponse([
'success' => false,
'message' => 'Selected parent part does not belong to this sample.'
]);
}
}
/*
* Validate supplier/producer belong to the sample company.
*/
foreach (
[
'supplier' => $supplierId,
'producer' => $producerId,
] as $label => $partnerId
) {
if ($partnerId !== null) {
$stmt = $db->prepare("
SELECT COUNT(*)
FROM business_partners
WHERE idpartner = :idpartner
AND idcompany = :idcompany
");
$stmt->execute([
':idpartner' => $partnerId,
':idcompany' => $idcompany,
]);
if ((int) $stmt->fetchColumn() === 0) {
jsonResponse([
'success' => false,
'message' => 'Selected ' . $label . ' does not belong to the sample company.'
]);
}
}
}
if ($idpart > 0) {
$stmt = $db->prepare("
UPDATE sample_parts
SET
parent_idpart = :parent_idpart,
part_code = :part_code,
part_name = :part_name,
part_description = :part_description,
material = :material,
color = :color,
quantity = :quantity,
unit = :unit,
supplier_id = :supplier_id,
producer_id = :producer_id,
position = :position,
risk_level = :risk_level,
notes = :notes,
sort_order = :sort_order,
updated_at = NOW()
WHERE idpart = :idpart
AND idsample = :idsample
");
$stmt->execute([
':parent_idpart' => $parentIdpart,
':part_code' => $partCode !== '' ? $partCode : null,
':part_name' => $partName,
':part_description' => $partDescription !== '' ? $partDescription : null,
':material' => $material !== '' ? $material : null,
':color' => $color !== '' ? $color : null,
':quantity' => $quantity !== '' ? $quantity : null,
':unit' => $unit !== '' ? $unit : null,
':supplier_id' => $supplierId,
':producer_id' => $producerId,
':position' => $position !== '' ? $position : null,
':risk_level' => $riskLevel,
':notes' => $notes !== '' ? $notes : null,
':sort_order' => $sortOrder,
':idpart' => $idpart,
':idsample' => $idsample,
]);
jsonResponse([
'success' => true,
'message' => 'BOM part updated successfully.'
]);
}
$stmt = $db->prepare("
INSERT INTO sample_parts (
idsample,
parent_idpart,
part_code,
part_name,
part_description,
material,
color,
quantity,
unit,
supplier_id,
producer_id,
position,
risk_level,
notes,
sort_order,
created_at,
updated_at
) VALUES (
:idsample,
:parent_idpart,
:part_code,
:part_name,
:part_description,
:material,
:color,
:quantity,
:unit,
:supplier_id,
:producer_id,
:position,
:risk_level,
:notes,
:sort_order,
NOW(),
NOW()
)
");
$stmt->execute([
':idsample' => $idsample,
':parent_idpart' => $parentIdpart,
':part_code' => $partCode !== '' ? $partCode : null,
':part_name' => $partName,
':part_description' => $partDescription !== '' ? $partDescription : null,
':material' => $material !== '' ? $material : null,
':color' => $color !== '' ? $color : null,
':quantity' => $quantity !== '' ? $quantity : null,
':unit' => $unit !== '' ? $unit : null,
':supplier_id' => $supplierId,
':producer_id' => $producerId,
':position' => $position !== '' ? $position : null,
':risk_level' => $riskLevel,
':notes' => $notes !== '' ? $notes : null,
':sort_order' => $sortOrder,
]);
jsonResponse([
'success' => true,
'message' => 'BOM part created successfully.'
]);
}
if ($action === 'get_part') {
$idpart = isset($_POST['idpart']) ? (int) $_POST['idpart'] : 0;
if ($idpart <= 0) {
jsonResponse([
'success' => false,
'message' => 'Invalid part id.'
]);
}
$stmt = $db->prepare("
SELECT *
FROM sample_parts
WHERE idpart = :idpart
AND idsample = :idsample
LIMIT 1
");
$stmt->execute([
':idpart' => $idpart,
':idsample' => $idsample,
]);
$part = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$part) {
jsonResponse([
'success' => false,
'message' => 'Part not found.'
]);
}
jsonResponse([
'success' => true,
'part' => $part
]);
}
if ($action === 'delete_part') {
$idpart = isset($_POST['idpart']) ? (int) $_POST['idpart'] : 0;
if ($idpart <= 0) {
jsonResponse([
'success' => false,
'message' => 'Invalid part id.'
]);
}
/*
* Prevent delete if child parts or documents/photos exist.
*/
$stmt = $db->prepare("
SELECT
(SELECT COUNT(*) FROM sample_parts WHERE parent_idpart = :idpart1) AS child_count,
(SELECT COUNT(*) FROM sample_part_photos WHERE idpart = :idpart2) AS photos_count,
(SELECT COUNT(*) FROM sample_part_documents WHERE idpart = :idpart3) AS documents_count
");
$stmt->execute([
':idpart1' => $idpart,
':idpart2' => $idpart,
':idpart3' => $idpart,
]);
$usage = $stmt->fetch(PDO::FETCH_ASSOC);
if (
(int) $usage['child_count'] > 0 ||
(int) $usage['photos_count'] > 0 ||
(int) $usage['documents_count'] > 0
) {
jsonResponse([
'success' => false,
'message' => 'This part has child parts, photos or documents. Remove them before deleting the part.'
]);
}
$stmt = $db->prepare("
DELETE FROM sample_parts
WHERE idpart = :idpart
AND idsample = :idsample
");
$stmt->execute([
':idpart' => $idpart,
':idsample' => $idsample,
]);
jsonResponse([
'success' => true,
'message' => 'BOM part deleted successfully.'
]);
}
if ($action === 'upload_sample_photo') {
if (empty($_FILES['photo_file']['name'])) {
jsonResponse([
'success' => false,
'message' => 'No photo uploaded.'
]);
}
if (!extension_loaded('gd')) {
jsonResponse([
'success' => false,
'message' => 'PHP GD extension is required to resize and convert images.'
]);
}
$photoType = $_POST['photo_type'] ?? 'product';
$description = trim($_POST['description'] ?? '');
$isMain = isset($_POST['is_main']) ? (int) $_POST['is_main'] : 0;
if (!array_key_exists($photoType, $photoTypes)) {
$photoType = 'product';
}
$originalFilename = $_FILES['photo_file']['name'];
$tmpFile = $_FILES['photo_file']['tmp_name'];
$imageInfo = getimagesize($tmpFile);
if ($imageInfo === false) {
jsonResponse([
'success' => false,
'message' => 'Uploaded file is not a valid image.'
]);
}
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/webp',
];
if (!in_array($imageInfo['mime'], $allowedMimeTypes, true)) {
jsonResponse([
'success' => false,
'message' => 'Invalid photo format. Allowed: JPG, PNG, WEBP.'
]);
}
// All photos are converted to JPG for web consistency.
$newFilename = buildDatedFilename('photo', $idcompany, $idsample, $originalFilename, 'jpg');
$relativeFilename = $samplePhotoRelativeDir . '/' . $newFilename;
$targetPath = $samplePhotoDir . '/' . $newFilename;
$converted = resizeAndConvertImageToJpeg($tmpFile, $targetPath, 1600, 82);
if (!$converted) {
jsonResponse([
'success' => false,
'message' => 'Unable to resize and convert photo.'
]);
}
if ($isMain === 1) {
$stmt = $db->prepare("
UPDATE sample_photos
SET is_main = 0
WHERE idsample = :idsample
");
$stmt->execute([':idsample' => $idsample]);
}
$stmt = $db->prepare("
INSERT INTO sample_photos (
idsample,
photo_type,
filename,
original_filename,
description,
is_main,
sort_order,
uploaded_by,
created_at
) VALUES (
:idsample,
:photo_type,
:filename,
:original_filename,
:description,
:is_main,
0,
:uploaded_by,
NOW()
)
");
$stmt->execute([
':idsample' => $idsample,
':photo_type' => $photoType,
':filename' => $relativeFilename,
':original_filename' => $originalFilename,
':description' => $description !== '' ? $description : null,
':is_main' => $isMain === 1 ? 1 : 0,
':uploaded_by' => $iduserlogin,
]);
jsonResponse([
'success' => true,
'message' => 'Photo uploaded, resized and converted successfully.'
]);
}
if ($action === 'delete_sample_photo') {
$idsamplephoto = isset($_POST['idsamplephoto']) ? (int) $_POST['idsamplephoto'] : 0;
if ($idsamplephoto <= 0) {
jsonResponse([
'success' => false,
'message' => 'Invalid photo id.'
]);
}
$stmt = $db->prepare("
SELECT filename
FROM sample_photos
WHERE idsamplephoto = :idsamplephoto
AND idsample = :idsample
LIMIT 1
");
$stmt->execute([
':idsamplephoto' => $idsamplephoto,
':idsample' => $idsample,
]);
$photo = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$photo) {
jsonResponse([
'success' => false,
'message' => 'Photo not found.'
]);
}
$filePath = $uploadBaseDir . '/' . $photo['filename'];
if (is_file($filePath)) {
unlink($filePath);
}
$stmt = $db->prepare("
DELETE FROM sample_photos
WHERE idsamplephoto = :idsamplephoto
AND idsample = :idsample
");
$stmt->execute([
':idsamplephoto' => $idsamplephoto,
':idsample' => $idsample,
]);
jsonResponse([
'success' => true,
'message' => 'Photo deleted successfully.'
]);
}
if ($action === 'upload_document') {
if (empty($_FILES['document_file']['name'])) {
jsonResponse([
'success' => false,
'message' => 'No document uploaded.'
]);
}
$documentType = $_POST['document_type'] ?? 'other';
$title = trim($_POST['title'] ?? '');
$expiryDate = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
$notes = trim($_POST['notes'] ?? '');
if (!array_key_exists($documentType, $documentTypes)) {
$documentType = 'other';
}
$originalFilename = $_FILES['document_file']['name'];
$extension = strtolower(pathinfo($originalFilename, PATHINFO_EXTENSION));
$allowedExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'jpg', 'jpeg', 'png', 'webp', 'txt'];
if (!in_array($extension, $allowedExtensions, true)) {
jsonResponse([
'success' => false,
'message' => 'Invalid document format.'
]);
}
if ($title === '') {
$title = pathinfo($originalFilename, PATHINFO_FILENAME);
}
$newFilename = buildDatedFilename('document', $idcompany, $idsample, $originalFilename, $extension);
$relativeFilename = $documentRelativeDir . '/' . $newFilename;
$targetPath = $documentDir . '/' . $newFilename;
if (!move_uploaded_file($_FILES['document_file']['tmp_name'], $targetPath)) {
jsonResponse([
'success' => false,
'message' => 'Unable to upload document.'
]);
}
$stmt = $db->prepare("
INSERT INTO documents (
idcompany,
document_type,
title,
filename,
original_filename,
mime_type,
file_size,
expiry_date,
status,
notes,
uploaded_by,
created_at,
updated_at
) VALUES (
:idcompany,
:document_type,
:title,
:filename,
:original_filename,
:mime_type,
:file_size,
:expiry_date,
'active',
:notes,
:uploaded_by,
NOW(),
NOW()
)
");
$stmt->execute([
':idcompany' => $idcompany,
':document_type' => $documentType,
':title' => $title,
':filename' => $relativeFilename,
':original_filename' => $originalFilename,
':mime_type' => $_FILES['document_file']['type'] ?? null,
':file_size' => $_FILES['document_file']['size'] ?? null,
':expiry_date' => $expiryDate,
':notes' => $notes !== '' ? $notes : null,
':uploaded_by' => $iduserlogin,
]);
$iddocument = (int) $db->lastInsertId();
$stmt = $db->prepare("
INSERT INTO sample_documents (
idsample,
iddocument,
created_at
) VALUES (
:idsample,
:iddocument,
NOW()
)
");
$stmt->execute([
':idsample' => $idsample,
':iddocument' => $iddocument,
]);
jsonResponse([
'success' => true,
'message' => 'Document uploaded successfully.'
]);
}
if ($action === 'delete_document') {
$iddocument = isset($_POST['iddocument']) ? (int) $_POST['iddocument'] : 0;
if ($iddocument <= 0) {
jsonResponse([
'success' => false,
'message' => 'Invalid document id.'
]);
}
$stmt = $db->prepare("
SELECT d.filename
FROM documents d
INNER JOIN sample_documents sd ON sd.iddocument = d.iddocument
WHERE d.iddocument = :iddocument
AND sd.idsample = :idsample
LIMIT 1
");
$stmt->execute([
':iddocument' => $iddocument,
':idsample' => $idsample,
]);
$document = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$document) {
jsonResponse([
'success' => false,
'message' => 'Document not found.'
]);
}
$filePath = $uploadBaseDir . '/' . $document['filename'];
if (is_file($filePath)) {
unlink($filePath);
}
$stmt = $db->prepare("
DELETE FROM documents
WHERE iddocument = :iddocument
");
$stmt->execute([':iddocument' => $iddocument]);
jsonResponse([
'success' => true,
'message' => 'Document deleted successfully.'
]);
}
jsonResponse([
'success' => false,
'message' => 'Unknown action.'
]);
} catch (Throwable $e) {
jsonResponse([
'success' => false,
'message' => $e->getMessage()
]);
}
}
/*
* Page data.
*/
$parts = [];
$photos = [];
$documents = [];
$history = [];
$partners = [];
try {
$stmt = $db->prepare("
SELECT
sp.*,
parent.part_name AS parent_part_name,
bp1.partner_name AS supplier_name,
bp2.partner_name AS producer_name
FROM sample_parts sp
LEFT JOIN sample_parts parent ON parent.idpart = sp.parent_idpart
LEFT JOIN business_partners bp1 ON bp1.idpartner = sp.supplier_id
LEFT JOIN business_partners bp2 ON bp2.idpartner = sp.producer_id
WHERE sp.idsample = :idsample
ORDER BY sp.sort_order ASC, sp.idpart ASC
");
$stmt->execute([':idsample' => $idsample]);
$parts = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$parts = [];
}
try {
$stmt = $db->prepare("
SELECT *
FROM sample_photos
WHERE idsample = :idsample
ORDER BY is_main DESC, sort_order ASC, idsamplephoto DESC
");
$stmt->execute([':idsample' => $idsample]);
$photos = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$photos = [];
}
try {
$stmt = $db->prepare("
SELECT
d.*,
sd.idsampledocument,
u.email AS uploaded_by_email
FROM sample_documents sd
INNER JOIN documents d ON d.iddocument = sd.iddocument
LEFT JOIN auth_users u ON u.id = d.uploaded_by
WHERE sd.idsample = :idsample
ORDER BY d.created_at DESC, d.iddocument DESC
");
$stmt->execute([':idsample' => $idsample]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$documents = [];
}
try {
$stmt = $db->prepare("
SELECT
h.*,
u.email,
u.first_name,
u.last_name
FROM sample_status_history h
LEFT JOIN auth_users u ON u.id = h.changed_by
WHERE h.idsample = :idsample
ORDER BY h.created_at DESC
");
$stmt->execute([':idsample' => $idsample]);
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$history = [];
}
try {
$stmt = $db->prepare("
SELECT idpartner, partner_type, partner_name, status
FROM business_partners
WHERE idcompany = :idcompany
ORDER BY partner_name ASC
");
$stmt->execute([':idcompany' => $idcompany]);
$partners = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$partners = [];
}
$pageTitle = 'Sample Detail';
$statusLabel = ucfirst(str_replace('_', ' ', $sample['status']));
$createdBy = trim(($sample['created_by_first_name'] ?? '') . ' ' . ($sample['created_by_last_name'] ?? ''));
if ($createdBy === '') {
$createdBy = $sample['created_by_email'] ?? '-';
}
?>
= e($pageTitle); ?> - = isset($titlewebsite) ? e($titlewebsite) : 'TRFgo'; ?>
Sample Identity Card
= e($sample['sample_code']); ?> — = e($sample['sample_description']); ?>
Product/sample master record with overview, photos, BOM parts, documents and lifecycle history.
Status
= e($statusLabel); ?>
BOM Parts
= e(count($parts)); ?>
Photos
= e(count($photos)); ?>
Documents
= e(count($documents)); ?>
-
-
-
-
-
Product Information
Main product/sample identity data
Company
= e($sample['company_name']); ?>
Brand
= e($sample['brand_name'] ?: '-'); ?>
Department
= e($sample['department_name'] ?: '-'); ?>
Article No.
= e($sample['article_no'] ?: '-'); ?>
PO No.
= e($sample['po_no'] ?: '-'); ?>
Season
= e($sample['season'] ?: '-'); ?>
Color
= e($sample['color'] ?: '-'); ?>
Production Stage
= e($sample['production_stage'] ?: '-'); ?>
Technical Information
Materials, partners and source data
Producer
= e($sample['producer_name'] ?: '-'); ?>
Supplier
= e($sample['supplier_name'] ?: '-'); ?>
Product Category
= e($sample['product_category'] ?: '-'); ?>
Product Type
= e($sample['product_type'] ?: '-'); ?>
Product Standard
= e($sample['product_standard'] ?: '-'); ?>
Country of Origin
= e($sample['country_of_origin_name'] ?: '-'); ?>
Created By
= e($createdBy); ?>
Created At
= !empty($sample['created_at']) ? e(date('d/m/Y H:i', strtotime($sample['created_at']))) : '-'; ?>
Material Details
Fiber Content
= nl2br(e($sample['fiber_content'] ?: '-')); ?>
Material Description
= nl2br(e($sample['material_description'] ?: '-')); ?>
Sample Photos
Product, label, packaging and detail images
0): ?>
= e($photoTypes[$photo['photo_type']] ?? $photo['photo_type']); ?>
= e($photo['original_filename'] ?: $photo['filename']); ?>
Main
= e($photo['description']); ?>
No photos uploaded yet
Upload product, label, packaging or detail photos.
Parts / BOM
Bill of materials and product components
0): ?>
| Part |
Parent |
Material / Color |
Producer / Supplier |
Qty |
Risk |
Actions |
|
= e($part['part_name']); ?>
Code: = e($part['part_code']); ?>
Position: = e($part['position']); ?>
|
= e($part['parent_part_name'] ?: '-'); ?> |
= e($part['material'] ?: '-'); ?>
= e($part['color'] ?: '-'); ?>
|
= e($part['producer_name'] ?: '-'); ?>
= e($part['supplier_name'] ?: '-'); ?>
|
= e($part['quantity'] ?: '-'); ?>
= e($part['unit'] ?: ''); ?>
|
= e($riskLabel); ?>
|
|
No BOM parts yet
Add parts such as upper, lining, sole, zip, label, packaging or other components.
Documents
Technical sheets, certificates, declarations and related files
0): ?>
| Document |
Type |
Expiry |
Uploaded |
Actions |
|
= e($document['title']); ?>
= e($document['original_filename'] ?: $document['filename']); ?>
|
= e($documentTypes[$document['document_type']] ?? $document['document_type']); ?>
|
= !empty($document['expiry_date']) ? e(date('d/m/Y', strtotime($document['expiry_date']))) : '-'; ?>
|
= !empty($document['created_at']) ? e(date('d/m/Y H:i', strtotime($document['created_at']))) : '-'; ?>
= e($document['uploaded_by_email'] ?: '-'); ?>
|
|
No documents uploaded yet
Upload technical sheets, certificates, declarations or product documentation.
Status History
Sample lifecycle changes
0): ?>
= e(ucfirst(str_replace('_', ' ', $item['new_status']))); ?>
= !empty($item['created_at']) ? e(date('d/m/Y H:i', strtotime($item['created_at']))) : '-'; ?>
|
= e($userName); ?>
= e(ucfirst(str_replace('_', ' ', $item['old_status']))); ?>
→
= e(ucfirst(str_replace('_', ' ', $item['new_status']))); ?>
= e($item['note']); ?>
No history yet
Status changes will appear here.