Align POS header with other apps: AI assistant, register button, sidebar
Deploy Ladill POS / deploy (push) Successful in 47s
Deploy Ladill POS / deploy (push) Successful in 47s
- Add the Afia AI assistant to the header (topbar AI button + slide-over), matching every other Ladill app. Ports config/afia.php, AfiaService, a POS AiController (pos.ai.chat) scoped to register/sales/products, and includes the slide-over in the app layout. The afia() Alpine component already existed. - Hide the header "Open register" button while on the register page. - Move Settings to a pinned bottom section of the sidebar, like other apps. - Commit the updated POS logo + launcher icon assets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b7c7a32ee6
commit
e9a0c92308
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Pos;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Pos\Concerns\ScopesToAccount;
|
||||
use App\Models\PosProduct;
|
||||
use App\Models\PosSale;
|
||||
use App\Services\Afia\AfiaService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AiController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function chat(Request $request, AfiaService $afia): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'message' => ['required', 'string', 'max:2000'],
|
||||
'history' => ['nullable', 'array', 'max:20'],
|
||||
'history.*.role' => ['nullable', 'string'],
|
||||
'history.*.text' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if (! $afia->enabled()) {
|
||||
return response()->json(['message' => 'Afia is not available right now.'], 503);
|
||||
}
|
||||
|
||||
try {
|
||||
$reply = $afia->chat(trim($validated['message']), $validated['history'] ?? [], $this->context($request));
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json(['message' => 'Afia could not respond right now. Please try again.'], 502);
|
||||
}
|
||||
|
||||
return response()->json(['reply' => $reply]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function context(Request $request): array
|
||||
{
|
||||
$owner = $this->ownerRef($request);
|
||||
$currency = strtoupper((string) config('pos.default_currency', 'GHS'));
|
||||
|
||||
$todaySales = PosSale::owned($owner)
|
||||
->where('status', PosSale::STATUS_PAID)
|
||||
->where('paid_at', '>=', now()->startOfDay());
|
||||
|
||||
$todayTotalMinor = (int) (clone $todaySales)->sum('total_minor');
|
||||
|
||||
return [
|
||||
'signed_in' => 'yes',
|
||||
'today_takings' => $currency.' '.number_format($todayTotalMinor / 100, 2),
|
||||
'today_sales_count' => (clone $todaySales)->count(),
|
||||
'active_products' => PosProduct::owned($owner)->active()->count(),
|
||||
'pending_sales' => PosSale::owned($owner)->where('status', PosSale::STATUS_PENDING)->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Afia;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Afia — Ladill in-app AI assistant scoped to POS. Calls the configured
|
||||
* provider (OpenAI/Anthropic) directly with the app's own key.
|
||||
*/
|
||||
class AfiaService
|
||||
{
|
||||
public function enabled(): bool
|
||||
{
|
||||
return (bool) config('afia.enabled', true) && (string) config('afia.api_key', '') !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{role:string,text:string}> $history
|
||||
* @param array<string,mixed> $context
|
||||
*/
|
||||
public function chat(string $message, array $history, array $context): string
|
||||
{
|
||||
if (! $this->enabled()) {
|
||||
throw new RuntimeException('Afia is not configured.');
|
||||
}
|
||||
|
||||
$provider = (string) config('afia.provider', 'openai');
|
||||
$model = (string) config('afia.model', 'gpt-4o-mini');
|
||||
$apiKey = (string) config('afia.api_key');
|
||||
|
||||
$messages = [['role' => 'system', 'content' => $this->systemPrompt($context)]];
|
||||
foreach (array_slice($history, -8) as $turn) {
|
||||
$role = ($turn['role'] ?? 'user') === 'assistant' ? 'assistant' : 'user';
|
||||
$text = trim((string) ($turn['text'] ?? ''));
|
||||
if ($text !== '') {
|
||||
$messages[] = ['role' => $role, 'content' => $text];
|
||||
}
|
||||
}
|
||||
$messages[] = ['role' => 'user', 'content' => $message];
|
||||
|
||||
return $provider === 'anthropic'
|
||||
? $this->viaAnthropic($model, $apiKey, $messages)
|
||||
: $this->viaOpenAi($model, $apiKey, $messages);
|
||||
}
|
||||
|
||||
private function viaOpenAi(string $model, string $apiKey, array $messages): string
|
||||
{
|
||||
$res = Http::withToken($apiKey)->acceptJson()->timeout(45)
|
||||
->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => $model,
|
||||
'temperature' => 0.3,
|
||||
'max_tokens' => 600,
|
||||
'messages' => $messages,
|
||||
]);
|
||||
|
||||
if ($res->failed()) {
|
||||
throw new RuntimeException('OpenAI request failed: '.$res->status());
|
||||
}
|
||||
|
||||
return trim((string) $res->json('choices.0.message.content', ''));
|
||||
}
|
||||
|
||||
private function viaAnthropic(string $model, string $apiKey, array $messages): string
|
||||
{
|
||||
$system = $messages[0]['content'];
|
||||
$turns = array_values(array_filter($messages, fn ($m) => $m['role'] !== 'system'));
|
||||
|
||||
$res = Http::withHeaders([
|
||||
'x-api-key' => $apiKey,
|
||||
'anthropic-version' => '2023-06-01',
|
||||
])->acceptJson()->timeout(45)->post('https://api.anthropic.com/v1/messages', [
|
||||
'model' => $model,
|
||||
'max_tokens' => 600,
|
||||
'system' => $system,
|
||||
'messages' => array_map(fn ($m) => ['role' => $m['role'], 'content' => $m['content']], $turns),
|
||||
]);
|
||||
|
||||
if ($res->failed()) {
|
||||
throw new RuntimeException('Anthropic request failed: '.$res->status());
|
||||
}
|
||||
|
||||
return trim((string) $res->json('content.0.text', ''));
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $context */
|
||||
private function systemPrompt(array $context): string
|
||||
{
|
||||
$ctx = collect($context)->map(fn ($v, $k) => "- {$k}: {$v}")->implode("\n");
|
||||
|
||||
return <<<PROMPT
|
||||
You are Afia, the assistant inside Ladill POS (pos.ladill.com).
|
||||
Help users run the in-store register — taking sales, charging by cash or Ladill Pay, managing the product catalog, and reading their sales history. Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
|
||||
|
||||
What Ladill POS does and where things live:
|
||||
- Overview (Dashboard): today's takings, number of sales, active products, and recent sales.
|
||||
- Register: the point-of-sale screen — add catalog products or a quick custom amount to the cart, then charge by cash or Ladill Pay (MoMo/card).
|
||||
- Products: the local catalog (name, SKU, price). Products can also be imported from Ladill CRM in Settings.
|
||||
- Sales: history of completed sales with receipts; a paid sale can be turned into a Ladill Invoice.
|
||||
- Settings: store details, default currency, and importing products from CRM or the Merchant storefront.
|
||||
|
||||
Rules:
|
||||
- Only answer questions about Ladill POS — the register, sales, products, checkout (cash/Ladill Pay), receipts, and settings.
|
||||
- If asked about contacts, invoices, domains, hosting, email mailboxes, or QR codes, briefly say those live in other Ladill apps and suggest the app launcher.
|
||||
- Never invent figures (takings, counts) — use the user context below or tell them where to check.
|
||||
|
||||
Current user context:
|
||||
{$ctx}
|
||||
PROMPT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Afia — Ladill in-app AI assistant, scoped to POS. Provisioned per app via
|
||||
// its own key (falls back to OPENAI_API_KEY). See App\Services\Afia\AfiaService.
|
||||
'product' => env('AFIA_PRODUCT', 'pos'),
|
||||
'enabled' => (bool) env('AFIA_ENABLED', true),
|
||||
'provider' => env('AFIA_PROVIDER', 'openai'), // openai | anthropic
|
||||
'model' => env('AFIA_MODEL', 'gpt-4o-mini'),
|
||||
'api_key' => env('AFIA_API_KEY', env('OPENAI_API_KEY')),
|
||||
];
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #868f77;
|
||||
fill: #2a9779;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
@@ -23,9 +23,8 @@
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="cls-4" cx="99.4" cy="109" r="98"/>
|
||||
<rect class="cls-2" x="1.4" y="11" width="83.2" height="338"/>
|
||||
<path class="cls-3" d="M84.6,205.7V12.2C37.5,19.4,1.4,59.9,1.4,109s36.1,89.6,83.2,96.7Z"/>
|
||||
<path class="cls-1" d="M123.4,245c-.1,2-.3,4-.3,6,0,54.1,43.9,98,98,98s98-43.9,98-98-.2-4-.3-6H123.4Z"/>
|
||||
<path class="cls-1" d="M162.9,238.5c-.1-2-.3-4-.3-6,0-54.1,43.9-98,98-98s98,43.9,98,98-.2,4-.3,6h-195.3Z"/>
|
||||
<path class="cls-1" d="M111.4,249.7c-.1,2.1-.3,4.2-.3,6.3,0,56.7,46,102.7,102.7,102.7s102.7-46,102.7-102.7-.2-4.2-.3-6.3H111.4Z"/>
|
||||
<path class="cls-3" d="M152.8,264.8c-.1-2.1-.3-4.2-.3-6.3,0-56.7,46-102.7,102.7-102.7s102.7,46,102.7,102.7-.2,4.2-.3,6.3h-204.9Z"/>
|
||||
<path class="cls-2" d="M111,43c-2.1-.1-4.2-.3-6.3-.3C48,42.7,2,88.7,2,145.5s46,102.7,102.7,102.7,4.2-.2,6.3-.3V43Z"/>
|
||||
<path class="cls-4" d="M94.8,1.6c2.1-.1,4.2-.3,6.3-.3,56.7,0,102.7,46,102.7,102.7s-46,102.7-102.7,102.7-4.2-.2-6.3-.3V1.6Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 976 B |
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 415.1 76.2">
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 411.6 76.2">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #868f77;
|
||||
fill: #2a9779;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
@@ -28,21 +28,20 @@
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="cls-3" d="M136.4,53.9v8.6h-34.8V12.2h8.6v41.8h26.2,0Z"/>
|
||||
<path class="cls-3" d="M140.1,44.5c0-2.7.5-5.3,1.5-7.7s2.4-4.4,4.2-6.2,3.9-3.1,6.2-4.2c2.4-1,4.9-1.5,7.7-1.5s5.3.5,7.7,1.5,4.4,2.4,6.2,4.2c1.8,1.8,3.1,3.8,4.2,6.2,1,2.4,1.5,4.9,1.5,7.7v18.1h-8.4v-2c-3.3,2.4-7.1,3.5-11.2,3.5s-5.7-.5-8.1-1.5-4.5-2.4-6.2-4.2-3-3.9-4-6.2c-.9-2.4-1.4-4.9-1.4-7.7,0,0,0,0,.1,0ZM148.5,44.5c0,1.5.3,3,.9,4.4s1.4,2.6,2.4,3.6,2.2,1.8,3.6,2.4c1.4.6,2.8.9,4.4.9s3-.3,4.4-.8c1.4-.6,2.6-1.3,3.6-2.3s1.8-2.2,2.4-3.6c.6-1.4.9-2.9.9-4.5s-.3-3-.9-4.4c-.6-1.4-1.4-2.6-2.4-3.6s-2.2-1.8-3.6-2.4c-1.4-.6-2.8-.9-4.4-.9s-3,.3-4.4.9-2.6,1.4-3.6,2.4-1.8,2.2-2.4,3.6c-.6,1.4-.9,2.8-.9,4.4h0Z"/>
|
||||
<path class="cls-3" d="M223.2,44.5c0,2.8-.5,5.4-1.5,7.7-1,2.4-2.4,4.4-4.2,6.2s-3.9,3.1-6.2,4.1-4.9,1.5-7.7,1.5-5.3-.5-7.7-1.5-4.4-2.4-6.2-4.2-3.1-3.8-4.2-6.2c-1-2.4-1.5-4.9-1.5-7.7s.5-5.3,1.5-7.7,2.4-4.4,4.2-6.2,3.8-3.1,6.2-4.2c2.4-1,4.9-1.5,7.7-1.5s2.1.1,3.2.4c1.1.2,2.1.6,3.1.9,1,.4,1.9.8,2.8,1.3.9.5,1.6,1,2.2,1.6V12.2h8.4v32.3h-.1ZM214.8,44.5c0-1.5-.3-3-.9-4.4-.6-1.4-1.4-2.6-2.4-3.6s-2.2-1.8-3.6-2.4c-1.4-.6-2.8-.9-4.4-.9s-3,.3-4.4.9c-1.4.6-2.6,1.4-3.6,2.4s-1.8,2.2-2.4,3.6c-.6,1.4-.9,2.8-.9,4.4s.3,3,.9,4.4c.6,1.4,1.4,2.6,2.4,3.6s2.2,1.8,3.6,2.4c1.4.6,2.8.9,4.4.9s3-.3,4.4-.9c1.4-.6,2.6-1.4,3.6-2.4s1.8-2.2,2.4-3.6c.6-1.4.9-2.8.9-4.4Z"/>
|
||||
<path class="cls-3" d="M227.7,17.4c0-1.5.5-2.8,1.5-3.8s2.2-1.5,3.7-1.5,2.8.5,3.8,1.5,1.5,2.3,1.5,3.8-.5,2.7-1.5,3.7-2.3,1.5-3.8,1.5-2.7-.5-3.7-1.5-1.5-2.2-1.5-3.7ZM228.7,26.6h8.4v36h-8.4V26.6Z"/>
|
||||
<path class="cls-3" d="M242.8,12.2h8.4v50.4h-8.4V12.2Z"/>
|
||||
<path class="cls-3" d="M256.9,12.2h8.4v50.4h-8.4V12.2Z"/>
|
||||
<path class="cls-3" d="M132.4,53.9v8.6h-34.8V12.2h8.6v41.8h26.2,0Z"/>
|
||||
<path class="cls-3" d="M136.1,44.5c0-2.7.5-5.3,1.5-7.7s2.4-4.4,4.2-6.2,3.9-3.1,6.2-4.2c2.4-1,4.9-1.5,7.7-1.5s5.3.5,7.7,1.5,4.4,2.4,6.2,4.2c1.8,1.8,3.1,3.8,4.2,6.2,1,2.4,1.5,4.9,1.5,7.7v18.1h-8.4v-2c-3.3,2.4-7.1,3.5-11.2,3.5s-5.7-.5-8.1-1.5-4.5-2.4-6.2-4.2-3-3.9-4-6.2c-.9-2.4-1.4-4.9-1.4-7.7,0,0,0,0,.1,0ZM144.5,44.5c0,1.5.3,3,.9,4.4s1.4,2.6,2.4,3.6,2.2,1.8,3.6,2.4c1.4.6,2.8.9,4.4.9s3-.3,4.4-.8c1.4-.6,2.6-1.3,3.6-2.3s1.8-2.2,2.4-3.6c.6-1.4.9-2.9.9-4.5s-.3-3-.9-4.4c-.6-1.4-1.4-2.6-2.4-3.6s-2.2-1.8-3.6-2.4c-1.4-.6-2.8-.9-4.4-.9s-3,.3-4.4.9-2.6,1.4-3.6,2.4-1.8,2.2-2.4,3.6c-.6,1.4-.9,2.8-.9,4.4h0Z"/>
|
||||
<path class="cls-3" d="M219.2,44.5c0,2.8-.5,5.4-1.5,7.7-1,2.4-2.4,4.4-4.2,6.2s-3.9,3.1-6.2,4.1-4.9,1.5-7.7,1.5-5.3-.5-7.7-1.5-4.4-2.4-6.2-4.2-3.1-3.8-4.2-6.2c-1-2.4-1.5-4.9-1.5-7.7s.5-5.3,1.5-7.7,2.4-4.4,4.2-6.2,3.8-3.1,6.2-4.2c2.4-1,4.9-1.5,7.7-1.5s2.1.1,3.2.4c1.1.2,2.1.6,3.1.9,1,.4,1.9.8,2.8,1.3.9.5,1.6,1,2.2,1.6V12.2h8.4v32.3h-.1ZM210.8,44.5c0-1.5-.3-3-.9-4.4-.6-1.4-1.4-2.6-2.4-3.6s-2.2-1.8-3.6-2.4c-1.4-.6-2.8-.9-4.4-.9s-3,.3-4.4.9c-1.4.6-2.6,1.4-3.6,2.4s-1.8,2.2-2.4,3.6c-.6,1.4-.9,2.8-.9,4.4s.3,3,.9,4.4c.6,1.4,1.4,2.6,2.4,3.6s2.2,1.8,3.6,2.4c1.4.6,2.8.9,4.4.9s3-.3,4.4-.9c1.4-.6,2.6-1.4,3.6-2.4s1.8-2.2,2.4-3.6c.6-1.4.9-2.8.9-4.4Z"/>
|
||||
<path class="cls-3" d="M223.7,17.4c0-1.5.5-2.8,1.5-3.8s2.2-1.5,3.7-1.5,2.8.5,3.8,1.5,1.5,2.3,1.5,3.8-.5,2.7-1.5,3.7-2.3,1.5-3.8,1.5-2.7-.5-3.7-1.5-1.5-2.2-1.5-3.7ZM224.7,26.6h8.4v36h-8.4V26.6Z"/>
|
||||
<path class="cls-3" d="M238.8,12.2h8.4v50.4h-8.4V12.2Z"/>
|
||||
<path class="cls-3" d="M252.9,12.2h8.4v50.4h-8.4V12.2Z"/>
|
||||
</g>
|
||||
<circle class="cls-5" cx="22.1" cy="22.1" r="22.1"/>
|
||||
<rect class="cls-2" width="18.1" height="76.2"/>
|
||||
<path class="cls-4" d="M18.8,43.9V.3C8.1,1.9,0,11,0,22.1s8.1,20.2,18.8,21.8Z"/>
|
||||
<path class="cls-1" d="M27.5,52.8c0,.4,0,.9,0,1.3,0,12.2,9.9,22.1,22.1,22.1s22.1-9.9,22.1-22.1,0-.9,0-1.3H27.5Z"/>
|
||||
<path class="cls-1" d="M36.4,51.3c0-.4,0-.9,0-1.3,0-12.2,9.9-22.1,22.1-22.1s22.1,9.9,22.1,22.1,0,.9,0,1.3h-44Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M286.8,12.9h15c5.2,0,9.3,1.2,12.2,3.6,2.9,2.4,4.3,6.4,4.3,11.7s-.4,6-1.1,7.6c-.7,1.6-1.8,3.1-3.2,4.3-3,2.4-7.1,3.5-12.2,3.5h-9.9v19.6h-5V12.9ZM310.1,36.5c1.9-1.5,2.9-4.3,2.9-8.3s-1-6.5-2.9-8.1c-2.1-1.5-4.8-2.3-8.1-2.3h-10.1v21h10.1c3.4,0,6.1-.8,8.1-2.3Z"/>
|
||||
<path class="cls-3" d="M321.8,38.1c0-3.7.7-7.2,2.1-10.4,1.4-3.3,3.3-6.1,5.8-8.6,2.4-2.4,5.3-4.4,8.6-5.8,3.3-1.4,6.7-2.1,10.4-2.1s7.2.7,10.4,2.1c3.3,1.4,6.1,3.3,8.6,5.8,2.4,2.4,4.4,5.3,5.8,8.6,1.4,3.3,2.1,6.7,2.1,10.4s-.7,7.2-2.1,10.4c-1.4,3.3-3.3,6.1-5.8,8.6-2.4,2.4-5.3,4.4-8.6,5.8-3.3,1.4-6.7,2.1-10.4,2.1s-7.2-.7-10.4-2.1c-3.3-1.4-6.1-3.3-8.6-5.8-2.4-2.4-4.4-5.3-5.8-8.6-1.4-3.3-2.1-6.7-2.1-10.4ZM326.9,38.1c0,3,.6,5.8,1.7,8.5,1.2,2.7,2.7,5,4.7,7,2,2,4.3,3.6,7,4.7,2.7,1.2,5.5,1.7,8.5,1.7s5.8-.6,8.4-1.7c2.6-1.2,5-2.7,6.9-4.7,2-2,3.6-4.3,4.7-7,1.2-2.7,1.7-5.5,1.7-8.5s-.6-5.8-1.7-8.4c-1.2-2.6-2.7-5-4.7-7-2-2-4.3-3.6-6.9-4.7-2.6-1.2-5.4-1.7-8.4-1.7s-5.8.6-8.5,1.7-5,2.7-7,4.7c-2,2-3.6,4.3-4.7,7-1.2,2.6-1.7,5.5-1.7,8.4Z"/>
|
||||
<path class="cls-3" d="M410,50.4c0-2.4-.6-4.2-1.9-5.7s-2.9-2.6-4.9-3.6c-2-.9-4.2-1.8-6.6-2.5-2.4-.7-4.7-1.6-6.8-2.6-2.2-1-4.1-2.2-5.7-3.6-1.6-1.5-2.7-3.4-3.2-5.8-.2-2.1,0-4,.9-5.9.8-1.9,2-3.5,3.6-4.9,1.6-1.4,3.4-2.5,5.5-3.4,2.1-.8,4.4-1.3,6.7-1.3s4.8.5,6.8,1.4c1.9.9,3.6,2,4.9,3.3,1.3,1.3,2.4,2.6,3.1,4,.7,1.4,1.2,2.6,1.3,3.5h-5.5c-.9-2.3-2.4-4.1-4.3-5.3-2-1.2-4.1-1.8-6.3-1.9-1.5,0-3,.2-4.5.7-1.5.5-2.8,1.2-3.9,2.2-1.1.9-2,2-2.6,3.2-.6,1.2-.8,2.6-.5,4,.4,1.7,1.4,3,2.9,4,1.6,1,3.4,1.9,5.6,2.6,2.2.8,4.4,1.5,6.8,2.3,2.4.8,4.6,1.8,6.6,3,2,1.2,3.7,2.8,5,4.7,1.3,1.9,2,4.4,2,7.5s-.5,4.4-1.4,6.2c-1,1.8-2.2,3.4-3.9,4.6-1.6,1.2-3.5,2.2-5.6,2.8-2.1.6-4.3,1-6.6,1s-4.6-.4-6.8-1.2c-2.2-.8-4.2-2-5.8-3.4-1.7-1.5-3-3.2-4.1-5.3-1-2.1-1.5-4.3-1.5-6.8h5c0,1.6.3,3.2,1,4.6.7,1.4,1.6,2.7,2.8,3.7,1.2,1.1,2.6,1.9,4.2,2.5,1.6.6,3.3.9,5.1.9,1.6,0,3.2-.3,4.7-.6,1.5-.4,2.8-1,4-1.8s2.1-1.8,2.8-3c.7-1.2,1-2.6,1-4.2Z"/>
|
||||
<path class="cls-3" d="M282.8,12.9h15c5.2,0,9.3,1.2,12.2,3.6,2.9,2.4,4.3,6.4,4.3,11.7s-.4,6-1.1,7.6c-.7,1.6-1.8,3.1-3.2,4.3-3,2.4-7.1,3.5-12.2,3.5h-9.9v19.6h-5V12.9ZM306.1,36.5c1.9-1.5,2.9-4.3,2.9-8.3s-1-6.5-2.9-8.1c-2.1-1.5-4.8-2.3-8.1-2.3h-10.1v21h10.1c3.4,0,6.1-.8,8.1-2.3Z"/>
|
||||
<path class="cls-3" d="M317.8,38.1c0-3.7.7-7.2,2.1-10.4,1.4-3.3,3.3-6.1,5.8-8.6,2.4-2.4,5.3-4.4,8.6-5.8,3.3-1.4,6.7-2.1,10.4-2.1s7.2.7,10.4,2.1c3.3,1.4,6.1,3.3,8.6,5.8,2.4,2.4,4.4,5.3,5.8,8.6,1.4,3.3,2.1,6.7,2.1,10.4s-.7,7.2-2.1,10.4c-1.4,3.3-3.3,6.1-5.8,8.6-2.4,2.4-5.3,4.4-8.6,5.8-3.3,1.4-6.7,2.1-10.4,2.1s-7.2-.7-10.4-2.1c-3.3-1.4-6.1-3.3-8.6-5.8-2.4-2.4-4.4-5.3-5.8-8.6-1.4-3.3-2.1-6.7-2.1-10.4ZM322.9,38.1c0,3,.6,5.8,1.7,8.5,1.2,2.7,2.7,5,4.7,7,2,2,4.3,3.6,7,4.7,2.7,1.2,5.5,1.7,8.5,1.7s5.8-.6,8.4-1.7c2.6-1.2,5-2.7,6.9-4.7,2-2,3.6-4.3,4.7-7,1.2-2.7,1.7-5.5,1.7-8.5s-.6-5.8-1.7-8.4c-1.2-2.6-2.7-5-4.7-7-2-2-4.3-3.6-6.9-4.7-2.6-1.2-5.4-1.7-8.4-1.7s-5.8.6-8.5,1.7-5,2.7-7,4.7c-2,2-3.6,4.3-4.7,7-1.2,2.6-1.7,5.5-1.7,8.4Z"/>
|
||||
<path class="cls-3" d="M406,50.4c0-2.4-.6-4.2-1.9-5.7s-2.9-2.6-4.9-3.6c-2-.9-4.2-1.8-6.6-2.5-2.4-.7-4.7-1.6-6.8-2.6-2.2-1-4.1-2.2-5.7-3.6-1.6-1.5-2.7-3.4-3.2-5.8-.2-2.1,0-4,.9-5.9.8-1.9,2-3.5,3.6-4.9,1.6-1.4,3.4-2.5,5.5-3.4,2.1-.8,4.4-1.3,6.7-1.3s4.8.5,6.8,1.4c1.9.9,3.6,2,4.9,3.3,1.3,1.3,2.4,2.6,3.1,4,.7,1.4,1.2,2.6,1.3,3.5h-5.5c-.9-2.3-2.4-4.1-4.3-5.3-2-1.2-4.1-1.8-6.3-1.9-1.5,0-3,.2-4.5.7-1.5.5-2.8,1.2-3.9,2.2-1.1.9-2,2-2.6,3.2-.6,1.2-.8,2.6-.5,4,.4,1.7,1.4,3,2.9,4,1.6,1,3.4,1.9,5.6,2.6,2.2.8,4.4,1.5,6.8,2.3,2.4.8,4.6,1.8,6.6,3,2,1.2,3.7,2.8,5,4.7,1.3,1.9,2,4.4,2,7.5s-.5,4.4-1.4,6.2c-1,1.8-2.2,3.4-3.9,4.6-1.6,1.2-3.5,2.2-5.6,2.8-2.1.6-4.3,1-6.6,1s-4.6-.4-6.8-1.2c-2.2-.8-4.2-2-5.8-3.4-1.7-1.5-3-3.2-4.1-5.3-1-2.1-1.5-4.3-1.5-6.8h5c0,1.6.3,3.2,1,4.6.7,1.4,1.6,2.7,2.8,3.7,1.2,1.1,2.6,1.9,4.2,2.5,1.6.6,3.3.9,5.1.9,1.6,0,3.2-.3,4.7-.6,1.5-.4,2.8-1,4-1.8s2.1-1.8,2.8-3c.7-1.2,1-2.6,1-4.2Z"/>
|
||||
</g>
|
||||
<path class="cls-1" d="M23.3,53c0,.4,0,.9,0,1.3,0,12.1,9.8,21.9,21.9,21.9s21.9-9.8,21.9-21.9,0-.9,0-1.3H23.3Z"/>
|
||||
<path class="cls-4" d="M32.1,56.2c0-.4,0-.9,0-1.3,0-12.1,9.8-21.9,21.9-21.9s21.9,9.8,21.9,21.9,0,.9,0,1.3h-43.7Z"/>
|
||||
<path class="cls-2" d="M23.2,8.9c-.4,0-.9,0-1.3,0C9.8,8.8,0,18.6,0,30.7s9.8,21.9,21.9,21.9.9,0,1.3,0V8.9Z"/>
|
||||
<path class="cls-5" d="M19.8,0c.4,0,.9,0,1.3,0,12.1,0,21.9,9.8,21.9,21.9s-9.8,21.9-21.9,21.9-.9,0-1.3,0V0Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -27,5 +27,6 @@
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@include('partials.afia')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
@props(['compact' => false])
|
||||
|
||||
<button type="button"
|
||||
@click="window.dispatchEvent(new CustomEvent('afia-open'))"
|
||||
@class([
|
||||
'inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-700 via-blue-600 to-emerald-400 text-sm font-semibold text-white shadow-sm transition hover:opacity-95',
|
||||
'h-9 w-9 px-0 lg:h-auto lg:w-auto lg:px-4 lg:py-2' => $compact,
|
||||
'px-3 py-2 lg:px-4' => ! $compact,
|
||||
])
|
||||
aria-label="Open Afia AI assistant">
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2.25c.414 0 .75.336.75.75a5.25 5.25 0 0 0 5.25 5.25.75.75 0 0 1 0 1.5A5.25 5.25 0 0 0 12.75 15a.75.75 0 0 1-1.5 0A5.25 5.25 0 0 0 6 9.75a.75.75 0 0 1 0-1.5A5.25 5.25 0 0 0 11.25 3a.75.75 0 0 1 .75-.75Zm6.75 11.25a.75.75 0 0 1 .75.75A2.25 2.25 0 0 0 21.75 16.5a.75.75 0 0 1 0 1.5A2.25 2.25 0 0 0 19.5 20.25a.75.75 0 0 1-1.5 0A2.25 2.25 0 0 0 15.75 18a.75.75 0 0 1 0-1.5A2.25 2.25 0 0 0 18 14.25a.75.75 0 0 1 .75-.75ZM5.25 14.25a.75.75 0 0 1 .75.75 3 3 0 0 0 3 3 .75.75 0 0 1 0 1.5 3 3 0 0 0-3 3 .75.75 0 0 1-1.5 0 3 3 0 0 0-3-3 .75.75 0 0 1 0-1.5 3 3 0 0 0 3-3 .75.75 0 0 1 .75-.75Z"/>
|
||||
</svg>
|
||||
<span @class(['sr-only lg:not-sr-only lg:inline' => $compact, 'hidden sm:inline' => ! $compact])>AI</span>
|
||||
</button>
|
||||
@@ -0,0 +1,106 @@
|
||||
@php
|
||||
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about taking a sale, charging by cash or Ladill Pay, managing products, or your sales history…";
|
||||
$afiaSuggestions = [
|
||||
'How do I take a sale?',
|
||||
'How do I charge with Ladill Pay?',
|
||||
'How do I add a product?',
|
||||
'How do I import products from CRM?',
|
||||
];
|
||||
@endphp
|
||||
{{-- Afia — Ladill AI assistant slide-over. Opened via $dispatch('afia-open'). --}}
|
||||
<div x-data="afia({
|
||||
chatUrl: '{{ route('pos.ai.chat') }}',
|
||||
csrf: '{{ csrf_token() }}',
|
||||
greeting: @js($afiaGreeting),
|
||||
suggestions: @js($afiaSuggestions),
|
||||
})" @afia-open.window="open = true; $nextTick(() => $refs.input && $refs.input.focus())">
|
||||
<div x-show="open" x-cloak @click="close()" class="fixed inset-0 z-50 bg-slate-900/20"></div>
|
||||
|
||||
<section x-show="open" x-cloak
|
||||
x-transition:enter="transition transform ease-out duration-200"
|
||||
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transition transform ease-in duration-150"
|
||||
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||
class="fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3.5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="relative flex h-8 w-8 shrink-0 items-center justify-center">
|
||||
<svg viewBox="0 0 36 36" class="h-8 w-8" aria-hidden="true">
|
||||
<defs>
|
||||
<radialGradient id="afia-orb" cx="40%" cy="35%" r="60%">
|
||||
<stop offset="0%" stop-color="#a5b4fc"/>
|
||||
<stop offset="50%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#3730a3"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="18" cy="18" r="14" fill="url(#afia-orb)">
|
||||
<animate attributeName="r" values="14;14.8;14" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="18" cy="18" r="11" fill="none" stroke="#c7d2fe" stroke-width=".6" opacity=".5">
|
||||
<animate attributeName="r" values="11;12.5;11" dur="4s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values=".5;.15;.5" dur="4s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle r="1.2" fill="#c7d2fe" opacity=".8">
|
||||
<animateMotion dur="3s" repeatCount="indefinite" path="M18,8 A10,10 0 1,1 17.99,8" rotate="auto"/>
|
||||
</circle>
|
||||
</svg>
|
||||
<span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-white bg-emerald-400"></span>
|
||||
</span>
|
||||
<div class="leading-tight">
|
||||
<p class="text-sm font-semibold text-slate-900">Afia</p>
|
||||
<p class="text-[11px] text-slate-400">POS assistant</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<a href="{{ ladill_account_url('ai') }}"
|
||||
class="rounded-lg px-2 py-1 text-[11px] font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-700"
|
||||
title="AI usage history on your Ladill account">History</a>
|
||||
<button @click="close()" class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-600">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-4 py-4" x-ref="scroll">
|
||||
<div class="space-y-3">
|
||||
<template x-for="(m, i) in messages" :key="i">
|
||||
<div class="flex" :class="m.role === 'user' ? 'justify-end' : 'justify-start'">
|
||||
<div class="max-w-[85%] whitespace-pre-line rounded-2xl px-3.5 py-2.5 text-[13px] leading-relaxed"
|
||||
:class="m.role === 'user' ? 'bg-indigo-600 text-white rounded-br-md' : 'bg-slate-100 text-slate-700 rounded-bl-md'"
|
||||
x-html="m.text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\*\*([^*]+?)\*\*/g,'<strong>$1</strong>')"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="loading" class="flex justify-start">
|
||||
<div class="rounded-2xl rounded-bl-md bg-slate-100 px-4 py-3">
|
||||
<div class="flex gap-1">
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay:0ms"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay:150ms"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay:300ms"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="messages.length <= 1 && suggestions.length" class="mt-4 space-y-2">
|
||||
<template x-for="s in suggestions" :key="s">
|
||||
<button @click="useSuggestion(s)" :disabled="loading"
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-left text-[13px] font-medium text-slate-700 transition hover:border-indigo-200 hover:bg-indigo-50 disabled:opacity-50">
|
||||
<span class="text-indigo-400">→</span><span x-text="s"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 px-4 pb-4 pt-3">
|
||||
<form @submit.prevent="send()" class="flex items-end gap-2">
|
||||
<input type="text" x-ref="input" x-model="input" :disabled="loading" placeholder="Message Afia…"
|
||||
class="flex-1 rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 text-sm text-slate-700 placeholder-slate-400 focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
<button type="submit" :disabled="loading || !input.trim()"
|
||||
class="btn-fab shrink-0 disabled:opacity-40">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-2 text-center text-[10px] text-slate-400">Afia can make mistakes — verify important details.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -14,8 +14,6 @@
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />'],
|
||||
['name' => 'Sales', 'route' => route('pos.sales.index'), 'active' => request()->routeIs('pos.sales.*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />'],
|
||||
['name' => 'Settings', 'route' => route('pos.settings'), 'active' => request()->routeIs('pos.settings*'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />'],
|
||||
];
|
||||
@endphp
|
||||
<nav class="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||
@@ -26,4 +24,14 @@
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
<div class="shrink-0 border-t border-slate-100 px-3 py-3">
|
||||
<a href="{{ route('pos.settings') }}"
|
||||
class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ request()->routeIs('pos.settings*') ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
||||
<svg class="h-[18px] w-[18px] shrink-0 {{ request()->routeIs('pos.settings*') ? 'text-indigo-600' : 'text-slate-400' }}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
<span class="flex-1 truncate">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{--
|
||||
Standard desktop top-right widgets:
|
||||
notifications → launcher → divider → avatar dropdown.
|
||||
AI → notifications → launcher → divider → avatar dropdown.
|
||||
--}}
|
||||
@php
|
||||
$topbarUser = $user ?? auth()->user();
|
||||
@@ -12,6 +12,8 @@
|
||||
$showUserHeader = $showUser ?? true;
|
||||
@endphp
|
||||
|
||||
@includeIf('partials.afia-button', ['compact' => true])
|
||||
|
||||
@includeIf('partials.notification-dropdown')
|
||||
|
||||
@include('partials.launcher')
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
</button>
|
||||
<h1 class="truncate text-base font-semibold text-slate-900 lg:hidden">{{ $heading ?? 'Ladill POS' }}</h1>
|
||||
@unless(request()->routeIs('pos.register'))
|
||||
<a href="{{ route('pos.register') }}" class="hidden rounded-xl bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 lg:inline-flex">Open register</a>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Auth\SsoLoginController;
|
||||
use App\Http\Controllers\Pos\AiController;
|
||||
use App\Http\Controllers\Pos\DashboardController;
|
||||
use App\Http\Controllers\Pos\ProductController;
|
||||
use App\Http\Controllers\Pos\RegisterController;
|
||||
@@ -28,6 +29,8 @@ Route::get('/sales/{sale}/callback', [SaleController::class, 'callback'])->name(
|
||||
Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::get('/wallet/balance', [WalletBalanceController::class, 'balance'])->name('wallet.balance');
|
||||
|
||||
Route::post('/ai/chat', [AiController::class, 'chat'])->middleware('throttle:30,1')->name('pos.ai.chat');
|
||||
|
||||
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
|
||||
Route::get('/notifications/unread', [NotificationController::class, 'unread'])->name('notifications.unread');
|
||||
Route::post('/notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.mark-read');
|
||||
|
||||
Reference in New Issue
Block a user