Compare commits

..

No commits in common. "7c8a0ba94bca6d0051a4778009c13eb8b6d528fe" and "c199e0a9dd586ef15af86d799f7242b3eab078fd" have entirely different histories.

6 changed files with 8 additions and 492 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,23 +1,18 @@
<x-layouts::app :title="__('Dashboard')"> <x-layouts::app :title="__('Dashboard')">
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl"> <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="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"> <div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<livewire:stripe-balance /> <x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div> </div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800"> <div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<livewire:stripe-payout /> <x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div> </div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800"> <div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<livewire:open-invoices /> <x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div> </div>
</div> </div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<div class="relative overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800"> <x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
<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> </div>
</div> </div>
</x-layouts::app> </x-layouts::app>