mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
1532 lines
40 KiB
Vue
1532 lines
40 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
AlertTriangle,
|
|
ArrowLeftRight,
|
|
Building2,
|
|
CalendarDays,
|
|
CheckCircle2,
|
|
ChevronRight,
|
|
Clock3,
|
|
FileCheck2,
|
|
Hash,
|
|
History,
|
|
House,
|
|
Inbox,
|
|
ListFilter,
|
|
ReceiptText,
|
|
RefreshCw,
|
|
SearchX,
|
|
ShieldAlert,
|
|
Send,
|
|
WalletCards,
|
|
X,
|
|
} from 'lucide-vue-next'
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
|
import { useBillingStore } from '@/stores/billing'
|
|
import { usePhoneStore } from '@/stores/phone'
|
|
import type {
|
|
BillingDirection,
|
|
BillingFilter,
|
|
BillingStatus,
|
|
InvoiceSummary,
|
|
} from '@/types/billing'
|
|
import {
|
|
SkyAppPage,
|
|
SkyBadge,
|
|
SkyButton,
|
|
SkyCard,
|
|
SkyGlass,
|
|
SkyLink,
|
|
SkyNavbar,
|
|
SkyNavbarBackLink,
|
|
SkySearchbar,
|
|
SkySegmented,
|
|
SkySegmentedButton,
|
|
SkySheet,
|
|
SkySpinner,
|
|
SkyTabBar,
|
|
SkyTabButton,
|
|
SkyNotification,
|
|
} from '@/ui'
|
|
import { isTrustedRootMessageSource } from '@/utils/windowMessages'
|
|
|
|
type BillingTab = 'overview' | 'inbox' | 'history'
|
|
type BillingScreen = 'main' | 'detail'
|
|
|
|
const phone = usePhoneStore()
|
|
const billing = useBillingStore()
|
|
const tab = ref<BillingTab>('overview')
|
|
const screen = ref<BillingScreen>('main')
|
|
const direction = ref<BillingDirection>('inbox')
|
|
const filter = ref<BillingFilter>('all')
|
|
const filters: BillingFilter[] = ['all', 'open', 'paid']
|
|
const search = ref('')
|
|
const paymentOpen = ref(false)
|
|
const toastOpen = ref(false)
|
|
const toastText = ref('')
|
|
let searchTimer: number | undefined
|
|
let toastTimer: number | undefined
|
|
|
|
const t = (key: string, values?: Record<string, string | number>) =>
|
|
phone.t(
|
|
`Apps.billing.${key}`,
|
|
values &&
|
|
Object.fromEntries(
|
|
Object.entries(values).map(([name, value]) => [name, String(value)]),
|
|
),
|
|
)
|
|
|
|
const visibleInvoices = computed(() => {
|
|
if (tab.value !== 'history') return billing.invoices
|
|
return billing.invoices.filter((invoice) =>
|
|
['paid', 'cancelled', 'refunded', 'disputed'].includes(invoice.status),
|
|
)
|
|
})
|
|
|
|
const statusIcons: Record<BillingStatus, typeof Clock3> = {
|
|
open: Clock3,
|
|
processing: RefreshCw,
|
|
paid: CheckCircle2,
|
|
disputed: ShieldAlert,
|
|
cancelled: X,
|
|
refunded: ArrowLeftRight,
|
|
}
|
|
|
|
const filterIcons: Record<BillingFilter, typeof Clock3> = {
|
|
all: ListFilter,
|
|
open: Clock3,
|
|
overdue: AlertTriangle,
|
|
paid: CheckCircle2,
|
|
}
|
|
|
|
function formatMoney(
|
|
amount: number,
|
|
currency = billing.overview?.currency ?? '$',
|
|
): string {
|
|
return `${new Intl.NumberFormat(phone.lang, { maximumFractionDigits: 0 }).format(amount)} ${currency}`
|
|
}
|
|
|
|
function formatDate(timestamp: number | null): string {
|
|
if (!timestamp) return t('detail.noDueDate')
|
|
return new Intl.DateTimeFormat(phone.lang, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
}).format(timestamp)
|
|
}
|
|
|
|
function formatDateTime(timestamp: number | null): string {
|
|
if (!timestamp) return t('detail.noDueDate')
|
|
return new Intl.DateTimeFormat(phone.lang, {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(timestamp)
|
|
}
|
|
|
|
function statusKey(invoice: InvoiceSummary): string {
|
|
return invoice.isOverdue ? 'overdue' : invoice.status
|
|
}
|
|
|
|
function showToast(message: string): void {
|
|
toastText.value = message
|
|
toastOpen.value = true
|
|
if (toastTimer) window.clearTimeout(toastTimer)
|
|
toastTimer = window.setTimeout(() => (toastOpen.value = false), 2800)
|
|
}
|
|
|
|
async function loadList(): Promise<boolean> {
|
|
const activeFilter = tab.value === 'history' ? 'paid' : filter.value
|
|
return billing.loadInvoices(direction.value, activeFilter, search.value)
|
|
}
|
|
|
|
async function selectTab(nextTab: BillingTab): Promise<void> {
|
|
tab.value = nextTab
|
|
screen.value = 'main'
|
|
paymentOpen.value = false
|
|
if (nextTab === 'overview') {
|
|
await billing.loadOverview(direction.value)
|
|
return
|
|
}
|
|
direction.value = 'inbox'
|
|
filter.value = nextTab === 'history' ? 'paid' : 'all'
|
|
await loadList()
|
|
}
|
|
|
|
async function selectDirection(nextDirection: BillingDirection): Promise<void> {
|
|
direction.value = nextDirection
|
|
if (tab.value === 'overview') await billing.loadOverview(nextDirection)
|
|
else await loadList()
|
|
}
|
|
|
|
async function selectFilter(nextFilter: BillingFilter): Promise<void> {
|
|
filter.value = nextFilter
|
|
await loadList()
|
|
}
|
|
|
|
function updateSearch(event: Event): void {
|
|
const input = event.target
|
|
if (!(input instanceof HTMLInputElement)) {
|
|
console.error('[billing] Search input emitted without an input target.')
|
|
return
|
|
}
|
|
search.value = input.value
|
|
if (searchTimer) window.clearTimeout(searchTimer)
|
|
searchTimer = window.setTimeout(() => void loadList(), 220)
|
|
}
|
|
|
|
function clearSearch(): void {
|
|
search.value = ''
|
|
void loadList()
|
|
}
|
|
|
|
async function openInvoice(invoice: InvoiceSummary): Promise<void> {
|
|
screen.value = 'detail'
|
|
if (!(await billing.loadDetail(invoice.id))) screen.value = 'main'
|
|
}
|
|
|
|
function goBack(): void {
|
|
paymentOpen.value = false
|
|
billing.detail = null
|
|
screen.value = 'main'
|
|
}
|
|
|
|
async function payInvoice(): Promise<void> {
|
|
if (!billing.detail) return
|
|
const response = await billing.pay(billing.detail.id)
|
|
if (!response.success) {
|
|
showToast(t(`errors.${response.error ?? 'payment_failed'}`))
|
|
return
|
|
}
|
|
paymentOpen.value = false
|
|
showToast(t('payment.success'))
|
|
}
|
|
|
|
async function disputeInvoice(): Promise<void> {
|
|
if (!billing.detail) return
|
|
const response = await billing.dispute(billing.detail.id)
|
|
if (!response.success) {
|
|
showToast(t(`errors.${response.error ?? 'dispute_unavailable'}`))
|
|
return
|
|
}
|
|
showToast(t('detail.disputedSuccess'))
|
|
}
|
|
|
|
function onBillingMessage(event: MessageEvent): void {
|
|
if (!isTrustedRootMessageSource(event.source, window)) return
|
|
if (
|
|
event.data?.type !== 'billing:changed' &&
|
|
event.data?.type !== 'billing:new'
|
|
) {
|
|
return
|
|
}
|
|
|
|
if (tab.value !== 'overview') void loadList()
|
|
if (screen.value === 'detail' && billing.detail) {
|
|
void billing.loadDetail(billing.detail.id)
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
window.addEventListener('message', onBillingMessage)
|
|
await billing.loadOverview(direction.value)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('message', onBillingMessage)
|
|
if (searchTimer) window.clearTimeout(searchTimer)
|
|
if (toastTimer) window.clearTimeout(toastTimer)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<SkyAppPage
|
|
class="billing-app native-app"
|
|
:label="t('name')"
|
|
:dark="phone.isDarkMode"
|
|
accent="#1784ff"
|
|
accent-soft="rgba(23, 132, 255, 0.16)"
|
|
:class="{
|
|
'billing-app--light': !phone.isDarkMode,
|
|
}"
|
|
>
|
|
<SkyNavbar class="billing-navbar" :title="t('name')">
|
|
<template v-if="screen === 'detail'" #left>
|
|
<SkyNavbarBackLink :ariaLabel="t('back')" @click="goBack" />
|
|
</template>
|
|
<template #title>
|
|
<span class="billing-navbar__brand">
|
|
<ReceiptText :size="22" :stroke-width="2" />
|
|
<strong>{{ t('name') }}</strong>
|
|
</span>
|
|
</template>
|
|
</SkyNavbar>
|
|
|
|
<section v-if="screen === 'detail'" class="billing-scroll billing-detail">
|
|
<div v-if="billing.isLoading && !billing.detail" class="billing-loading">
|
|
<SkySpinner :label="phone.t('Common.loading')" />
|
|
<span>{{ phone.t('Common.loading') }}</span>
|
|
</div>
|
|
<template v-else-if="billing.detail">
|
|
<SkyGlass
|
|
:highlight="false"
|
|
class="billing-detail__hero"
|
|
:class="{
|
|
'billing-detail__hero--paid': billing.detail.status === 'paid',
|
|
}"
|
|
>
|
|
<template v-if="billing.detail.status === 'paid'">
|
|
<div class="billing-paid-mark">
|
|
<CheckCircle2 :size="38" :stroke-width="2.2" />
|
|
</div>
|
|
<strong class="billing-paid-title">{{ t('status.paid') }}</strong>
|
|
<strong class="billing-paid-amount">
|
|
{{ formatMoney(billing.detail.amount, billing.detail.currency) }}
|
|
</strong>
|
|
<span class="billing-paid-date">
|
|
{{
|
|
t('detail.paidOn', {
|
|
date: formatDateTime(billing.detail.paidAt),
|
|
})
|
|
}}
|
|
</span>
|
|
</template>
|
|
<template v-else>
|
|
<div class="billing-issuer-mark"><Building2 :size="26" /></div>
|
|
<div class="billing-detail__identity">
|
|
<strong>{{ billing.detail.issuerLabel }}</strong>
|
|
<span>#{{ billing.detail.id.slice(0, 13).toUpperCase() }}</span>
|
|
</div>
|
|
<SkyBadge
|
|
:class="`billing-status billing-status--${statusKey(billing.detail)}`"
|
|
>
|
|
{{ t(`status.${statusKey(billing.detail)}`) }}
|
|
</SkyBadge>
|
|
<div class="billing-detail__amount">
|
|
<span>{{ t('detail.total') }}</span>
|
|
<strong>{{
|
|
formatMoney(billing.detail.amount, billing.detail.currency)
|
|
}}</strong>
|
|
</div>
|
|
</template>
|
|
</SkyGlass>
|
|
|
|
<div class="billing-detail__section-title">
|
|
{{
|
|
t(
|
|
billing.detail.status === 'paid'
|
|
? 'detail.paymentInformation'
|
|
: 'detail.invoiceInformation',
|
|
)
|
|
}}
|
|
</div>
|
|
|
|
<SkyCard :content-wrap="false" class="billing-panel">
|
|
<div
|
|
v-if="billing.detail.status === 'paid'"
|
|
class="billing-detail-row"
|
|
>
|
|
<span class="billing-detail-row__icon"
|
|
><Building2 :size="18"
|
|
/></span>
|
|
<span class="billing-detail-row__copy">
|
|
<small>{{ t('detail.issuer') }}</small>
|
|
<strong>{{ billing.detail.issuerLabel }}</strong>
|
|
</span>
|
|
</div>
|
|
<div v-else class="billing-detail-row">
|
|
<span class="billing-detail-row__icon"
|
|
><ReceiptText :size="18"
|
|
/></span>
|
|
<span class="billing-detail-row__copy">
|
|
<small>{{ t('detail.reason') }}</small>
|
|
<strong>{{ billing.detail.title }}</strong>
|
|
</span>
|
|
</div>
|
|
<div class="billing-detail-row">
|
|
<span class="billing-detail-row__icon">
|
|
<FileCheck2 v-if="billing.detail.status === 'paid'" :size="18" />
|
|
<CalendarDays v-else :size="18" />
|
|
</span>
|
|
<span class="billing-detail-row__copy">
|
|
<small>{{
|
|
t(
|
|
billing.detail.status === 'paid'
|
|
? 'detail.paidAt'
|
|
: 'detail.issued',
|
|
)
|
|
}}</small>
|
|
<strong>{{
|
|
billing.detail.status === 'paid'
|
|
? formatDateTime(billing.detail.paidAt)
|
|
: formatDate(billing.detail.issuedAt)
|
|
}}</strong>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="billing.detail.status !== 'paid'"
|
|
class="billing-detail-row"
|
|
>
|
|
<span class="billing-detail-row__icon"><Clock3 :size="18" /></span>
|
|
<span class="billing-detail-row__copy">
|
|
<small>{{ t('detail.due') }}</small>
|
|
<strong>{{ formatDate(billing.detail.dueAt) }}</strong>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="billing.detail.paymentReference"
|
|
class="billing-detail-row"
|
|
>
|
|
<span class="billing-detail-row__icon"><Hash :size="18" /></span>
|
|
<span class="billing-detail-row__copy">
|
|
<small>{{ t('detail.paymentReference') }}</small>
|
|
<strong class="billing-detail-row__reference">
|
|
{{ billing.detail.paymentReference }}
|
|
</strong>
|
|
</span>
|
|
</div>
|
|
<div class="billing-detail-row">
|
|
<span class="billing-detail-row__icon"><Hash :size="18" /></span>
|
|
<span class="billing-detail-row__copy">
|
|
<small>{{ t('detail.invoiceNumber') }}</small>
|
|
<strong>#{{ billing.detail.id.toUpperCase() }}</strong>
|
|
</span>
|
|
</div>
|
|
</SkyCard>
|
|
|
|
<SkyCard v-if="billing.detail.description" class="billing-note">
|
|
<span>{{ t('detail.note') }}</span>
|
|
<p>{{ billing.detail.description }}</p>
|
|
</SkyCard>
|
|
|
|
<div v-if="billing.detail.canPay" class="billing-detail__actions">
|
|
<SkyButton
|
|
class="billing-action billing-action--pay"
|
|
@click="paymentOpen = true"
|
|
>
|
|
<WalletCards :size="16" />
|
|
{{ t('payment.payNow') }}
|
|
<ChevronRight class="billing-action-chevron" :size="16" />
|
|
</SkyButton>
|
|
<SkyButton
|
|
v-if="billing.detail.canDispute"
|
|
class="billing-action billing-action--dispute"
|
|
variant="secondary"
|
|
@click="disputeInvoice"
|
|
>
|
|
<ShieldAlert :size="16" />
|
|
{{ t('detail.dispute') }}
|
|
<ChevronRight class="billing-action-chevron" :size="16" />
|
|
</SkyButton>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
|
|
<section v-else class="billing-scroll billing-main">
|
|
<div
|
|
v-if="billing.isLoading && !billing.overview"
|
|
class="billing-loading"
|
|
>
|
|
<SkySpinner :label="phone.t('Common.loading')" />
|
|
<span>{{ phone.t('Common.loading') }}</span>
|
|
</div>
|
|
|
|
<div v-else-if="billing.error && !billing.overview" class="billing-empty">
|
|
<AlertTriangle :size="39" />
|
|
<strong>{{ t(`errors.${billing.error}`) }}</strong>
|
|
<SkyButton rounded @click="billing.loadOverview(direction)">
|
|
{{ t('tryAgain') }}
|
|
</SkyButton>
|
|
</div>
|
|
|
|
<template v-else-if="tab === 'overview' && billing.overview">
|
|
<div class="billing-summary">
|
|
<SkyGlass
|
|
:highlight="false"
|
|
class="billing-summary__item billing-summary__item--open"
|
|
>
|
|
<ReceiptText :size="19" />
|
|
<span>{{ t('summary.open') }}</span>
|
|
<strong>{{ billing.overview.openCount }}</strong>
|
|
</SkyGlass>
|
|
<SkyGlass
|
|
:highlight="false"
|
|
class="billing-summary__item billing-summary__item--due"
|
|
>
|
|
<CalendarDays :size="19" />
|
|
<span>{{ t('summary.due') }}</span>
|
|
<strong>{{ formatMoney(billing.overview.openTotal) }}</strong>
|
|
</SkyGlass>
|
|
<SkyGlass
|
|
:highlight="false"
|
|
class="billing-summary__item billing-summary__item--overdue"
|
|
>
|
|
<AlertTriangle :size="19" />
|
|
<span>{{ t('summary.overdue') }}</span>
|
|
<strong>{{ billing.overview.overdueCount }}</strong>
|
|
</SkyGlass>
|
|
</div>
|
|
|
|
<div
|
|
v-if="billing.overview.supportsSent"
|
|
class="billing-filter-panel billing-filter-panel--overview"
|
|
>
|
|
<span class="billing-filter-label">{{ t('filters.scope') }}</span>
|
|
<SkySegmented class="billing-direction">
|
|
<SkySegmentedButton
|
|
type="button"
|
|
:active="direction === 'inbox'"
|
|
@click="selectDirection('inbox')"
|
|
>
|
|
<Inbox :size="15" />
|
|
<span>{{ t('direction.inbox') }}</span>
|
|
<b
|
|
v-if="billing.overview.unreadCount"
|
|
class="billing-filter-count"
|
|
>
|
|
{{ Math.min(99, billing.overview.unreadCount) }}
|
|
</b>
|
|
</SkySegmentedButton>
|
|
<SkySegmentedButton
|
|
type="button"
|
|
:active="direction === 'sent'"
|
|
@click="selectDirection('sent')"
|
|
>
|
|
<Send :size="15" />
|
|
<span>{{ t('direction.sent') }}</span>
|
|
</SkySegmentedButton>
|
|
</SkySegmented>
|
|
</div>
|
|
|
|
<div class="billing-section-heading">
|
|
<div>
|
|
<span>{{ t('overview.eyebrow') }}</span>
|
|
<h2>{{ t('overview.urgent') }}</h2>
|
|
</div>
|
|
<SkyLink component="button" type="button" @click="selectTab('inbox')">
|
|
{{ t('overview.viewAll') }}
|
|
</SkyLink>
|
|
</div>
|
|
|
|
<div
|
|
v-if="billing.overview.urgentInvoices.length"
|
|
class="billing-card-list"
|
|
>
|
|
<button
|
|
v-for="invoice in billing.overview.urgentInvoices"
|
|
:key="invoice.id"
|
|
type="button"
|
|
class="billing-invoice-card"
|
|
@click="openInvoice(invoice)"
|
|
>
|
|
<span class="billing-issuer-mark"><Building2 :size="22" /></span>
|
|
<span class="billing-invoice-card__copy">
|
|
<strong>{{ invoice.title }}</strong>
|
|
<small>{{ invoice.issuerLabel }}</small>
|
|
<small
|
|
>{{ t('detail.due') }} {{ formatDate(invoice.dueAt) }}</small
|
|
>
|
|
</span>
|
|
<span class="billing-invoice-card__amount">
|
|
<SkyBadge
|
|
:class="`billing-status billing-status--${statusKey(invoice)}`"
|
|
>
|
|
{{ t(`status.${statusKey(invoice)}`) }}
|
|
</SkyBadge>
|
|
<strong>{{
|
|
formatMoney(invoice.amount, invoice.currency)
|
|
}}</strong>
|
|
<ChevronRight :size="17" />
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div v-else class="billing-empty">
|
|
<CheckCircle2 :size="39" />
|
|
<strong>{{ t('empty.openTitle') }}</strong>
|
|
<p>{{ t('empty.openBody') }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<header class="billing-view-heading">
|
|
<span class="billing-view-heading__icon">
|
|
<Inbox v-if="tab === 'inbox'" :size="20" />
|
|
<History v-else :size="20" />
|
|
</span>
|
|
<div>
|
|
<small>{{ t('name') }}</small>
|
|
<h1>{{ t(`tabs.${tab}`) }}</h1>
|
|
</div>
|
|
</header>
|
|
|
|
<div
|
|
v-if="tab === 'inbox' && billing.overview?.supportsSent"
|
|
class="billing-filter-panel"
|
|
>
|
|
<span class="billing-filter-label">{{ t('filters.scope') }}</span>
|
|
<SkySegmented class="billing-direction">
|
|
<SkySegmentedButton
|
|
type="button"
|
|
:active="direction === 'inbox'"
|
|
@click="selectDirection('inbox')"
|
|
>
|
|
<Inbox :size="15" />
|
|
<span>{{ t('direction.inbox') }}</span>
|
|
<b
|
|
v-if="billing.overview?.unreadCount"
|
|
class="billing-filter-count"
|
|
>
|
|
{{ Math.min(99, billing.overview.unreadCount) }}
|
|
</b>
|
|
</SkySegmentedButton>
|
|
<SkySegmentedButton
|
|
type="button"
|
|
:active="direction === 'sent'"
|
|
@click="selectDirection('sent')"
|
|
>
|
|
<Send :size="15" />
|
|
<span>{{ t('direction.sent') }}</span>
|
|
</SkySegmentedButton>
|
|
</SkySegmented>
|
|
</div>
|
|
<SkySearchbar
|
|
v-if="tab === 'inbox'"
|
|
class="billing-search"
|
|
:clear-label="phone.t('Common.clear')"
|
|
:placeholder="t('search')"
|
|
:value="search"
|
|
@clear="clearSearch"
|
|
@input="updateSearch"
|
|
/>
|
|
<div v-if="tab === 'inbox'" class="billing-status-filter">
|
|
<span class="billing-filter-label">{{ t('filters.status') }}</span>
|
|
<div class="billing-status-filter__scroll">
|
|
<SkySegmented class="billing-filters">
|
|
<SkySegmentedButton
|
|
v-for="entry in filters"
|
|
:key="entry"
|
|
type="button"
|
|
:active="filter === entry"
|
|
@click="selectFilter(entry)"
|
|
>
|
|
<component :is="filterIcons[entry]" :size="14" />
|
|
<span>{{ t(`filters.${entry}`) }}</span>
|
|
</SkySegmentedButton>
|
|
</SkySegmented>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="billing.isLoading"
|
|
class="billing-loading billing-loading--list"
|
|
>
|
|
<SkySpinner :label="phone.t('Common.loading')" />
|
|
</div>
|
|
<div v-else-if="visibleInvoices.length" class="billing-list">
|
|
<button
|
|
v-for="invoice in visibleInvoices"
|
|
:key="invoice.id"
|
|
type="button"
|
|
class="billing-list-row"
|
|
:class="{ 'is-unread': invoice.isUnread }"
|
|
@click="openInvoice(invoice)"
|
|
>
|
|
<span class="billing-list-row__icon">
|
|
<component :is="statusIcons[invoice.status]" :size="20" />
|
|
</span>
|
|
<span class="billing-list-row__copy">
|
|
<strong>{{ invoice.issuerLabel }}</strong>
|
|
<span>{{ invoice.title }}</span>
|
|
<small
|
|
>#{{ invoice.id.slice(0, 8).toUpperCase() }} ·
|
|
{{ formatDate(invoice.issuedAt) }}</small
|
|
>
|
|
</span>
|
|
<span class="billing-list-row__meta">
|
|
<SkyBadge
|
|
:class="`billing-status billing-status--${statusKey(invoice)}`"
|
|
>
|
|
{{ t(`status.${statusKey(invoice)}`) }}
|
|
</SkyBadge>
|
|
<strong>{{
|
|
formatMoney(invoice.amount, invoice.currency)
|
|
}}</strong>
|
|
</span>
|
|
</button>
|
|
<SkyButton
|
|
v-if="billing.hasMore"
|
|
clear
|
|
:disabled="billing.isLoadingMore"
|
|
class="billing-load-more"
|
|
@click="
|
|
billing.loadInvoices(
|
|
direction,
|
|
tab === 'history' ? 'paid' : filter,
|
|
search,
|
|
true,
|
|
)
|
|
"
|
|
>
|
|
<SkySpinner
|
|
v-if="billing.isLoadingMore"
|
|
:label="phone.t('Common.loading')"
|
|
/>
|
|
<span v-else>{{ t('loadMore') }}</span>
|
|
</SkyButton>
|
|
</div>
|
|
<div v-else class="billing-empty billing-empty--list">
|
|
<SearchX :size="39" />
|
|
<strong>{{ t(`empty.${tab}Title`) }}</strong>
|
|
<p>{{ t(`empty.${tab}Body`) }}</p>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
|
|
<SkyTabBar
|
|
v-if="screen === 'main'"
|
|
icons
|
|
labels
|
|
class="billing-tabbar"
|
|
:label="t('navigation')"
|
|
>
|
|
<SkyTabButton
|
|
class="billing-tab-button"
|
|
:active="tab === 'overview'"
|
|
:label="t('tabs.overview')"
|
|
@click="selectTab('overview')"
|
|
>
|
|
<template #icon>
|
|
<House
|
|
:size="20"
|
|
:fill="tab === 'overview' ? 'currentColor' : 'none'"
|
|
/>
|
|
</template>
|
|
</SkyTabButton>
|
|
<SkyTabButton
|
|
class="billing-tab-button"
|
|
:active="tab === 'inbox'"
|
|
:label="t('tabs.inbox')"
|
|
@click="selectTab('inbox')"
|
|
>
|
|
<template #icon>
|
|
<span class="billing-tab-icon">
|
|
<Inbox
|
|
:size="20"
|
|
:fill="tab === 'inbox' ? 'currentColor' : 'none'"
|
|
/>
|
|
<b v-if="billing.overview?.unreadCount">{{
|
|
Math.min(99, billing.overview.unreadCount)
|
|
}}</b>
|
|
</span>
|
|
</template>
|
|
</SkyTabButton>
|
|
<SkyTabButton
|
|
class="billing-tab-button"
|
|
:active="tab === 'history'"
|
|
:label="t('tabs.history')"
|
|
@click="selectTab('history')"
|
|
>
|
|
<template #icon><History :size="20" /></template>
|
|
</SkyTabButton>
|
|
</SkyTabBar>
|
|
|
|
<SkySheet
|
|
:opened="paymentOpen"
|
|
class="billing-payment-sheet"
|
|
:ariaLabelledby="billing.detail ? 'billing-payment-title' : undefined"
|
|
@backdropclick="paymentOpen = false"
|
|
@escape="paymentOpen = false"
|
|
>
|
|
<section v-if="billing.detail" class="billing-payment-sheet__content">
|
|
<span class="billing-payment-sheet__icon"
|
|
><WalletCards :size="27"
|
|
/></span>
|
|
<h2 id="billing-payment-title">{{ t('payment.title') }}</h2>
|
|
<p>{{ t('payment.body', { issuer: billing.detail.issuerLabel }) }}</p>
|
|
<SkyGlass :highlight="false" class="billing-payment-total">
|
|
<span>{{ billing.detail.title }}</span>
|
|
<strong>{{
|
|
formatMoney(billing.detail.amount, billing.detail.currency)
|
|
}}</strong>
|
|
</SkyGlass>
|
|
<SkyButton
|
|
block
|
|
rounded
|
|
large
|
|
:disabled="billing.isPaying"
|
|
@click="payInvoice"
|
|
>
|
|
<SkySpinner
|
|
v-if="billing.isPaying"
|
|
:label="phone.t('Common.loading')"
|
|
/>
|
|
<span v-else>{{ t('payment.confirm') }}</span>
|
|
</SkyButton>
|
|
<SkyLink component="button" type="button" @click="paymentOpen = false">
|
|
{{ t('payment.cancel') }}
|
|
</SkyLink>
|
|
</section>
|
|
</SkySheet>
|
|
|
|
<SkyNotification
|
|
:opened="toastOpen"
|
|
:text="toastText"
|
|
class="billing-notification"
|
|
/>
|
|
</SkyAppPage>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.billing-app {
|
|
--billing-blue: #1784ff;
|
|
--billing-border: rgb(255 255 255 / 9%);
|
|
--billing-panel: #171b21;
|
|
position: relative;
|
|
display: flex;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
flex-direction: column;
|
|
color: #f6f7f9;
|
|
background: #07090c;
|
|
font-family: var(--sky-font-family);
|
|
}
|
|
.billing-app--light {
|
|
--billing-border: rgb(15 23 42 / 10%);
|
|
--billing-panel: #fff;
|
|
color: #111827;
|
|
background: #f5f7fa;
|
|
}
|
|
.billing-navbar {
|
|
--sky-navbar-glass: color-mix(in srgb, #07090c 90%, transparent);
|
|
--sky-navbar-safe-area-top: 46px;
|
|
position: absolute;
|
|
z-index: 8;
|
|
inset: 0 0 auto;
|
|
border-bottom: 0;
|
|
background: color-mix(in srgb, #07090c 88%, transparent);
|
|
backdrop-filter: blur(18px);
|
|
}
|
|
.billing-app--light .billing-navbar {
|
|
--sky-navbar-glass: color-mix(in srgb, #f5f7fa 91%, transparent);
|
|
background: color-mix(in srgb, #f5f7fa 88%, transparent);
|
|
}
|
|
.billing-navbar::after {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: -18px;
|
|
left: 0;
|
|
height: 18px;
|
|
background: linear-gradient(to bottom, rgb(7 9 12 / 88%), transparent);
|
|
content: '';
|
|
pointer-events: none;
|
|
}
|
|
.billing-app--light .billing-navbar::after {
|
|
background: linear-gradient(to bottom, rgb(245 247 250 / 88%), transparent);
|
|
}
|
|
.billing-navbar__brand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
color: #ff6f67;
|
|
font-size: 22px;
|
|
font-weight: 850;
|
|
letter-spacing: -0.8px;
|
|
filter: drop-shadow(0 3px 8px rgb(0 0 0 / 18%));
|
|
transform: translateY(3px);
|
|
}
|
|
.billing-navbar__brand strong {
|
|
font: inherit;
|
|
}
|
|
.billing-scroll {
|
|
position: absolute;
|
|
inset: 94px 0 0;
|
|
overflow: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
.billing-main {
|
|
padding: 13px 12px 76px;
|
|
}
|
|
.billing-view-heading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 3px 4px 13px;
|
|
}
|
|
.billing-view-heading__icon {
|
|
display: grid;
|
|
width: 38px;
|
|
height: 38px;
|
|
flex: 0 0 auto;
|
|
place-items: center;
|
|
border: 1px solid rgb(23 132 255 / 20%);
|
|
border-radius: 12px;
|
|
color: var(--billing-blue);
|
|
background: rgb(23 132 255 / 12%);
|
|
}
|
|
.billing-view-heading small {
|
|
color: #7f8995;
|
|
font-size: 9px;
|
|
font-weight: 750;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
.billing-view-heading h1 {
|
|
margin: 1px 0 0;
|
|
font-size: 23px;
|
|
line-height: 1;
|
|
letter-spacing: -0.025em;
|
|
}
|
|
.billing-detail {
|
|
padding: 14px 12px 28px;
|
|
}
|
|
.billing-loading {
|
|
display: flex;
|
|
min-height: 60%;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
color: #929ba7;
|
|
}
|
|
.billing-loading--list {
|
|
min-height: 180px;
|
|
}
|
|
.billing-summary {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 8px;
|
|
}
|
|
.billing-summary__item {
|
|
display: flex;
|
|
min-height: 106px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
border: 1px solid var(--billing-border);
|
|
border-radius: 15px;
|
|
background: color-mix(in srgb, var(--billing-panel) 90%, transparent);
|
|
}
|
|
.billing-summary__item span {
|
|
color: #929ba7;
|
|
font-size: 10px;
|
|
}
|
|
.billing-summary__item strong {
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
font-size: 17px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.billing-summary__item svg {
|
|
color: #fff;
|
|
}
|
|
.billing-filter-panel {
|
|
display: grid;
|
|
gap: 7px;
|
|
margin-bottom: 11px;
|
|
border: 1px solid var(--billing-border);
|
|
border-radius: 15px;
|
|
padding: 10px;
|
|
background: color-mix(in srgb, var(--billing-panel) 94%, transparent);
|
|
}
|
|
.billing-filter-panel--overview {
|
|
margin-top: 12px;
|
|
margin-bottom: 0;
|
|
}
|
|
.billing-filter-label {
|
|
padding-left: 3px;
|
|
color: #7f8995;
|
|
font-size: 8px;
|
|
font-weight: 750;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
.billing-direction {
|
|
width: 100%;
|
|
}
|
|
.billing-direction :deep(button) {
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
}
|
|
.billing-filter-count {
|
|
min-width: 17px;
|
|
border-radius: 999px;
|
|
padding: 1px 5px;
|
|
color: #fff;
|
|
background: #ff3b30;
|
|
font-size: 8px;
|
|
line-height: 15px;
|
|
text-align: center;
|
|
}
|
|
.billing-section-heading {
|
|
display: flex;
|
|
align-items: end;
|
|
justify-content: space-between;
|
|
padding: 20px 4px 9px;
|
|
}
|
|
.billing-section-heading span {
|
|
color: #7f8995;
|
|
font-size: 9px;
|
|
font-weight: 750;
|
|
letter-spacing: 0.11em;
|
|
text-transform: uppercase;
|
|
}
|
|
.billing-section-heading h2 {
|
|
margin: 2px 0 0;
|
|
font-size: 20px;
|
|
}
|
|
.billing-card-list {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
.billing-invoice-card,
|
|
.billing-list-row {
|
|
width: 100%;
|
|
border: 1px solid var(--billing-border);
|
|
color: inherit;
|
|
background: color-mix(in srgb, var(--billing-panel) 94%, transparent);
|
|
text-align: left;
|
|
}
|
|
.billing-invoice-card {
|
|
display: grid;
|
|
min-height: 88px;
|
|
align-items: center;
|
|
grid-template-columns: 45px minmax(0, 1fr) auto;
|
|
gap: 10px;
|
|
border-radius: 16px;
|
|
padding: 11px 13px;
|
|
box-shadow: 0 10px 24px rgb(0 0 0 / 18%);
|
|
}
|
|
.billing-issuer-mark {
|
|
display: grid;
|
|
width: 42px;
|
|
height: 42px;
|
|
place-items: center;
|
|
border-radius: 13px;
|
|
color: #fff;
|
|
background: linear-gradient(145deg, #ff846c, #ff5266);
|
|
}
|
|
.billing-invoice-card__copy,
|
|
.billing-list-row__copy {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
}
|
|
.billing-invoice-card__copy strong {
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.billing-invoice-card__copy small {
|
|
margin-top: 3px;
|
|
overflow: hidden;
|
|
color: #8d96a1;
|
|
font-size: 10px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.billing-invoice-card__amount {
|
|
display: grid;
|
|
align-items: center;
|
|
grid-template-columns: auto 17px;
|
|
justify-items: end;
|
|
column-gap: 6px;
|
|
row-gap: 5px;
|
|
}
|
|
.billing-invoice-card__amount .billing-status {
|
|
grid-column: 1 / -1;
|
|
}
|
|
.billing-invoice-card__amount strong {
|
|
grid-column: 1;
|
|
grid-row: 2;
|
|
font-size: 16px;
|
|
}
|
|
.billing-invoice-card__amount svg {
|
|
grid-column: 2;
|
|
grid-row: 2;
|
|
color: #74808c;
|
|
}
|
|
.billing-status {
|
|
border: 1px solid currentColor;
|
|
border-radius: 999px;
|
|
padding: 2px 6px;
|
|
font-size: 8px;
|
|
font-weight: 750;
|
|
}
|
|
.billing-status--open {
|
|
color: #4da0ff;
|
|
background: rgb(25 132 255 / 12%);
|
|
}
|
|
.billing-status--overdue {
|
|
color: #ff625d;
|
|
background: rgb(255 98 93 / 12%);
|
|
}
|
|
.billing-status--processing {
|
|
color: #f1ad35;
|
|
background: rgb(241 173 53 / 12%);
|
|
}
|
|
.billing-status--paid {
|
|
color: #48c76f;
|
|
background: rgb(72 199 111 / 12%);
|
|
}
|
|
.billing-status--disputed {
|
|
color: #a987ff;
|
|
background: rgb(169 135 255 / 12%);
|
|
}
|
|
.billing-status--cancelled,
|
|
.billing-status--refunded {
|
|
color: #8e98a5;
|
|
background: rgb(142 152 165 / 12%);
|
|
}
|
|
.billing-search {
|
|
margin: 0 0 11px;
|
|
}
|
|
.billing-search :deep(.sky-searchbar__control) {
|
|
min-height: 36px;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--billing-panel) 96%, transparent);
|
|
}
|
|
.billing-status-filter {
|
|
display: grid;
|
|
gap: 7px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.billing-status-filter__scroll {
|
|
overflow: hidden;
|
|
}
|
|
.billing-filters {
|
|
width: 100%;
|
|
}
|
|
.billing-filters :deep(button) {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex: 1;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
}
|
|
.billing-list {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
.billing-list-row {
|
|
display: grid;
|
|
min-height: 82px;
|
|
align-items: center;
|
|
grid-template-columns: 38px minmax(0, 1fr) auto;
|
|
gap: 9px;
|
|
border-radius: 14px;
|
|
padding: 10px;
|
|
}
|
|
.billing-list-row.is-unread {
|
|
border-color: rgb(23 132 255 / 48%);
|
|
}
|
|
.billing-list-row__icon {
|
|
display: grid;
|
|
width: 36px;
|
|
height: 36px;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
color: var(--billing-blue);
|
|
background: rgb(23 132 255 / 13%);
|
|
}
|
|
.billing-list-row__copy strong,
|
|
.billing-list-row__copy span,
|
|
.billing-list-row__copy small {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.billing-list-row__copy strong {
|
|
font-size: 12px;
|
|
}
|
|
.billing-list-row__copy span {
|
|
margin-top: 2px;
|
|
font-size: 11px;
|
|
}
|
|
.billing-list-row__copy small {
|
|
margin-top: 3px;
|
|
color: #87919c;
|
|
font-size: 9px;
|
|
}
|
|
.billing-list-row__meta {
|
|
display: grid;
|
|
justify-items: end;
|
|
gap: 8px;
|
|
}
|
|
.billing-list-row__meta strong {
|
|
font-size: 13px;
|
|
}
|
|
.billing-load-more {
|
|
margin: 8px auto 0;
|
|
}
|
|
.billing-empty {
|
|
display: flex;
|
|
min-height: 270px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
color: #7f8995;
|
|
text-align: center;
|
|
}
|
|
.billing-empty svg {
|
|
color: #48c76f;
|
|
}
|
|
.billing-empty strong {
|
|
color: inherit;
|
|
font-size: 15px;
|
|
}
|
|
.billing-empty p {
|
|
max-width: 245px;
|
|
margin: 0;
|
|
font-size: 11px;
|
|
line-height: 1.5;
|
|
}
|
|
.billing-empty--list svg {
|
|
color: #73808d;
|
|
}
|
|
.billing-detail__hero {
|
|
display: grid;
|
|
align-items: center;
|
|
grid-template-columns: 44px minmax(0, 1fr) auto;
|
|
gap: 10px;
|
|
border: 1px solid var(--billing-border);
|
|
border-radius: 17px;
|
|
padding: 14px;
|
|
background: color-mix(in srgb, var(--billing-panel) 93%, transparent);
|
|
}
|
|
.billing-detail__hero--paid {
|
|
display: flex;
|
|
min-height: 208px;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
border-color: rgb(72 199 111 / 24%);
|
|
padding: 24px 18px;
|
|
background:
|
|
radial-gradient(circle at 50% 6%, rgb(72 199 111 / 18%), transparent 52%),
|
|
color-mix(in srgb, var(--billing-panel) 94%, transparent);
|
|
text-align: center;
|
|
}
|
|
.billing-paid-mark {
|
|
display: grid;
|
|
width: 72px;
|
|
height: 72px;
|
|
margin-bottom: 3px;
|
|
place-items: center;
|
|
border: 1px solid rgb(72 199 111 / 28%);
|
|
border-radius: 50%;
|
|
color: #48c76f;
|
|
background: rgb(72 199 111 / 12%);
|
|
box-shadow: 0 12px 32px rgb(72 199 111 / 12%);
|
|
}
|
|
.billing-paid-title {
|
|
color: #48c76f;
|
|
font-size: 19px;
|
|
}
|
|
.billing-paid-amount {
|
|
font-size: 31px;
|
|
letter-spacing: -0.035em;
|
|
}
|
|
.billing-paid-date {
|
|
color: #89939f;
|
|
font-size: 10px;
|
|
}
|
|
.billing-detail__identity {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
}
|
|
.billing-detail__identity strong {
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.billing-detail__identity span {
|
|
margin-top: 3px;
|
|
color: #89939f;
|
|
font-size: 9px;
|
|
}
|
|
.billing-detail__amount {
|
|
display: flex;
|
|
align-items: center;
|
|
grid-column: 1 / -1;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
border-top: 1px solid var(--billing-border);
|
|
padding-top: 17px;
|
|
}
|
|
.billing-detail__amount span {
|
|
color: #8b95a0;
|
|
font-size: 10px;
|
|
}
|
|
.billing-detail__amount strong {
|
|
font-size: 30px;
|
|
letter-spacing: -0.03em;
|
|
}
|
|
.billing-detail__section-title {
|
|
margin: 17px 4px 7px;
|
|
color: #89939f;
|
|
font-size: 11px;
|
|
font-weight: 750;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
.billing-panel,
|
|
.billing-note {
|
|
margin-top: 12px;
|
|
margin-right: 0;
|
|
margin-left: 0;
|
|
border: 1px solid var(--billing-border);
|
|
border-radius: 16px;
|
|
background: color-mix(in srgb, var(--billing-panel) 94%, transparent);
|
|
}
|
|
.billing-detail__section-title + .billing-panel {
|
|
margin-top: 0;
|
|
}
|
|
.billing-detail-row {
|
|
display: flex;
|
|
min-height: 55px;
|
|
align-items: center;
|
|
gap: 11px;
|
|
margin: 0 13px;
|
|
border-bottom: 1px solid var(--billing-border);
|
|
padding: 9px 0;
|
|
}
|
|
.billing-detail-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
.billing-detail-row__icon {
|
|
display: grid;
|
|
width: 32px;
|
|
height: 32px;
|
|
flex: 0 0 auto;
|
|
place-items: center;
|
|
border-radius: 10px;
|
|
color: var(--billing-blue);
|
|
background: rgb(23 132 255 / 11%);
|
|
}
|
|
.billing-detail__hero--paid
|
|
+ .billing-detail__section-title
|
|
+ .billing-panel
|
|
.billing-detail-row__icon {
|
|
color: #48c76f;
|
|
background: rgb(72 199 111 / 11%);
|
|
}
|
|
.billing-detail-row__copy {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
}
|
|
.billing-detail-row__copy small {
|
|
color: #89939f;
|
|
font-size: 9px;
|
|
}
|
|
.billing-detail-row__copy strong {
|
|
overflow-wrap: anywhere;
|
|
font-size: 11px;
|
|
font-weight: 650;
|
|
line-height: 1.35;
|
|
}
|
|
.billing-detail-row__reference {
|
|
color: #48c76f;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 9.5px !important;
|
|
}
|
|
.billing-note span {
|
|
color: #89939f;
|
|
font-size: 9px;
|
|
font-weight: 750;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
.billing-note p {
|
|
margin: 7px 0 0;
|
|
font-size: 11px;
|
|
line-height: 1.55;
|
|
}
|
|
.billing-detail__actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 8px;
|
|
margin-top: 16px;
|
|
}
|
|
.billing-detail__actions :deep(.sky-button) {
|
|
width: 100%;
|
|
height: 44px;
|
|
min-height: 44px;
|
|
justify-content: flex-start;
|
|
border-radius: 14px;
|
|
padding: 0 10px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
transition:
|
|
transform 160ms ease,
|
|
border-color 160ms ease,
|
|
background-color 160ms ease,
|
|
box-shadow 160ms ease;
|
|
}
|
|
.billing-detail__actions :deep(.billing-action--pay) {
|
|
background: var(--billing-blue);
|
|
box-shadow: 0 8px 22px rgb(23 132 255 / 18%);
|
|
}
|
|
.billing-detail__actions :deep(.billing-action--dispute) {
|
|
border-color: var(--billing-border);
|
|
color: #f6f7f9;
|
|
background: color-mix(in srgb, var(--billing-panel) 94%, transparent);
|
|
}
|
|
.billing-app--light .billing-detail__actions :deep(.billing-action--dispute) {
|
|
color: #111827;
|
|
}
|
|
.billing-action-chevron {
|
|
margin-left: auto;
|
|
transition: transform 160ms ease;
|
|
}
|
|
@media (hover: hover) {
|
|
.billing-detail__actions :deep(.billing-action:hover) {
|
|
transform: translateY(-2px);
|
|
}
|
|
.billing-detail__actions :deep(.billing-action--pay:hover) {
|
|
background: #369bff;
|
|
box-shadow: 0 10px 26px rgb(23 132 255 / 32%);
|
|
}
|
|
.billing-detail__actions :deep(.billing-action--dispute:hover) {
|
|
border-color: rgb(255 255 255 / 20%);
|
|
background: color-mix(in srgb, var(--billing-panel) 88%, white);
|
|
}
|
|
.billing-app--light
|
|
.billing-detail__actions
|
|
:deep(.billing-action--dispute:hover) {
|
|
border-color: rgb(15 23 42 / 18%);
|
|
background: #eef2f7;
|
|
}
|
|
.billing-detail__actions
|
|
:deep(.billing-action:hover .billing-action-chevron) {
|
|
transform: translateX(2px);
|
|
}
|
|
}
|
|
:global(.billing-tab-button) {
|
|
width: auto !important;
|
|
min-width: 0 !important;
|
|
max-width: none !important;
|
|
flex: 1 1 0 !important;
|
|
padding-inline: 3px !important;
|
|
}
|
|
.billing-tab-label {
|
|
display: block;
|
|
max-width: 72px;
|
|
overflow: hidden;
|
|
font-size: 9.5px;
|
|
line-height: 12px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.billing-tab-icon {
|
|
position: relative;
|
|
}
|
|
.billing-tab-icon b {
|
|
position: absolute;
|
|
top: -6px;
|
|
right: -9px;
|
|
min-width: 15px;
|
|
border-radius: 999px;
|
|
padding: 1px 4px;
|
|
color: #fff;
|
|
background: #ff3b30;
|
|
font-size: 8px;
|
|
text-align: center;
|
|
}
|
|
.billing-payment-sheet__content {
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: 12px;
|
|
border-radius: 22px 22px 0 0;
|
|
padding: 22px 16px 36px;
|
|
color: #f6f7f9;
|
|
background: #15191f;
|
|
text-align: center;
|
|
}
|
|
.billing-app--light .billing-payment-sheet__content {
|
|
color: #111827;
|
|
background: #fff;
|
|
}
|
|
.billing-payment-sheet__icon {
|
|
display: grid;
|
|
width: 52px;
|
|
height: 52px;
|
|
place-items: center;
|
|
border-radius: 16px;
|
|
color: #fff;
|
|
background: var(--billing-blue);
|
|
}
|
|
.billing-payment-sheet h2 {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
}
|
|
.billing-payment-sheet p {
|
|
margin: 0;
|
|
color: #909aa5;
|
|
font-size: 11px;
|
|
}
|
|
.billing-payment-total {
|
|
display: flex;
|
|
width: 100%;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border: 1px solid var(--billing-border);
|
|
border-radius: 14px;
|
|
padding: 13px;
|
|
text-align: left;
|
|
}
|
|
.billing-payment-total span {
|
|
color: #909aa5;
|
|
font-size: 11px;
|
|
}
|
|
.billing-payment-total strong {
|
|
font-size: 18px;
|
|
}
|
|
.billing-payment-sheet :deep(.sky-button) {
|
|
width: 100%;
|
|
}
|
|
@supports not (color: color-mix(in srgb, white, black)) {
|
|
.billing-navbar {
|
|
--sky-navbar-glass: rgb(7 9 12 / 90%);
|
|
background: rgb(7 9 12 / 88%);
|
|
}
|
|
.billing-app--light .billing-navbar {
|
|
--sky-navbar-glass: rgb(245 247 250 / 91%);
|
|
background: rgb(245 247 250 / 88%);
|
|
}
|
|
.billing-summary__item,
|
|
.billing-filter-panel,
|
|
.billing-invoice-card,
|
|
.billing-list-row,
|
|
.billing-search :deep(.sky-searchbar__control),
|
|
.billing-detail__hero,
|
|
.billing-panel,
|
|
.billing-note,
|
|
.billing-detail__actions :deep(.billing-action--dispute) {
|
|
background: var(--billing-panel);
|
|
}
|
|
.billing-detail__hero--paid {
|
|
background:
|
|
radial-gradient(circle at 50% 6%, rgb(72 199 111 / 18%), transparent 52%),
|
|
var(--billing-panel);
|
|
}
|
|
}
|
|
.billing-notification {
|
|
z-index: 50;
|
|
}
|
|
</style>
|