validate([ 'purpose' => ['nullable', 'in:free_allocation,store_inventory,promotional'], 'household_id' => ['nullable', 'integer'], 'q' => ['nullable', 'string', 'max:100'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:500'], ]); $perPage = (int) $request->input('per_page', 25); $batches = QrCodeBatch::query() ->with(['household.head', 'createdBy']) ->withCount([ 'codes', 'codes as active_codes_count' => fn ($q) => $q->where('status', 'active'), 'codes as used_codes_count' => fn ($q) => $q->where('status', 'used'), 'codes as voided_codes_count' => fn ($q) => $q->where('status', 'voided'), ]) ->when($request->filled('purpose'), fn ($q) => $q->where('purpose', $request->string('purpose'))) ->when( $request->filled('household_id'), fn ($q) => $q->where('household_id', $request->integer('household_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(); $household = Household::where('uuid', $data['household_id'])->firstOrFail(); $batch = $generator->generate( quantity: (int) $data['quantity'], purpose: $data['purpose'], targetArea: null, targetStoreId: $data['target_store_id'] ?? null, createdBy: $request->user(), expiresAt: isset($data['expires_at']) ? Carbon::parse($data['expires_at']) : null, notes: $data['notes'] ?? null, householdId: $household->id, ); $batch->load(['household.head', 'createdBy', 'codes']); return $this->created(new QrCodeBatchResource($batch), 'Batch generated'); } public function show(QrCodeBatch $qrCodeBatch): JsonResponse { $qrCodeBatch->load(['household.head', '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 voidBatch(QrCodeBatch $qrCodeBatch): JsonResponse { // Update all codes that aren't already used $qrCodeBatch->codes() ->whereNotIn('status', ['used']) ->update(['status' => 'voided']); return $this->ok( new QrCodeBatchResource($qrCodeBatch->fresh()->load(['household.head', 'createdBy', 'codes'])), 'Batch voided successfully', ); } public function destroy(QrCodeBatch $qrCodeBatch): JsonResponse { // Delete any unused/unassigned codes to free up space $qrCodeBatch->codes() ->whereNotIn('status', ['used', 'active']) ->delete(); // Soft delete the batch itself $qrCodeBatch->delete(); return $this->ok(null, 'Batch deleted successfully'); } public function showCode(string $serial): JsonResponse { $code = 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 { try { $result = (new Builder( writer: new PngWriter, data: $serial, errorCorrectionLevel: ErrorCorrectionLevel::Medium, size: 140, margin: 4, ))->build(); return $result->getDataUri(); } catch (\Throwable $e) { if (class_exists(SvgWriter::class)) { $result = (new Builder( writer: new SvgWriter, data: $serial, errorCorrectionLevel: ErrorCorrectionLevel::Medium, size: 140, margin: 4, ))->build(); return 'data:image/svg+xml;base64,'.base64_encode($result->getString()); } throw $e; } } private function barcodeDataUri(string $serial): string { try { $generator = new BarcodeGeneratorPNG; $png = $generator->getBarcode($serial, BarcodeGeneratorPNG::TYPE_CODE_128, 1, 28); return 'data:image/png;base64,'.base64_encode($png); } catch (\Throwable $e) { if (class_exists(BarcodeGeneratorSVG::class)) { $generator = new BarcodeGeneratorSVG; $svg = $generator->getBarcode($serial, BarcodeGeneratorSVG::TYPE_CODE_128, 1, 28); return 'data:image/svg+xml;base64,'.base64_encode($svg); } throw $e; } } }