Add bulk CSV product import for POS (retail + restaurant)
Deploy Ladill POS / deploy (push) Successful in 1m39s
Deploy Ladill POS / deploy (push) Successful in 1m39s
Customers with large catalogs can upload a CSV instead of adding products one by one. Mode-aware: restaurant imports into the local catalog (chunked INSERT, categories resolved/created by name); retail batches to the CRM products/bulk endpoint. Streamed parsing + batched writes handle thousands of rows in one request. Includes a downloadable template, an Import button on both product pages, and a skipped-rows report. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
62e396195b
commit
9fcea24680
@@ -11,9 +11,12 @@ use App\Models\PosProduct;
|
||||
use App\Models\PosSaleLine;
|
||||
use App\Models\PosStation;
|
||||
use App\Services\Crm\CrmClient;
|
||||
use App\Services\Import\PosProductCsvImportService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* Products are mode-aware:
|
||||
@@ -119,6 +122,62 @@ class ProductController extends Controller
|
||||
return redirect()->route('pos.products.index')->with('success', 'Product removed.');
|
||||
}
|
||||
|
||||
/** Bulk CSV import — upload form (mode-aware target catalog). */
|
||||
public function importForm(Request $request): View
|
||||
{
|
||||
return view('pos.products.import', [
|
||||
'restaurant' => $this->isRestaurant($request),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Bulk CSV import — handle the upload. */
|
||||
public function import(Request $request, PosProductCsvImportService $import): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => ['required', 'file', 'mimes:csv,txt', 'max:10240'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = $import->import(
|
||||
$this->ownerRef($request),
|
||||
$request->file('file'),
|
||||
$this->isRestaurant($request),
|
||||
);
|
||||
} catch (RuntimeException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$message = "Imported {$result['imported']} product(s).";
|
||||
if ($result['skipped'] > 0) {
|
||||
$message .= " Skipped {$result['skipped']}.";
|
||||
}
|
||||
|
||||
return redirect()->route('pos.products.index')
|
||||
->with('success', $message)
|
||||
->with('import_errors', array_slice($result['errors'], 0, 50));
|
||||
}
|
||||
|
||||
/** Bulk CSV import — downloadable template with example rows. */
|
||||
public function importTemplate(Request $request): StreamedResponse
|
||||
{
|
||||
$restaurant = $this->isRestaurant($request);
|
||||
$filename = 'pos-products-template.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($restaurant) {
|
||||
$out = fopen('php://output', 'wb');
|
||||
if ($restaurant) {
|
||||
fputcsv($out, ['name', 'sku', 'price', 'currency', 'category', 'active']);
|
||||
fputcsv($out, ['Coffee', 'CFE-01', '15.00', 'GHS', 'Drinks', 'yes']);
|
||||
fputcsv($out, ['Croissant', 'CRS-02', '8.50', 'GHS', 'Pastry', 'yes']);
|
||||
} else {
|
||||
fputcsv($out, ['name', 'sku', 'price', 'currency', 'description', 'tax_rate', 'active']);
|
||||
fputcsv($out, ['Mug', 'MUG-01', '25.00', 'GHS', 'Ceramic mug', '0', 'yes']);
|
||||
fputcsv($out, ['T-Shirt', 'TSH-02', '60.00', 'GHS', 'Cotton tee', '0', 'yes']);
|
||||
}
|
||||
fclose($out);
|
||||
}, $filename, ['Content-Type' => 'text/csv']);
|
||||
}
|
||||
|
||||
private function isRestaurant(Request $request): bool
|
||||
{
|
||||
return PosLocation::owned($this->ownerRef($request))
|
||||
|
||||
Reference in New Issue
Block a user