MiknasTrading/app/Services/SupplierImportService.php

273 lines
9.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use App\Models\Supplier;
use PhpOffice\PhpSpreadsheet\IOFactory;
class SupplierImportService
{
private array $mrfStopWords = ['comments if any', 'lowest price', 'avg price', 'recommendation'];
public function import(string $filePath): array
{
$spreadsheet = IOFactory::load($filePath);
$format = $this->detectFormat($spreadsheet);
$rows = $format === 'mrf'
? $this->extractFromMrf($spreadsheet)
: $this->extractFromTemplate($spreadsheet);
$imported = 0;
$updated = 0;
$skipped = 0;
foreach ($rows as $data) {
$name = trim($data['name'] ?? '');
if (empty($name)) {
continue;
}
$existing = Supplier::whereRaw('LOWER(name) = ?', [strtolower($name)])->first();
if ($existing) {
// Update the existing record with any new/missing fields
$existing->update($this->buildAttributes($data, true));
$updated++;
} else {
Supplier::create($this->buildAttributes($data, false));
$imported++;
}
}
return [
'imported' => $imported,
'updated' => $updated,
'skipped' => $skipped,
'format' => $format,
];
}
private function buildAttributes(array $data, bool $isUpdate): array
{
$attrs = [
'contact_person' => $this->str($data['contact_person'] ?? ''),
'email' => $this->str($data['email'] ?? ''),
'secondary_email' => $this->str($data['secondary_email'] ?? ''),
'phone' => $this->normalizePhone($data['phone'] ?? ''),
'phone2' => $this->normalizePhone($data['phone2'] ?? ''),
'whatsapp' => $this->normalizePhone($data['whatsapp'] ?? ''),
'address' => $this->str($data['address'] ?? ''),
'website' => $this->str($data['website'] ?? ''),
'tax_number' => $this->str($data['tax_number'] ?? ''),
'credit_terms' => $this->str($data['credit_terms'] ?? ''),
'credit_days' => $this->parseCreditDays($data['credit_days'] ?? ''),
'remarks' => $this->str($data['remarks'] ?? ''),
'is_active' => $this->parseBoolean($data['is_active'] ?? 'yes'),
];
if (!$isUpdate) {
$attrs['name'] = trim($data['name']);
$attrs['supplier_code'] = $this->str($data['supplier_code'] ?? '');
$attrs['category'] = $this->str($data['category'] ?? '');
} else {
// On update: only fill category/code if not already set
if (!empty($data['supplier_code'])) {
$attrs['supplier_code'] = $this->str($data['supplier_code']);
}
if (!empty($data['category'])) {
$attrs['category'] = $this->str($data['category']);
}
}
// Remove null-equivalent values so we don't overwrite real data with blanks
return array_filter($attrs, fn($v) => $v !== null && $v !== '');
}
// ── Format detection ────────────────────────────────────────────────────
private function detectFormat(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet): string
{
$sheet = $spreadsheet->getActiveSheet();
$a4 = strtolower(trim((string) $sheet->getCell('A4')->getValue()));
return $a4 === 's.no' ? 'mrf' : 'template';
}
// ── MRF extraction ──────────────────────────────────────────────────────
private function extractFromMrf(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet): array
{
$sheet = $spreadsheet->getActiveSheet();
$suppliers = [];
for ($col = 7; $col <= 50; $col++) {
$coord = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col) . '4';
$value = trim((string) $sheet->getCell($coord)->getValue());
if (empty($value) || in_array(strtolower($value), $this->mrfStopWords)) {
break;
}
$suppliers[] = ['name' => $value];
}
return $suppliers;
}
// ── Template / Unified format extraction ────────────────────────────────
private function extractFromTemplate(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet): array
{
$rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false);
$headers = array_map(
fn($h) => strtolower(trim(str_replace(['*', '_'], [' ', ' '], (string) $h))),
$rows[0] ?? []
);
// Map normalised header text → internal field name
$aliases = [
'supplier id' => 'supplier_code',
'name' => 'name',
'company name' => 'name',
'category' => 'category',
'contact person' => 'contact_person',
'primary email' => 'email',
'email' => 'email',
'secondary email' => 'secondary_email',
'phone 1' => 'phone',
'phone' => 'phone',
'phone 2' => 'phone2',
'whatsapp number' => 'whatsapp',
'whatsapp' => 'whatsapp',
'address' => 'address',
'website' => 'website',
'credit (y/n)' => 'credit_terms',
'credit days' => 'credit_days',
'tax number' => 'tax_number',
'is active' => 'is_active',
'remarks / key details' => 'remarks',
'remarks' => 'remarks',
];
$map = [];
foreach ($headers as $idx => $header) {
if (isset($aliases[$header]) && !isset($map[$aliases[$header]])) {
$map[$aliases[$header]] = $idx;
}
}
if (!isset($map['name'])) {
return [];
}
$suppliers = [];
foreach (array_slice($rows, 1) as $row) {
$name = trim((string) ($row[$map['name']] ?? ''));
if (empty($name) || str_starts_with($name, '*')) {
continue;
}
if (!$this->looksLikeSupplierName($name)) {
continue;
}
$entry = ['name' => $name];
foreach (array_keys($aliases) as $alias) {
$field = $aliases[$alias];
if ($field === 'name') continue;
if (isset($map[$field]) && !isset($entry[$field])) {
$entry[$field] = trim((string) ($row[$map[$field]] ?? ''));
}
}
$suppliers[] = $entry;
}
return $suppliers;
}
// ── Helpers ─────────────────────────────────────────────────────────────
/**
* Reject rows that are specification text, not company names.
* Patterns: starts with digit, measurement strings, known junk phrases.
*/
private function looksLikeSupplierName(string $name): bool
{
// Starts with a digit → quantity/spec row
if (preg_match('/^\d/', $name)) {
return false;
}
$lower = strtolower($name);
$junkPhrases = [
'gdcd minimum', 'approx. quantity', 'approx quantity',
'per 200 m', 'per 30 m', 'coverage mandatory',
'hydrant within', 'smoke extraction', 'mandatory for',
'required for buildings', '', '—',
];
foreach ($junkPhrases as $phrase) {
if (str_contains($lower, $phrase)) {
return false;
}
}
return true;
}
/**
* Normalise phone numbers to +COUNTRYCODELOCAL (digits only after +).
* Examples: "+973 3318 8311" → "+97333188311"
* "39209304" (8-digit Bahrain local) → "+97339209304"
*/
public function normalizePhone(string $raw): string
{
$raw = trim($raw);
if ($raw === '') {
return '';
}
// Strip parenthetical extensions: (ext 118), (ext 201, 202)
$val = preg_replace('/\(\s*ext[^)]*\)/i', '', $raw);
// Strip trailing extensions: " Ext. 7438", "x 26", "- 26" (1-4 digits at end after dash/space)
$val = preg_replace('/\s+(?:ext\.?|x)\s*[\d,\s]+$/i', '', $val);
$val = preg_replace('/\s*[-]\s*\d{1,4}\s*$/', '', $val);
// Extract only digits
$digits = preg_replace('/[^\d]/', '', $val);
if ($digits === '') {
return '';
}
// 8-digit Bahrain local number → prepend 973
if (strlen($digits) === 8) {
return '+973' . $digits;
}
// Already has country code as prefix
return '+' . $digits;
}
private function parseCreditDays(string $val): ?int
{
$digits = preg_replace('/[^\d]/', '', $val);
return $digits !== '' ? (int) $digits : null;
}
private function parseBoolean(string $value): bool
{
return in_array(strtolower(trim($value)), ['yes', 'true', '1', 'active', 'y']);
}
private function str(string $val): ?string
{
$v = trim($val);
return $v !== '' ? $v : null;
}
}