accountQuery() ->latest() ->get() ->map(fn (QrCode $code) => $this->present($code)); return response()->json(['data' => $codes]); } public function show(QrCode $qrCode): JsonResponse { $this->ensurePlusCode($qrCode); $this->authorize('view', $qrCode); return response()->json(['data' => $this->present($qrCode, detailed: true)]); } public function analytics(QrCode $qrCode): JsonResponse { $this->ensurePlusCode($qrCode); $this->authorize('view', $qrCode); return response()->json([ 'data' => [ 'summary' => $this->analytics->summaryFor($qrCode), 'daily_scans' => $this->analytics->dailyScans($qrCode, 30)->values(), 'devices' => $this->analytics->breakdown($qrCode, 'device_type'), 'browsers' => $this->analytics->breakdown($qrCode, 'browser'), ], ]); } public function store(Request $request): JsonResponse { abort_unless($request->user()->tokenCan('qr:write'), 403, 'Token requires qr:write ability.'); $validated = $request->validate([ 'label' => ['required', 'string', 'max:120'], 'type' => ['required', 'in:'.implode(',', QrTypeCatalog::keys())], 'destination_url' => ['nullable', 'url', 'max:2048'], 'custom_short_code' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$/', 'unique:qr_codes,short_code'], 'is_active' => ['sometimes', 'boolean'], ]); if ($validated['type'] === QrCode::TYPE_DOCUMENT) { return response()->json([ 'message' => 'PDF QR codes must be created in the QR Plus app (file upload required).', ], 422); } try { $qrCode = $this->manager->create(ladill_account(), array_merge( $request->all(), $validated, )); } catch (RuntimeException $e) { return response()->json(['message' => $e->getMessage()], 422); } return response()->json(['data' => $this->present($qrCode->fresh(), detailed: true)], 201); } public function update(Request $request, QrCode $qrCode): JsonResponse { abort_unless($request->user()->tokenCan('qr:write'), 403, 'Token requires qr:write ability.'); $this->ensurePlusCode($qrCode); $this->authorize('update', $qrCode); $request->validate([ 'label' => ['sometimes', 'string', 'max:120'], 'destination_url' => ['nullable', 'url', 'max:2048'], 'is_active' => ['sometimes', 'boolean'], ]); if ($qrCode->isDocumentType() && $request->hasFile('document')) { return response()->json([ 'message' => 'Replace PDF files in the QR Plus app.', ], 422); } try { $updated = $this->manager->update($qrCode, $request->all()); } catch (RuntimeException $e) { return response()->json(['message' => $e->getMessage()], 422); } return response()->json(['data' => $this->present($updated->fresh(), detailed: true)]); } /** @return \Illuminate\Database\Eloquent\Builder */ private function accountQuery() { return QrCode::query() ->where('user_id', ladill_account()->id) ->whereIn('type', QrTypeCatalog::eventTypes()); } private function ensurePlusCode(QrCode $qrCode): void { abort_unless( $qrCode->user_id === ladill_account()->id && QrTypeCatalog::isValid($qrCode->type), 404, ); } /** @return array */ private function present(QrCode $qrCode, bool $detailed = false): array { $data = [ 'id' => $qrCode->id, 'label' => $qrCode->label, 'type' => $qrCode->type, 'type_label' => $qrCode->typeLabel(), 'short_code' => $qrCode->short_code, 'public_url' => $qrCode->publicUrl(), 'is_active' => $qrCode->is_active, 'scans_total' => $qrCode->scans_total, 'unique_scans_total' => $qrCode->unique_scans_total, 'last_scanned_at' => $qrCode->last_scanned_at?->toIso8601String(), 'created_at' => $qrCode->created_at?->toIso8601String(), 'updated_at' => $qrCode->updated_at?->toIso8601String(), ]; if ($detailed) { $data['destination_url'] = $qrCode->destination_url; $data['content'] = $qrCode->content(); } return $data; } }