Deploy Ladill POS / deploy (push) Successful in 42s
Register tills, customer displays, kitchen screens, printers, scanners, and tablets per branch. Customer displays share the branch display token and mark online when the public screen polls; stale devices go offline on a schedule.
194 lines
6.7 KiB
PHP
194 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Public;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\PosCustomerDisplay;
|
|
use App\Models\PosSale;
|
|
use App\Services\Pos\CustomerDisplayService;
|
|
use App\Services\Pos\DeviceService;
|
|
use App\Services\Pos\ReceiptEmailService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
use RuntimeException;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class CustomerDisplayController extends Controller
|
|
{
|
|
public function __construct(
|
|
private CustomerDisplayService $displays,
|
|
private ReceiptEmailService $receiptEmail,
|
|
private DeviceService $devices,
|
|
) {}
|
|
|
|
public function show(string $token): View
|
|
{
|
|
$display = $this->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();
|
|
}
|
|
}
|