mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
Add payments tracking
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\BasePageController;
|
||||
use App\Http\Controllers\BtcPaymentController;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AdminPaymentController extends BasePageController
|
||||
{
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const ALLOWED_SORT_COLUMNS = [
|
||||
'id',
|
||||
'created_at',
|
||||
'username',
|
||||
'email',
|
||||
'invoice_amount',
|
||||
'payment_status',
|
||||
'invoice_status',
|
||||
'order_id',
|
||||
];
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->setAdminPrefs();
|
||||
|
||||
$meta_title = $title = 'Payments';
|
||||
|
||||
$filters = [
|
||||
'username' => $request->string('username')->trim()->value(),
|
||||
'email' => $request->string('email')->trim()->value(),
|
||||
'payment_status' => $request->string('payment_status')->trim()->value(),
|
||||
'invoice_status' => $request->string('invoice_status')->trim()->value(),
|
||||
];
|
||||
|
||||
$sort = $request->string('sort')->value();
|
||||
if (! \in_array($sort, self::ALLOWED_SORT_COLUMNS, true)) {
|
||||
$sort = 'created_at';
|
||||
}
|
||||
|
||||
$order = strtolower($request->string('order')->value()) === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$payments = Payment::query()
|
||||
->filter($filters)
|
||||
->orderBy($sort, $order)
|
||||
->paginate((int) config('nntmux.items_per_page'))
|
||||
->withQueryString();
|
||||
|
||||
$paymentStatuses = BtcPaymentController::paymentStatusesForAdminFilter();
|
||||
$invoiceStatuses = BtcPaymentController::invoiceStatusesForAdminFilter();
|
||||
|
||||
$this->viewData = array_merge($this->viewData, [
|
||||
'payments' => $payments,
|
||||
'filters' => $filters,
|
||||
'sort' => $sort,
|
||||
'order' => $order,
|
||||
'paymentStatuses' => $paymentStatuses,
|
||||
'invoiceStatuses' => $invoiceStatuses,
|
||||
'title' => $title,
|
||||
'meta_title' => $meta_title,
|
||||
'page_title' => $title,
|
||||
]);
|
||||
|
||||
return view('admin.payments.index', $this->viewData);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class BtcPaymentController extends BasePageController
|
||||
if ($payload['type'] === 'InvoicePaymentSettled') {
|
||||
$user = User::query()->where('email', '=', $payload['metadata']['buyerEmail'])->first();
|
||||
if ($user) {
|
||||
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', 'Settled')->first();
|
||||
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', Payment::PAYMENT_STATUS_SETTLED)->first();
|
||||
if ($checkOrder !== null) {
|
||||
Log::channel('btc_payment')->error('Duplicate BTCPay webhook: '.$payload['webhookId']);
|
||||
|
||||
@@ -59,8 +59,8 @@ class BtcPaymentController extends BasePageController
|
||||
|
||||
if ($payload['type'] === 'InvoiceSettled') {
|
||||
// Check if we have the invoice_id in payments table and if we do, update the user account
|
||||
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', 'Settled')->where(function ($query) {
|
||||
return $query->where('invoice_status', 'Pending')->orWhereNull('invoice_status');
|
||||
$checkOrder = Payment::query()->where('invoice_id', '=', $payload['invoiceId'])->where('payment_status', '=', Payment::PAYMENT_STATUS_SETTLED)->where(function ($query) {
|
||||
return $query->where('invoice_status', Payment::INVOICE_STATUS_PENDING)->orWhereNull('invoice_status');
|
||||
})->first();
|
||||
if ($checkOrder !== null) {
|
||||
$user = User::query()->where('email', '=', $checkOrder->email)->first();
|
||||
@@ -85,7 +85,7 @@ class BtcPaymentController extends BasePageController
|
||||
}
|
||||
|
||||
User::updateUserRole($user->id, $roleName, addYears: $addYears);
|
||||
$checkOrder->update(['invoice_status' => 'Settled']);
|
||||
$checkOrder->update(['invoice_status' => Payment::INVOICE_STATUS_SETTLED]);
|
||||
Log::channel('btc_payment')->info('User: '.$user->username.' upgraded to '.$roleName.' (+'.$addYears.' years) for BTCPay webhook: '.$checkOrder->webhook_id);
|
||||
|
||||
return response('OK', 200);
|
||||
@@ -99,4 +99,33 @@ class BtcPaymentController extends BasePageController
|
||||
|
||||
return response('OK', 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment.status values from BTCPay Greenfield API (payload.payment.status). Used for admin filters.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function paymentStatusesForAdminFilter(): array
|
||||
{
|
||||
return [
|
||||
'Pending',
|
||||
'Processing',
|
||||
Payment::PAYMENT_STATUS_SETTLED,
|
||||
'Invalid',
|
||||
'Manual',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* invoice_status values used in our DB (migration default Pending; webhook sets Settled after role upgrade).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function invoiceStatusesForAdminFilter(): array
|
||||
{
|
||||
return [
|
||||
Payment::INVOICE_STATUS_PENDING,
|
||||
Payment::INVOICE_STATUS_SETTLED,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -12,4 +13,45 @@ class Payment extends Model
|
||||
use HasFactory; // @phpstan-ignore missingType.generics
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
/** Stored from BTCPay webhook payload.payment.status; duplicate checks use Settled. */
|
||||
public const PAYMENT_STATUS_SETTLED = 'Settled';
|
||||
|
||||
/** Migration default; webhook clears to Settled after role upgrade. */
|
||||
public const INVOICE_STATUS_PENDING = 'Pending';
|
||||
|
||||
public const INVOICE_STATUS_SETTLED = 'Settled';
|
||||
|
||||
/**
|
||||
* @param Builder<Payment> $query
|
||||
* @param array{username?: string, email?: string, payment_status?: string, invoice_status?: string} $filters
|
||||
* @return Builder<Payment>
|
||||
*/
|
||||
public function scopeFilter(Builder $query, array $filters): Builder
|
||||
{
|
||||
if (! empty($filters['username'])) {
|
||||
$query->where('username', 'like', '%'.$filters['username'].'%');
|
||||
}
|
||||
|
||||
if (! empty($filters['email'])) {
|
||||
$query->where('email', 'like', '%'.$filters['email'].'%');
|
||||
}
|
||||
|
||||
if (! empty($filters['payment_status'])) {
|
||||
$query->where('payment_status', $filters['payment_status']);
|
||||
}
|
||||
|
||||
if (! empty($filters['invoice_status'])) {
|
||||
if ($filters['invoice_status'] === self::INVOICE_STATUS_PENDING) {
|
||||
$query->where(function (Builder $q): void {
|
||||
$q->whereNull('invoice_status')
|
||||
->orWhere('invoice_status', self::INVOICE_STATUS_PENDING);
|
||||
});
|
||||
} else {
|
||||
$query->where('invoice_status', $filters['invoice_status']);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title', $title ?? 'Payments')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6">
|
||||
<x-admin.card>
|
||||
<x-admin.page-header :title="$title" icon="fas fa-credit-card">
|
||||
</x-admin.page-header>
|
||||
|
||||
<x-admin.info-alert>
|
||||
BTCPay webhook records appear here after invoices are settled. Use filters to find a user or status.
|
||||
</x-admin.info-alert>
|
||||
|
||||
<div class="border-b border-gray-200 bg-gray-50 px-6 py-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<form method="get" action="{{ route('admin.payment-list') }}" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<label for="filter-username" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Username</label>
|
||||
<input type="text" name="username" id="filter-username" value="{{ $filters['username'] }}"
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
||||
placeholder="Contains…">
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-email" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
|
||||
<input type="text" name="email" id="filter-email" value="{{ $filters['email'] }}"
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-400 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
||||
placeholder="Contains…">
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-payment-status" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Payment status</label>
|
||||
<select name="payment_status" id="filter-payment-status"
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400">
|
||||
<option value="">All</option>
|
||||
@foreach($paymentStatuses as $ps)
|
||||
<option value="{{ $ps }}" @selected($filters['payment_status'] === $ps)>{{ $ps }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filter-invoice-status" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Invoice status</label>
|
||||
<select name="invoice_status" id="filter-invoice-status"
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400">
|
||||
<option value="">All</option>
|
||||
@foreach($invoiceStatuses as $is)
|
||||
<option value="{{ $is }}" @selected($filters['invoice_status'] === $is)>{{ $is }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input type="hidden" name="sort" value="{{ $sort }}">
|
||||
<input type="hidden" name="order" value="{{ $order }}">
|
||||
<button type="submit" class="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800">
|
||||
<i class="fas fa-filter mr-2"></i>Apply filters
|
||||
</button>
|
||||
<a href="{{ route('admin.payment-list') }}" class="inline-flex items-center rounded-md bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
|
||||
Reset
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if($payments->count() > 0)
|
||||
<x-admin.data-table>
|
||||
<x-slot:head>
|
||||
<x-admin.th align="center" width="16">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<span>ID</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'id', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'id' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-numeric-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'id', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'id' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-numeric-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Date</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'created_at', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'created_at' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-numeric-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'created_at', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'created_at' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-numeric-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Username</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'username', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'username' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-alpha-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'username', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'username' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-alpha-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Email</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'email', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'email' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-alpha-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'email', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'email' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-alpha-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>Item</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Order ID</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'order_id', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'order_id' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-alpha-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'order_id', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'order_id' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-alpha-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Amount</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'invoice_amount', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'invoice_amount' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-numeric-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'invoice_amount', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'invoice_amount' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-numeric-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>Method</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Pay. status</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'payment_status', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'payment_status' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-alpha-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'payment_status', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'payment_status' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-alpha-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
<x-admin.th>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Inv. status</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'invoice_status', 'order' => 'asc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'invoice_status' && $order === 'asc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Ascending">
|
||||
<i class="fas fa-sort-alpha-down text-xs"></i>
|
||||
</a>
|
||||
<a href="{{ request()->fullUrlWithQuery(['sort' => 'invoice_status', 'order' => 'desc', 'page' => null]) }}" class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 {{ ($sort === 'invoice_status' && $order === 'desc') ? 'text-blue-600 dark:text-blue-400' : '' }}" title="Sort Descending">
|
||||
<i class="fas fa-sort-alpha-down-alt text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-admin.th>
|
||||
</x-slot:head>
|
||||
|
||||
@foreach($payments as $payment)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $payment->id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ $payment->created_at?->timezone(config('app.timezone'))->format('Y-m-d H:i') ?? '—' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ $payment->username }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">{{ $payment->email }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-300 max-w-xs truncate" title="{{ $payment->item_description }}">{{ Str::limit($payment->item_description, 48) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-600 dark:text-gray-400">{{ Str::limit($payment->order_id, 24) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ $payment->invoice_amount }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">{{ Str::limit($payment->payment_method, 20) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if(strcasecmp((string) $payment->payment_status, \App\Models\Payment::PAYMENT_STATUS_SETTLED) === 0)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
|
||||
{{ $payment->payment_status }}
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200">
|
||||
{{ $payment->payment_status }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@php
|
||||
$inv = $payment->invoice_status ?? \App\Models\Payment::INVOICE_STATUS_PENDING;
|
||||
@endphp
|
||||
@if(strcasecmp((string) $inv, \App\Models\Payment::INVOICE_STATUS_SETTLED) === 0)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
|
||||
{{ $inv }}
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">
|
||||
{{ $inv }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</x-admin.data-table>
|
||||
|
||||
<div class="mt-4 px-6">
|
||||
{{ $payments->withQueryString()->links() }}
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 bg-gray-50 px-6 py-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Total: <strong class="text-gray-900 dark:text-gray-100">{{ $payments->total() }}</strong> payments (this filter)</span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-empty-state
|
||||
icon="fas fa-credit-card"
|
||||
title="No payments found"
|
||||
message="No payment records match your filters, or none have been recorded yet."
|
||||
:actionUrl="route('admin.payment-list')"
|
||||
actionLabel="Clear filters"
|
||||
actionIcon="fas fa-redo"
|
||||
/>
|
||||
@endif
|
||||
</x-admin.card>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -43,6 +43,14 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Payments -->
|
||||
<div class="mb-4">
|
||||
<a href="{{ url('/admin/payment-list') }}" class="flex items-center space-x-3 text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-white hover:bg-white/10 dark:hover:bg-white/5 py-2 px-3 rounded transition">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
<span>Payments</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="mb-4" x-data="adminSubmenu">
|
||||
<button type="button" @click="toggle()" class="flex items-center justify-between w-full text-left text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-white hover:bg-white/10 dark:hover:bg-white/5 py-2 px-3 rounded transition">
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Http\Controllers\Admin\AdminLogViewerController;
|
||||
use App\Http\Controllers\Admin\AdminMovieController;
|
||||
use App\Http\Controllers\Admin\AdminMusicController;
|
||||
use App\Http\Controllers\Admin\AdminPageController;
|
||||
use App\Http\Controllers\Admin\AdminPaymentController;
|
||||
use App\Http\Controllers\Admin\AdminPredbController;
|
||||
use App\Http\Controllers\Admin\AdminPromotionController;
|
||||
use App\Http\Controllers\Admin\AdminRegistrationController;
|
||||
@@ -289,6 +290,7 @@ Route::middleware(['role:Admin', '2fa'])->prefix('admin')->group(function () {
|
||||
Route::match(['GET', 'POST'], 'movie-add', [AdminMovieController::class, 'create'])->name('admin.movie-add');
|
||||
Route::get('music-list', [AdminMusicController::class, 'index'])->name('admin.music-list');
|
||||
Route::match(['GET', 'POST'], 'music-edit', [AdminMusicController::class, 'edit'])->name('admin.music-edit');
|
||||
Route::get('payment-list', [AdminPaymentController::class, 'index'])->name('admin.payment-list');
|
||||
Route::get('predb', [AdminPredbController::class, 'index'])->name('admin.predb');
|
||||
Route::get('group-list', [AdminGroupController::class, 'index'])->name('admin.group-list');
|
||||
Route::match(['GET', 'POST'], 'group-edit', [AdminGroupController::class, 'edit'])->name('admin.group-edit');
|
||||
|
||||
Reference in New Issue
Block a user