feat: portal redesign, pricing catalog, partner-staff invites
- portal: new admin/ and partner/ surfaces with full component library (AppLauncher, Avatar, Badge, Card, Modal, Tabs, etc.), composables, layouts, partner-routing middleware, and supporting server APIs - pricing: Price schema/module with operator CRUD, pricing.vue catalog UI, Subscription extended with cycle/currency/perSeatAmount/seats snapshots for stable MRR aggregation - partner staff: User.partnerId, invite-partner-user DTO and flow, /partners/:slug/users endpoints, InvitePartnerUserModal, shared dezky-partner-staff Authentik group - /me: partner-aware endpoint returning user + partner context so portal can route between end-user and partner-admin surfaces - tenant: seats field for portfolio displays and future MRR calculations - operator: pricing page, signed-out page, useMe/useToast composables, ToastStack
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
// Tiny inline SVG sparkline. Takes a series of numbers and renders a stroked
|
||||
// polyline plus a faint area fill underneath. Used on the partner dashboard
|
||||
// (90-day MRR trend) and on the reports/revenue tab.
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
values: number[]
|
||||
width?: number
|
||||
height?: number
|
||||
stroke?: string
|
||||
fill?: string
|
||||
strokeWidth?: number
|
||||
showDot?: boolean
|
||||
}>(),
|
||||
{
|
||||
width: 420,
|
||||
height: 64,
|
||||
stroke: 'var(--text)',
|
||||
fill: 'var(--row-hover)',
|
||||
strokeWidth: 1.4,
|
||||
showDot: true,
|
||||
},
|
||||
)
|
||||
|
||||
const geometry = computed(() => {
|
||||
const data = props.values
|
||||
if (!data.length) return { line: '', area: '', last: { x: 0, y: 0 }, min: 0, max: 0 }
|
||||
const min = Math.min(...data)
|
||||
const max = Math.max(...data)
|
||||
const range = max - min || 1
|
||||
const pts = data.map((v, i) => {
|
||||
const x = (i / (data.length - 1)) * props.width
|
||||
const y = props.height - ((v - min) / range) * (props.height - 6) - 3
|
||||
return [x, y] as const
|
||||
})
|
||||
const line = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`).join(' ')
|
||||
const area = `${line} L ${props.width} ${props.height} L 0 ${props.height} Z`
|
||||
const last = { x: pts[pts.length - 1][0], y: pts[pts.length - 1][1] }
|
||||
return { line, area, last, min, max }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" preserveAspectRatio="none" style="display:block">
|
||||
<path :d="geometry.area" :fill="fill" />
|
||||
<path :d="geometry.line" fill="none" :stroke="stroke" :stroke-width="strokeWidth" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle v-if="showDot" :cx="geometry.last.x" :cy="geometry.last.y" :r="3" :fill="stroke" />
|
||||
</svg>
|
||||
</template>
|
||||
Reference in New Issue
Block a user