Files
website/website/resources/js/Pages/Billing/Index.vue
Claude Dev b1e080d70c Implement Phase 2: Billing & Subscriptions
Add complete billing system with Stripe and PayPal gateway support,
checkout flow with coupon validation, subscription management
(cancel/resume/swap), payment method management, invoice and
transaction history, webhook handlers, dunning/suspension system
with scheduled processing, and 29 new tests (53 total passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 07:18:48 -05:00

170 lines
8.2 KiB
Vue

<script setup>
import { ref } from 'vue';
import { useForm, Link } from '@inertiajs/vue3';
import AppLayout from '@/Layouts/AppLayout.vue';
defineOptions({ layout: AppLayout });
defineProps({
paymentMethods: Array,
invoices: Array,
transactions: Array,
intent: Object,
stripeKey: String,
});
const addForm = useForm({
payment_method_id: '',
});
const defaultForm = useForm({
payment_method_id: '',
});
const setDefault = (id) => {
defaultForm.payment_method_id = id;
defaultForm.post('/billing/payment-methods/default');
};
const removeMethod = (id) => {
if (confirm('Are you sure you want to remove this payment method?')) {
useForm({}).delete(`/billing/payment-methods/${id}`);
}
};
</script>
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Billing</h1>
<div class="space-y-8">
<!-- Payment Methods -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900">Payment Methods</h2>
</div>
<div v-if="paymentMethods.length === 0" class="text-sm text-gray-500">
No payment methods on file.
</div>
<div v-else class="space-y-3">
<div
v-for="pm in paymentMethods"
:key="pm.id"
class="flex items-center justify-between p-3 border rounded-md"
:class="pm.is_default ? 'border-blue-300 bg-blue-50' : 'border-gray-200'"
>
<div class="flex items-center gap-3">
<span class="text-sm font-medium capitalize">{{ pm.brand }}</span>
<span class="text-sm text-gray-500">&bull;&bull;&bull;&bull; {{ pm.last_four }}</span>
<span class="text-sm text-gray-400">{{ pm.exp_month }}/{{ pm.exp_year }}</span>
<span v-if="pm.is_default" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">Default</span>
</div>
<div class="flex items-center gap-2">
<button
v-if="!pm.is_default"
@click="setDefault(pm.id)"
:disabled="defaultForm.processing"
class="text-sm text-blue-600 hover:text-blue-500"
>
Make Default
</button>
<button
@click="removeMethod(pm.id)"
class="text-sm text-red-600 hover:text-red-500"
>
Remove
</button>
</div>
</div>
</div>
</div>
<!-- Recent Invoices -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900">Recent Invoices</h2>
<Link href="/billing/invoices" class="text-sm text-blue-600 hover:text-blue-500">View All</Link>
</div>
<div v-if="invoices.length === 0" class="text-sm text-gray-500">No invoices yet.</div>
<table v-else class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-2 text-gray-500 font-medium">Number</th>
<th class="text-left py-2 text-gray-500 font-medium">Date</th>
<th class="text-left py-2 text-gray-500 font-medium">Status</th>
<th class="text-right py-2 text-gray-500 font-medium">Amount</th>
<th class="text-right py-2 text-gray-500 font-medium"></th>
</tr>
</thead>
<tbody>
<tr v-for="invoice in invoices" :key="invoice.id" class="border-b border-gray-100">
<td class="py-2 text-gray-900">{{ invoice.number }}</td>
<td class="py-2 text-gray-600">{{ new Date(invoice.created_at).toLocaleDateString() }}</td>
<td class="py-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium capitalize"
:class="{
'bg-green-100 text-green-800': invoice.status === 'paid',
'bg-yellow-100 text-yellow-800': invoice.status === 'pending',
'bg-red-100 text-red-800': invoice.status === 'overdue',
}"
>
{{ invoice.status }}
</span>
</td>
<td class="py-2 text-right text-gray-900">${{ parseFloat(invoice.total).toFixed(2) }}</td>
<td class="py-2 text-right">
<a :href="`/billing/invoices/${invoice.id}/download`" class="text-blue-600 hover:text-blue-500">Download</a>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Recent Transactions -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900">Recent Transactions</h2>
<Link href="/billing/transactions" class="text-sm text-blue-600 hover:text-blue-500">View All</Link>
</div>
<div v-if="transactions.length === 0" class="text-sm text-gray-500">No transactions yet.</div>
<table v-else class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-2 text-gray-500 font-medium">Date</th>
<th class="text-left py-2 text-gray-500 font-medium">Gateway</th>
<th class="text-left py-2 text-gray-500 font-medium">Status</th>
<th class="text-left py-2 text-gray-500 font-medium">Description</th>
<th class="text-right py-2 text-gray-500 font-medium">Amount</th>
</tr>
</thead>
<tbody>
<tr v-for="tx in transactions" :key="tx.id" class="border-b border-gray-100">
<td class="py-2 text-gray-600">{{ new Date(tx.created_at).toLocaleDateString() }}</td>
<td class="py-2 text-gray-600 capitalize">{{ tx.gateway }}</td>
<td class="py-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium capitalize"
:class="{
'bg-green-100 text-green-800': tx.status === 'succeeded',
'bg-red-100 text-red-800': tx.status === 'failed',
'bg-yellow-100 text-yellow-800': tx.status === 'pending',
}"
>
{{ tx.status }}
</span>
</td>
<td class="py-2 text-gray-600">{{ tx.description }}</td>
<td class="py-2 text-right text-gray-900">${{ parseFloat(tx.amount).toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>