145 lines
4.5 KiB
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;
|
|
}
|
|
}
|