Files
GSB-Construction/Modules/MasterData/app/Imports/BoqImport.php

145 lines
4.5 KiB
PHP

<?php
namespace Modules\MasterData\Imports;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
class BoqImport implements ToCollection
{
private array $header = [];
private array $items = [];
private array $errors = [];
/**
* Real file layout:
* Rows 0-3: Header (A=label, B=value) → PROJECT NAME, OWNER NAME, LOCATION, DATE
* Row 4: Empty
* Row 5: "BILL OF QUANTITIES" title
* Row 6: Column headers (Item no. | Description of Materials | UNIT | APPROVED QUANTITY | REQUEST QUANTITY | SYSTEM /WORK AREA)
* Row 7+: Data rows
*
* Mapping:
* B = material name (filter out "Description of Materials")
* C = unit
* D = APPROVED QUANTITY → actual_qty
* E = REQUEST QUANTITY → planned_qty
* F = SYSTEM /WORK AREA → notes
*/
public function collection(Collection $rows): void
{
// Parse header section (rows 0-3 → A=label, B=value)
foreach ($rows->slice(0, 4) as $row) {
$label = strtolower(trim($row[0] ?? ''));
$value = trim($row[1] ?? '');
match (true) {
str_contains($label, 'project') => $this->header['project_name'] = $value,
str_contains($label, 'owner') => $this->header['owner_name'] = $value,
str_contains($label, 'location') => $this->header['location'] = $value,
str_contains($label, 'date') => $this->header['date'] = $value,
default => null,
};
}
// Find the detail table header row by scanning for "Description of Materials" in column B
$dataStartIndex = null;
foreach ($rows as $index => $row) {
$colB = strtolower(trim($row[1] ?? ''));
if (str_contains($colB, 'description') && str_contains($colB, 'material')) {
$dataStartIndex = $index + 1;
break;
}
}
// Fallback: look for "Item" in column A
if ($dataStartIndex === null) {
foreach ($rows as $index => $row) {
$colA = strtolower(trim($row[0] ?? ''));
if (str_contains($colA, 'item') && str_contains($colA, 'no')) {
$dataStartIndex = $index + 1;
break;
}
}
}
if ($dataStartIndex === null) {
$this->errors[] = 'Could not find the detail table header row. Expected "Description of Materials" in column B.';
return;
}
// Parse detail rows
$itemNumber = 0;
foreach ($rows->slice($dataStartIndex) as $row) {
$materialName = trim($row[1] ?? '');
// Skip empty rows and the header text itself
if (empty($materialName)) {
continue;
}
// Filter out rows that contain the header text
if (str_contains(strtolower($materialName), 'description of material')) {
continue;
}
$itemNumber++;
$unit = strtolower(trim($row[2] ?? 'pcs'));
$approvedQty = $this->parseNumber($row[3] ?? 0);
$requestQty = $this->parseNumber($row[4] ?? 0);
$workArea = trim($row[5] ?? '');
$errors = [];
if ($requestQty <= 0 && $approvedQty <= 0) {
$errors[] = 'No quantity specified';
}
$this->items[] = [
'item_no' => $row[0] ?? $itemNumber,
'material' => $materialName,
'unit' => $unit,
'approved_qty' => $approvedQty,
'request_qty' => $requestQty,
'work_area' => $workArea,
'errors' => $errors,
];
}
}
private function parseNumber(mixed $value): float
{
if (is_numeric($value)) {
return (float) $value;
}
// Handle comma-formatted numbers like "2,970.00"
$cleaned = str_replace(',', '', (string) $value);
return is_numeric($cleaned) ? (float) $cleaned : 0;
}
public function getHeader(): array
{
return $this->header;
}
public function getItems(): array
{
return $this->items;
}
public function getErrors(): array
{
return $this->errors;
}
public function hasErrors(): bool
{
if (!empty($this->errors)) return true;
foreach ($this->items as $item) {
if (!empty($item['errors'])) return true;
}
return false;
}
}