Sidebar with all 8 nav groups (Operations / Planning / People / QR / Areas / Finance / Reports / Settings) per the admin-panel-flow spec. Disabled items show "Soon" and toast on click. Working pages (real API): - Dashboard with live stat cards + recent verification queue - Households (filter, approve, reject with reason) - Service Areas (CRUD via slide-over) - QR Batches (generate, mark printed, link to print PDF) - QR Code Search (lifecycle lookup by serial) - Drop-off Points (filter + create) - Dumpsites, Routes, All Users (filterable lists) Shared helpers on window.Verde — apiFetch, requireAuth, logout, toast, escapeHtml, formatDate. Tailwind brand tokens, table/card/badge classes, slide-over panels. GenerateBatchRequest now accepts target_area_id by uuid for consistency with the rest of the public API. 139 tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
5.2 KiB
PHP
160 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Admin;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Http\Requests\Qr\GenerateBatchRequest;
|
|
use App\Http\Resources\QrCodeBatchResource;
|
|
use App\Http\Resources\QrCodeResource;
|
|
use App\Models\QrCodeBatch;
|
|
use App\Models\ServiceArea;
|
|
use App\Services\Qr\BatchGenerator;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
use Endroid\QrCode\Builder\Builder;
|
|
use Endroid\QrCode\ErrorCorrectionLevel;
|
|
use Endroid\QrCode\Writer\PngWriter;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Picqer\Barcode\BarcodeGeneratorPNG;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class AdminQrBatchController extends ApiController
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'purpose' => ['nullable', 'in:free_allocation,store_inventory,promotional'],
|
|
'target_area_id' => ['nullable', 'integer'],
|
|
'q' => ['nullable', 'string', 'max:100'],
|
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
|
]);
|
|
|
|
$perPage = (int) $request->input('per_page', 25);
|
|
|
|
$batches = QrCodeBatch::query()
|
|
->with(['targetArea', 'createdBy'])
|
|
->withCount('codes')
|
|
->when($request->filled('purpose'), fn ($q) => $q->where('purpose', $request->string('purpose')))
|
|
->when(
|
|
$request->filled('target_area_id'),
|
|
fn ($q) => $q->where('target_area_id', $request->integer('target_area_id')),
|
|
)
|
|
->when(
|
|
$request->filled('q'),
|
|
fn ($q) => $q->where('batch_number', 'like', '%'.$request->string('q').'%'),
|
|
)
|
|
->orderByDesc('id')
|
|
->paginate($perPage);
|
|
|
|
return $this->ok(
|
|
QrCodeBatchResource::collection($batches),
|
|
null,
|
|
[
|
|
'page' => $batches->currentPage(),
|
|
'per_page' => $batches->perPage(),
|
|
'total' => $batches->total(),
|
|
'last_page' => $batches->lastPage(),
|
|
],
|
|
);
|
|
}
|
|
|
|
public function store(GenerateBatchRequest $request, BatchGenerator $generator): JsonResponse
|
|
{
|
|
$data = $request->validated();
|
|
$area = isset($data['target_area_id'])
|
|
? ServiceArea::where('uuid', $data['target_area_id'])->first()
|
|
: null;
|
|
|
|
$batch = $generator->generate(
|
|
quantity: (int) $data['quantity'],
|
|
purpose: $data['purpose'],
|
|
targetArea: $area,
|
|
targetStoreId: $data['target_store_id'] ?? null,
|
|
createdBy: $request->user(),
|
|
expiresAt: isset($data['expires_at']) ? \Carbon\Carbon::parse($data['expires_at']) : null,
|
|
notes: $data['notes'] ?? null,
|
|
);
|
|
|
|
$batch->load(['targetArea', 'createdBy', 'codes']);
|
|
|
|
return $this->created(new QrCodeBatchResource($batch), 'Batch generated');
|
|
}
|
|
|
|
public function show(QrCodeBatch $qrCodeBatch): JsonResponse
|
|
{
|
|
$qrCodeBatch->load(['targetArea', 'createdBy', 'codes']);
|
|
|
|
return $this->ok(new QrCodeBatchResource($qrCodeBatch));
|
|
}
|
|
|
|
public function markPrinted(QrCodeBatch $qrCodeBatch): JsonResponse
|
|
{
|
|
if ($qrCodeBatch->printed_at) {
|
|
return $this->fail('Batch already marked as printed', null, 422);
|
|
}
|
|
|
|
$qrCodeBatch->forceFill(['printed_at' => now()])->save();
|
|
|
|
return $this->ok(
|
|
new QrCodeBatchResource($qrCodeBatch->fresh()->load(['targetArea', 'createdBy', 'codes'])),
|
|
'Batch marked as printed',
|
|
);
|
|
}
|
|
|
|
public function showCode(string $serial): JsonResponse
|
|
{
|
|
$code = \App\Models\QrCode::query()
|
|
->with(['batch.targetArea', 'household.head', 'usedAtDropOff', 'scannedBy'])
|
|
->where('serial', $serial)
|
|
->firstOrFail();
|
|
|
|
return $this->ok(new QrCodeResource($code));
|
|
}
|
|
|
|
public function printPdf(QrCodeBatch $qrCodeBatch): Response
|
|
{
|
|
$codes = $qrCodeBatch->codes()->orderBy('id')->get();
|
|
|
|
$rendered = $codes->map(function ($c) {
|
|
return [
|
|
'serial' => $c->serial,
|
|
'qr_data_uri' => $this->qrDataUri($c->serial),
|
|
'barcode_data_uri' => $this->barcodeDataUri($c->serial),
|
|
];
|
|
})->all();
|
|
|
|
$perPage = 24;
|
|
$pages = array_chunk($rendered, $perPage);
|
|
|
|
$pdf = Pdf::loadView('pdf.qr-batch', [
|
|
'batch' => $qrCodeBatch,
|
|
'pages' => $pages,
|
|
])->setPaper('a4');
|
|
|
|
$filename = "verde-{$qrCodeBatch->batch_number}.pdf";
|
|
|
|
return $pdf->download($filename);
|
|
}
|
|
|
|
private function qrDataUri(string $serial): string
|
|
{
|
|
$result = (new Builder(
|
|
writer: new PngWriter(),
|
|
data: $serial,
|
|
errorCorrectionLevel: ErrorCorrectionLevel::Medium,
|
|
size: 140,
|
|
margin: 4,
|
|
))->build();
|
|
|
|
return $result->getDataUri();
|
|
}
|
|
|
|
private function barcodeDataUri(string $serial): string
|
|
{
|
|
$generator = new BarcodeGeneratorPNG();
|
|
$png = $generator->getBarcode($serial, BarcodeGeneratorPNG::TYPE_CODE_128, 1, 28);
|
|
|
|
return 'data:image/png;base64,'.base64_encode($png);
|
|
}
|
|
}
|