Add upcoming renewals page with days-until-renewal indicators
Shows subscriptions nearing renewal with color-coded urgency chips, auto-renew status, and quick manage links. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
259
website/resources/ts/Pages/Billing/UpcomingRenewals.vue
Normal file
259
website/resources/ts/Pages/Billing/UpcomingRenewals.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import { resolveSubscriptionStatusColor, formatPrice } from '@/utils/resolvers'
|
||||
import type { UpcomingRenewal } from '@/types'
|
||||
|
||||
interface Props {
|
||||
renewals: UpcomingRenewal[]
|
||||
}
|
||||
|
||||
defineOptions({ layout: AccountLayout })
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const nextRenewalDate = computed<string | null>(() => {
|
||||
if (props.renewals.length === 0) return null
|
||||
const sorted = [...props.renewals].sort(
|
||||
(a, b) => new Date(a.renewal_date).getTime() - new Date(b.renewal_date).getTime(),
|
||||
)
|
||||
return sorted[0].renewal_date
|
||||
})
|
||||
|
||||
const totalMonthlyCost = computed<number>(() => {
|
||||
return props.renewals.reduce((sum, r) => {
|
||||
if (!r.plan_price) return sum
|
||||
const price = parseFloat(r.plan_price)
|
||||
if (r.billing_cycle === 'yearly' || r.billing_cycle === 'annual') {
|
||||
return sum + price / 12
|
||||
}
|
||||
if (r.billing_cycle === 'quarterly') {
|
||||
return sum + price / 3
|
||||
}
|
||||
return sum + price
|
||||
}, 0)
|
||||
})
|
||||
|
||||
function resolveRenewalUrgencyColor(days: number): string {
|
||||
if (days < 0) return 'error'
|
||||
if (days < 7) return 'error'
|
||||
if (days <= 14) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
function formatRenewalDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<Link
|
||||
href="/billing"
|
||||
class="text-primary text-body-2 text-decoration-none"
|
||||
>
|
||||
← Back to Billing
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="text-h4 font-weight-bold mb-6">
|
||||
Upcoming Renewals
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<VRow
|
||||
v-if="renewals.length > 0"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center ga-3">
|
||||
<VAvatar
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded
|
||||
>
|
||||
<VIcon icon="tabler-calendar-repeat" />
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
Total Upcoming
|
||||
</div>
|
||||
<div class="text-h5 font-weight-bold">
|
||||
{{ renewals.length }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center ga-3">
|
||||
<VAvatar
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
rounded
|
||||
>
|
||||
<VIcon icon="tabler-clock" />
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
Next Renewal
|
||||
</div>
|
||||
<div class="text-h6 font-weight-bold">
|
||||
{{ nextRenewalDate ? formatRenewalDate(nextRenewalDate) : 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center ga-3">
|
||||
<VAvatar
|
||||
color="success"
|
||||
variant="tonal"
|
||||
rounded
|
||||
>
|
||||
<VIcon icon="tabler-currency-dollar" />
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
Est. Monthly Cost
|
||||
</div>
|
||||
<div class="text-h5 font-weight-bold">
|
||||
${{ totalMonthlyCost.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- Renewals Table -->
|
||||
<VCard>
|
||||
<VCardText
|
||||
v-if="renewals.length === 0"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-calendar-off"
|
||||
size="48"
|
||||
class="text-disabled mb-4"
|
||||
/>
|
||||
<div class="text-h6 text-medium-emphasis mb-2">
|
||||
No Upcoming Renewals
|
||||
</div>
|
||||
<div class="text-body-2 text-disabled">
|
||||
You don't have any active subscriptions with upcoming renewals.
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<template v-else>
|
||||
<VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plan</th>
|
||||
<th>Price</th>
|
||||
<th>Renewal Date</th>
|
||||
<th>Days Until Renewal</th>
|
||||
<th>Status</th>
|
||||
<th>Auto Renew</th>
|
||||
<th class="text-end" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="renewal in renewals"
|
||||
:key="renewal.id"
|
||||
>
|
||||
<td class="font-weight-medium">
|
||||
<Link
|
||||
:href="`/subscriptions/${renewal.id}`"
|
||||
class="text-primary text-decoration-none"
|
||||
>
|
||||
{{ renewal.plan_name }}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="renewal.plan_price">
|
||||
{{ formatPrice(renewal.plan_price, renewal.billing_cycle ?? undefined) }}
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="text-disabled"
|
||||
>--</span>
|
||||
</td>
|
||||
<td>{{ formatRenewalDate(renewal.renewal_date) }}</td>
|
||||
<td>
|
||||
<VChip
|
||||
:color="resolveRenewalUrgencyColor(renewal.days_until_renewal)"
|
||||
size="small"
|
||||
>
|
||||
<template v-if="renewal.days_until_renewal < 0">
|
||||
{{ Math.abs(renewal.days_until_renewal) }}d overdue
|
||||
</template>
|
||||
<template v-else-if="renewal.days_until_renewal === 0">
|
||||
Today
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ renewal.days_until_renewal }}d
|
||||
</template>
|
||||
</VChip>
|
||||
</td>
|
||||
<td>
|
||||
<VChip
|
||||
:color="resolveSubscriptionStatusColor(renewal.status)"
|
||||
size="small"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ renewal.status }}
|
||||
</VChip>
|
||||
</td>
|
||||
<td>
|
||||
<VChip
|
||||
:color="renewal.auto_renew ? 'success' : 'secondary'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ renewal.auto_renew ? 'On' : 'Off' }}
|
||||
</VChip>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<Link
|
||||
:href="`/subscriptions/${renewal.id}`"
|
||||
class="text-decoration-none"
|
||||
>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
>
|
||||
Manage
|
||||
</VBtn>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</template>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user