find($token); $this->devices->touchByToken($token); return view('pos.customer-display', [ 'display' => $display, 'state' => $this->displays->publicState($display), 'stateUrl' => route('pos.customer-display.state', ['token' => $token]), 'streamUrl' => route('pos.customer-display.stream', ['token' => $token]), 'actionUrl' => route('pos.customer-display.action', ['token' => $token]), ]); } public function state(string $token): JsonResponse { $display = $this->find($token); $this->devices->touchByToken($token); return response()->json($this->displays->publicState($display)); } public function stream(Request $request, string $token): StreamedResponse { $display = $this->find($token); $id = $display->id; $request->session()->save(); $response = new StreamedResponse(function () use ($id) { while (ob_get_level() > 0) { @ob_end_flush(); } @set_time_limit(0); $start = time(); $lastHash = null; while (true) { $display = PosCustomerDisplay::query()->find($id); if (! $display) { echo "event: end\ndata: {}\n\n"; break; } $payload = $this->displays->publicState($display); $json = json_encode($payload); $hash = md5((string) $json); if ($hash !== $lastHash) { echo 'data: '.$json."\n\n"; $lastHash = $hash; } else { echo ": ping\n\n"; } @ob_flush(); @flush(); if (connection_aborted() || (time() - $start) >= 25) { break; } usleep(400000); } }); $response->headers->set('Content-Type', 'text/event-stream'); $response->headers->set('Cache-Control', 'no-cache'); $response->headers->set('Connection', 'keep-alive'); $response->headers->set('X-Accel-Buffering', 'no'); return $response; } public function action(Request $request, string $token): JsonResponse { $display = $this->find($token); $data = $request->validate([ 'type' => ['required', 'string', 'in:tip,signature,receipt_email,payment_option'], 'tip_percent' => ['nullable', 'numeric', 'min:0', 'max:100'], 'tip_minor' => ['nullable', 'integer', 'min:0'], 'signature_data_url' => ['nullable', 'string', 'max:500000'], 'email' => ['nullable', 'email', 'max:255'], 'payment_option' => ['nullable', 'string', 'max:40'], ]); $action = ['type' => $data['type']]; if ($data['type'] === 'tip') { $action['tip_percent'] = $data['tip_percent'] ?? null; $action['tip_minor'] = $data['tip_minor'] ?? null; // Reflect selection on the screen immediately. $payload = $display->payload ?? []; $tip = $payload['tip'] ?? []; $tip['selected_percent'] = $data['tip_percent'] ?? null; $tip['selected_minor'] = $data['tip_minor'] ?? null; $payload['tip'] = $tip; $display->update(['payload' => $payload, 'last_pushed_at' => now()]); } if ($data['type'] === 'signature') { $action['signature_data_url'] = $data['signature_data_url'] ?? null; $payload = $display->payload ?? []; $sig = $payload['signature'] ?? []; $sig['data_url'] = $data['signature_data_url'] ?? null; $payload['signature'] = $sig; $display->update(['payload' => $payload, 'last_pushed_at' => now()]); } if ($data['type'] === 'receipt_email') { $action['email'] = $data['email'] ?? null; $emailResult = $this->emailReceiptForDisplay($display, (string) ($data['email'] ?? '')); $action['email_sent'] = $emailResult['ok']; $action['email_message'] = $emailResult['message']; } if ($data['type'] === 'payment_option') { $action['payment_option'] = $data['payment_option'] ?? null; } $this->displays->recordCustomerAction($display->fresh(), $action); return response()->json([ 'ok' => true, 'state' => $this->displays->publicState($display->fresh()), 'email_sent' => $action['email_sent'] ?? null, 'message' => $action['email_message'] ?? null, ]); } /** @return array{ok: bool, message: string} */ private function emailReceiptForDisplay(PosCustomerDisplay $display, string $email): array { $payload = is_array($display->payload) ? $display->payload : []; $saleId = (int) ($payload['sale_id'] ?? $payload['receipt']['sale_id'] ?? 0); $reference = (string) ($payload['sale_reference'] ?? $payload['receipt']['reference'] ?? ''); $sale = null; if ($saleId > 0) { $sale = PosSale::query() ->where('id', $saleId) ->where('location_id', $display->location_id) ->first(); } if (! $sale && $reference !== '') { $sale = PosSale::query() ->where('reference', $reference) ->where('location_id', $display->location_id) ->first(); } if (! $sale) { return ['ok' => false, 'message' => 'No receipt is ready to email yet.']; } try { return $this->receiptEmail->send($sale, $email); } catch (RuntimeException $e) { return ['ok' => false, 'message' => $e->getMessage()]; } } private function find(string $token): PosCustomerDisplay { return PosCustomerDisplay::query() ->where('token', $token) ->with('location') ->firstOrFail(); } }