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:
Claude Dev
2026-02-09 20:37:15 -05:00
parent edf428215f
commit dd558d5dcc
5 changed files with 329 additions and 0 deletions

View 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"
>
&larr; 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>