Replace Upgrade to Pro with Report Issue for staff.
Deploy Ladill Queue / deploy (push) Successful in 38s

Non-owners submit Queue issues to Ladill admin via Identity.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-14 22:23:48 +00:00
co-authored by Cursor
parent 6e66f7c584
commit 7c8a8f889e
5 changed files with 155 additions and 9 deletions
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Qms;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
use App\Services\Identity\IdentityTeamClient;
use App\Support\StaffUx;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class IssueReportController extends Controller
{
use ScopesToAccount;
public function create(Request $request): View
{
abort_unless(! StaffUx::showBilling($request->user()), 403);
return view('qms.issues.create');
}
public function store(Request $request, IdentityTeamClient $identity): RedirectResponse
{
abort_unless(! StaffUx::showBilling($request->user()), 403);
$validated = $request->validate([
'subject' => ['required', 'string', 'max:255'],
'message' => ['required', 'string', 'max:5000'],
'priority' => ['required', 'in:low,normal,high'],
]);
$user = $request->user();
$organization = $this->organization($request);
$member = $this->member($request);
$context = trim(implode("\n", array_filter([
'App: Ladill Queue',
'Organization: '.($organization->name ?? '—'),
'Role: '.($member?->role ?? '—'),
'Reporter: '.($user->email ?? $user->ownerRef()),
'',
$validated['message'],
])));
try {
$ticket = $identity->createSupportTicket($user->ownerRef(), [
'subject' => $validated['subject'],
'message' => $context,
'priority' => $validated['priority'],
'tags' => ['staff_issue', 'app:queue'],
], 'queue');
} catch (\Throwable $e) {
report($e);
return back()
->withInput()
->with('error', 'Could not send your report right now. Please try again in a moment.');
}
$ref = (string) ($ticket['ticket_ref'] ?? '');
return redirect()
->route('qms.dashboard')
->with('success', $ref !== ''
? "Issue reported to Ladill support ({$ref}). Our team will follow up."
: 'Issue reported to Ladill support. Our team will follow up.');
}
}
@@ -78,6 +78,24 @@ class IdentityTeamClient
return (array) $response->json('data', []);
}
/**
* @param array{subject: string, message: string, priority?: string, tags?: list<string>} $payload
* @return array<string, mixed>
*/
public function createSupportTicket(string $userPublicId, array $payload, string $app = 'queue'): array
{
$response = $this->request('post', '/identity/support/tickets', array_filter([
'user' => $userPublicId,
'subject' => $payload['subject'] ?? '',
'message' => $payload['message'] ?? '',
'priority' => $payload['priority'] ?? 'normal',
'app' => $app,
'tags' => $payload['tags'] ?? ['staff_issue'],
], fn ($value) => $value !== null && $value !== ''));
return (array) $response->json('data', []);
}
private function request(string $method, string $path, array $query = []): Response
{
$url = rtrim((string) config('identity.api_url'), '/').$path;
+20 -9
View File
@@ -100,16 +100,27 @@
</a>
@endif
@php $proActive = request()->routeIs('qms.pro.*'); @endphp
@if (! empty($hasPaidPlan))
<a href="{{ route('qms.pro.index') }}" class="group {{ $permissions->can($member, 'settings.view') ? 'mt-0.5' : '' }} flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $proActive ? '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 text-amber-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25c.3 0 .58.18.7.46l2.36 5.5 5.96.5a.75.75 0 0 1 .43 1.31l-4.53 3.9 1.36 5.83a.75.75 0 0 1-1.12.81L12 17.77l-5.16 3a.75.75 0 0 1-1.12-.81l1.36-5.83-4.53-3.9a.75.75 0 0 1 .43-1.31l5.96-.5 2.36-5.5c.12-.28.4-.46.7-.46Z"/></svg>
<span>{{ ! empty($isEnterprise) ? 'Queue Enterprise' : 'Queue Pro' }}</span>
</a>
@php
$proActive = request()->routeIs('qms.pro.*');
$issueActive = request()->routeIs('qms.issues.*');
$isAccountOwner = \App\Support\StaffUx::showBilling(auth()->user());
@endphp
@if ($isAccountOwner)
@if (! empty($hasPaidPlan))
<a href="{{ route('qms.pro.index') }}" class="group {{ $permissions->can($member, 'settings.view') ? 'mt-0.5' : '' }} flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $proActive ? '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 text-amber-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25c.3 0 .58.18.7.46l2.36 5.5 5.96.5a.75.75 0 0 1 .43 1.31l-4.53 3.9 1.36 5.83a.75.75 0 0 1-1.12.81L12 17.77l-5.16 3a.75.75 0 0 1-1.12-.81l1.36-5.83-4.53-3.9a.75.75 0 0 1 .43-1.31l5.96-.5 2.36-5.5c.12-.28.4-.46.7-.46Z"/></svg>
<span>{{ ! empty($isEnterprise) ? 'Queue Enterprise' : 'Queue Pro' }}</span>
</a>
@else
<a href="{{ route('qms.pro.index') }}" class="group {{ $permissions->can($member, 'settings.view') ? 'mt-0.5' : '' }} flex items-center gap-3 rounded-lg bg-gradient-to-r from-indigo-600 to-emerald-500 px-3 py-2 text-[13px] font-semibold text-white shadow-sm transition hover:opacity-95">
<svg class="h-[18px] w-[18px] shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25c.3 0 .58.18.7.46l2.36 5.5 5.96.5a.75.75 0 0 1 .43 1.31l-4.53 3.9 1.36 5.83a.75.75 0 0 1-1.12.81L12 17.77l-5.16 3a.75.75 0 0 1-1.12-.81l1.36-5.83-4.53-3.9a.75.75 0 0 1 .43-1.31l5.96-.5 2.36-5.5c.12-.28.4-.46.7-.46Z"/></svg>
<span>Upgrade to Pro</span>
</a>
@endif
@else
<a href="{{ route('qms.pro.index') }}" class="group {{ $permissions->can($member, 'settings.view') ? 'mt-0.5' : '' }} flex items-center gap-3 rounded-lg bg-gradient-to-r from-indigo-600 to-emerald-500 px-3 py-2 text-[13px] font-semibold text-white shadow-sm transition hover:opacity-95">
<svg class="h-[18px] w-[18px] shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.25c.3 0 .58.18.7.46l2.36 5.5 5.96.5a.75.75 0 0 1 .43 1.31l-4.53 3.9 1.36 5.83a.75.75 0 0 1-1.12.81L12 17.77l-5.16 3a.75.75 0 0 1-1.12-.81l1.36-5.83-4.53-3.9a.75.75 0 0 1 .43-1.31l5.96-.5 2.36-5.5c.12-.28.4-.46.7-.46Z"/></svg>
<span>Upgrade to Pro</span>
<a href="{{ route('qms.issues.create') }}" class="group mt-0.5 flex items-center gap-3 rounded-lg bg-gradient-to-r from-indigo-600 to-emerald-500 px-3 py-2 text-[13px] font-semibold text-white shadow-sm transition hover:opacity-95 {{ $issueActive ? 'ring-2 ring-indigo-300' : '' }}">
<svg class="h-[18px] w-[18px] shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>
<span>Report Issue</span>
</a>
@endif
</div>
@@ -0,0 +1,43 @@
<x-app-layout title="Report issue">
<div class="mx-auto max-w-lg space-y-6">
<div>
<h1 class="text-xl font-semibold text-slate-900">Report an issue</h1>
<p class="mt-1 text-sm text-slate-500">Send a problem to Ladill support. Account owners manage billing upgrades staff use this for help with Queue.</p>
</div>
@if (session('error'))
<p class="rounded-lg bg-rose-50 px-3 py-2 text-sm text-rose-800">{{ session('error') }}</p>
@endif
<form method="POST" action="{{ route('qms.issues.store') }}" class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700">Subject</label>
<input type="text" name="subject" value="{{ old('subject') }}" required maxlength="255"
class="mt-1 w-full rounded-lg border-slate-300 text-sm"
placeholder="e.g. Ticket handoff failed">
@error('subject')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Priority</label>
<select name="priority" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="normal" @selected(old('priority', 'normal') === 'normal')>Normal</option>
<option value="low" @selected(old('priority') === 'low')>Low</option>
<option value="high" @selected(old('priority') === 'high')>High</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">What happened?</label>
<textarea name="message" rows="6" required maxlength="5000"
class="mt-1 w-full rounded-lg border-slate-300 text-sm"
placeholder="Describe what you were doing and what went wrong…">{{ old('message') }}</textarea>
@error('message')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
</div>
<button type="submit" class="btn-primary w-full">Send to Ladill support</button>
</form>
</div>
</x-app-layout>
+4
View File
@@ -22,6 +22,7 @@ use App\Http\Controllers\Qms\MobileQueueController;
use App\Http\Controllers\Qms\OnboardingController;
use App\Http\Controllers\Qms\QueueBoardController;
use App\Http\Controllers\Qms\QueueRuleController;
use App\Http\Controllers\Qms\IssueReportController;
use App\Http\Controllers\Qms\ProController;
use App\Http\Controllers\Qms\ReportController;
use App\Http\Controllers\Qms\ServiceQueueController;
@@ -175,5 +176,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
Route::post('/pro/subscribe-enterprise', [ProController::class, 'subscribeEnterprise'])->name('qms.pro.subscribe-enterprise');
Route::post('/pro/subscribe-prepaid', [ProController::class, 'subscribePrepaid'])->name('qms.pro.subscribe-prepaid');
Route::get('/pro/paystack/callback', [ProController::class, 'paystackCallback'])->name('qms.pro.paystack.callback');
Route::get('/report-issue', [IssueReportController::class, 'create'])->name('qms.issues.create');
Route::post('/report-issue', [IssueReportController::class, 'store'])->name('qms.issues.store');
});
});