Compare commits
No commits in common. "7c8a0ba94bca6d0051a4778009c13eb8b6d528fe" and "c199e0a9dd586ef15af86d799f7242b3eab078fd" have entirely different histories.
7c8a0ba94b
...
c199e0a9dd
|
|
@ -1,90 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\InvoiceStatus;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
new class extends Component {
|
||||
public ?array $invoices = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadInvoices();
|
||||
}
|
||||
|
||||
public function loadInvoices(): void
|
||||
{
|
||||
$this->invoices = Cache::remember('draft_invoices', now()->addMinutes(15), function () {
|
||||
return Invoice::where('status', InvoiceStatus::DRAFT)
|
||||
->with('client')
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn($invoice) => [
|
||||
'uuid' => $invoice->uuid,
|
||||
'invoice_number' => $invoice->invoice_number,
|
||||
'client_name' => $invoice->client?->abbreviation ?? $invoice->client?->name ?? 'Unknown',
|
||||
'total' => $invoice->total,
|
||||
'created_at' => $invoice->created_at->format('M j'),
|
||||
])
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
#[On('invoice-created')]
|
||||
public function clearCache(): void
|
||||
{
|
||||
Cache::forget('draft_invoices');
|
||||
$this->loadInvoices();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget('draft_invoices');
|
||||
$this->loadInvoices();
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="flex h-full flex-col p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Draft Invoices</h3>
|
||||
<button wire:click="refresh" wire:loading.attr="disabled" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<flux:icon.arrow-path class="size-4" wire:loading.class="animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(empty($invoices))
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-sm text-gray-500">No draft invoices</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex-1 overflow-auto">
|
||||
<table class="w-full text-sm">
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($invoices as $invoice)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-neutral-700/50">
|
||||
<td class="py-2 pr-2">
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ $invoice['invoice_number'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $invoice['client_name'] }}</div>
|
||||
</td>
|
||||
<td class="py-2 pr-2 text-right">
|
||||
<div class="font-medium text-gray-900 dark:text-white">${{ number_format($invoice['total'], 0) }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $invoice['created_at'] }}</div>
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{{ route('invoices.edit', $invoice['uuid']) }}" class="inline-flex items-center justify-center rounded-md bg-blue-600 px-2 py-1 text-xs font-medium text-white hover:bg-blue-700">
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-2">Cached for 15 min</p>
|
||||
</div>
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\InvoiceStatus;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
new class extends Component {
|
||||
public ?array $invoices = null;
|
||||
public float $totalOpen = 0;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadInvoices();
|
||||
}
|
||||
|
||||
public function loadInvoices(): void
|
||||
{
|
||||
$data = Cache::remember('open_invoices', now()->addMinutes(15), function () {
|
||||
$allOpen = Invoice::where('status', InvoiceStatus::POSTED)->with('client')->get();
|
||||
|
||||
return [
|
||||
'total' => $allOpen->sum('balance_due'),
|
||||
'invoices' => $allOpen
|
||||
->sortBy('invoice_date')
|
||||
->take(5)
|
||||
->map(fn($invoice) => [
|
||||
'uuid' => $invoice->uuid,
|
||||
'invoice_number' => $invoice->invoice_number,
|
||||
'client_name' => $invoice->client?->abbreviation ?? $invoice->client?->name ?? 'Unknown',
|
||||
'invoice_date' => $invoice->invoice_date?->format('M j, Y'),
|
||||
'days_old' => $invoice->invoice_date?->diffInDays(now()),
|
||||
'balance_due' => $invoice->balance_due,
|
||||
])
|
||||
->values()
|
||||
->toArray(),
|
||||
];
|
||||
});
|
||||
|
||||
// Handle stale cache format
|
||||
if (!isset($data['total'])) {
|
||||
Cache::forget('open_invoices');
|
||||
$this->loadInvoices();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->totalOpen = $data['total'];
|
||||
$this->invoices = $data['invoices'];
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget('open_invoices');
|
||||
$this->loadInvoices();
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="flex h-full flex-col p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Open Invoices</h3>
|
||||
<button wire:click="refresh" wire:loading.attr="disabled" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<flux:icon.arrow-path class="size-4" wire:loading.class="animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xl font-semibold text-gray-900 dark:text-white mb-1">${{ number_format($totalOpen, 0) }}</p>
|
||||
|
||||
@if(empty($invoices))
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-sm text-gray-500">No open invoices</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex-1 overflow-auto -mx-1">
|
||||
<table class="w-full text-xs">
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($invoices as $invoice)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-neutral-700/50">
|
||||
<td class="py-1 px-1">
|
||||
<a href="{{ route('invoices.edit', $invoice['uuid']) }}" class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ $invoice['invoice_number'] }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="py-1 px-1 text-gray-600 dark:text-gray-300 truncate max-w-[80px]" title="{{ $invoice['client_name'] }}">
|
||||
{{ $invoice['client_name'] }}
|
||||
</td>
|
||||
<td class="py-1 px-1 text-right font-medium text-gray-900 dark:text-white">
|
||||
${{ number_format($invoice['balance_due'], 0) }}
|
||||
</td>
|
||||
<td class="py-1 px-1 text-right text-gray-500">
|
||||
{{ $invoice['days_old'] }}d
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cached for 15 min</p>
|
||||
</div>
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PaymentStatus;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
new class extends Component {
|
||||
public ?array $payments = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadPayments();
|
||||
}
|
||||
|
||||
public function loadPayments(): void
|
||||
{
|
||||
$this->payments = Cache::remember('recent_payments', now()->addMinutes(15), function () {
|
||||
return Payment::with(['invoice.client', 'contact'])
|
||||
->orderBy('payment_date', 'desc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn($payment) => [
|
||||
'id' => $payment->id,
|
||||
'amount' => $payment->amount,
|
||||
'payment_date' => $payment->payment_date->format('M j'),
|
||||
'client_name' => $payment->invoice?->client?->abbreviation ?? $payment->invoice?->client?->name ?? 'Unknown',
|
||||
'invoice_number' => $payment->invoice?->invoice_number,
|
||||
'method' => $payment->payment_method->value,
|
||||
'status' => $payment->status->value,
|
||||
'status_color' => $payment->status === PaymentStatus::COMPLETED ? 'green' : ($payment->status === PaymentStatus::PENDING ? 'yellow' : 'red'),
|
||||
])
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
#[On('payment-created')]
|
||||
public function clearCache(): void
|
||||
{
|
||||
Cache::forget('recent_payments');
|
||||
$this->loadPayments();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget('recent_payments');
|
||||
$this->loadPayments();
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="flex h-full flex-col p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Recent Payments</h3>
|
||||
<button wire:click="refresh" wire:loading.attr="disabled" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<flux:icon.arrow-path class="size-4" wire:loading.class="animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(empty($payments))
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-sm text-gray-500">No payments yet</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex-1 overflow-auto">
|
||||
<table class="w-full text-sm">
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@foreach($payments as $payment)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-neutral-700/50">
|
||||
<td class="py-2 pr-2">
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ $payment['client_name'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $payment['invoice_number'] }}</div>
|
||||
</td>
|
||||
<td class="py-2 pr-2 text-right">
|
||||
<div class="font-medium text-gray-900 dark:text-white">${{ number_format($payment['amount'], 2) }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $payment['payment_date'] }}</div>
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<flux:badge size="sm" :color="$payment['status_color']">{{ ucfirst($payment['method']) }}</flux:badge>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-2">Cached for 15 min</p>
|
||||
</div>
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Balance;
|
||||
|
||||
new class extends Component {
|
||||
public ?array $balance = null;
|
||||
public ?string $error = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadBalance();
|
||||
}
|
||||
|
||||
public function loadBalance(): void
|
||||
{
|
||||
try {
|
||||
$this->balance = Cache::remember('stripe_balance', now()->addMinutes(15), function () {
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
$balance = Balance::retrieve();
|
||||
|
||||
return [
|
||||
'available' => collect($balance->available)->map(fn($b) => [
|
||||
'amount' => $b->amount / 100,
|
||||
'currency' => strtoupper($b->currency),
|
||||
])->toArray(),
|
||||
'pending' => collect($balance->pending)->map(fn($b) => [
|
||||
'amount' => $b->amount / 100,
|
||||
'currency' => strtoupper($b->currency),
|
||||
])->toArray(),
|
||||
];
|
||||
});
|
||||
$this->error = null;
|
||||
} catch (\Exception $e) {
|
||||
$this->error = 'Unable to fetch Stripe balance';
|
||||
$this->balance = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget('stripe_balance');
|
||||
$this->loadBalance();
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="flex h-full flex-col justify-between p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Stripe Balance</h3>
|
||||
<button wire:click="refresh" wire:loading.attr="disabled" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<flux:icon.arrow-path class="size-4" wire:loading.class="animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if($error)
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-sm text-red-500">{{ $error }}</p>
|
||||
</div>
|
||||
@elseif($balance)
|
||||
<div class="flex flex-1 flex-col justify-center space-y-3">
|
||||
@foreach($balance['available'] as $available)
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Available</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
${{ number_format($available['amount'], 2) }}
|
||||
<span class="text-sm font-normal text-gray-500">{{ $available['currency'] }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@endforeach
|
||||
@foreach($balance['pending'] as $pending)
|
||||
@if($pending['amount'] > 0)
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Pending</p>
|
||||
<p class="text-lg font-medium text-gray-600 dark:text-gray-300">
|
||||
${{ number_format($pending['amount'], 2) }}
|
||||
<span class="text-sm font-normal text-gray-500">{{ $pending['currency'] }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<flux:icon.arrow-path class="size-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">Cached for 15 min</p>
|
||||
</div>
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Payout;
|
||||
|
||||
new class extends Component {
|
||||
public ?array $payout = null;
|
||||
public ?string $error = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadPayout();
|
||||
}
|
||||
|
||||
public function loadPayout(): void
|
||||
{
|
||||
try {
|
||||
$this->payout = Cache::remember('stripe_latest_payout', now()->addMinutes(15), function () {
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
$payouts = Payout::all(['limit' => 1]);
|
||||
|
||||
if (empty($payouts->data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payout = $payouts->data[0];
|
||||
|
||||
return [
|
||||
'amount' => $payout->amount / 100,
|
||||
'currency' => strtoupper($payout->currency),
|
||||
'status' => $payout->status,
|
||||
'arrival_date' => $payout->arrival_date,
|
||||
'created' => $payout->created,
|
||||
];
|
||||
});
|
||||
$this->error = null;
|
||||
} catch (\Exception $e) {
|
||||
$this->error = 'Unable to fetch payout';
|
||||
$this->payout = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget('stripe_latest_payout');
|
||||
$this->loadPayout();
|
||||
}
|
||||
|
||||
public function statusColor(): string
|
||||
{
|
||||
return match($this->payout['status'] ?? '') {
|
||||
'paid' => 'green',
|
||||
'pending' => 'yellow',
|
||||
'in_transit' => 'blue',
|
||||
'canceled', 'failed' => 'red',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="flex h-full flex-col justify-between p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Latest Payout</h3>
|
||||
<button wire:click="refresh" wire:loading.attr="disabled" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<flux:icon.arrow-path class="size-4" wire:loading.class="animate-spin" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if($error)
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-sm text-red-500">{{ $error }}</p>
|
||||
</div>
|
||||
@elseif($payout)
|
||||
<div class="flex flex-1 flex-col justify-center space-y-2">
|
||||
<div>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
${{ number_format($payout['amount'], 2) }}
|
||||
<span class="text-sm font-normal text-gray-500">{{ $payout['currency'] }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge :color="$this->statusColor()">{{ ucfirst($payout['status']) }}</flux:badge>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
@if($payout['status'] === 'paid')
|
||||
Arrived {{ \Carbon\Carbon::createFromTimestamp($payout['arrival_date'])->format('M j, Y') }}
|
||||
@else
|
||||
Expected {{ \Carbon\Carbon::createFromTimestamp($payout['arrival_date'])->format('M j, Y') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
@elseif($payout === null && !$error)
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<p class="text-sm text-gray-500">No payouts yet</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<flux:icon.arrow-path class="size-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">Cached for 15 min</p>
|
||||
</div>
|
||||
|
|
@ -1,23 +1,18 @@
|
|||
<x-layouts::app :title="__('Dashboard')">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
||||
<livewire:stripe-balance />
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
||||
<livewire:stripe-payout />
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
||||
<livewire:open-invoices />
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="relative overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
||||
<livewire:draft-invoices />
|
||||
</div>
|
||||
<div class="relative overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
|
||||
<livewire:recent-payments />
|
||||
</div>
|
||||
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
|
|
|
|||
Loading…
Reference in New Issue