Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.

This commit is contained in:
2026-04-17 14:45:30 +03:00
parent eb773c5531
commit 0a8a42d69b
447 changed files with 70696 additions and 492 deletions
@@ -0,0 +1,857 @@
// frontend/src/pages/bellcloud/devices/tabs/WarrantyTab.jsx
import Button from '@/components/ui/Button'
import StatusBadge from '@/components/ui/StatusBadge'
import { fmtDateMedium, fmtDateLong } from '@/lib/formatters'
// ─── Helpers ──────────────────────────────────────────────────────────────────
function parseDate(val) {
if (!val) return null
// Firestore Timestamp object
if (typeof val === 'object' && typeof val.seconds === 'number') {
return new Date(val.seconds * 1000)
}
const d = new Date(val)
return isNaN(d.getTime()) ? null : d
}
function addDays(date, days) {
if (!date || !days) return null
const d = new Date(date)
d.setDate(d.getDate() + Number(days))
return d
}
function daysUntil(date) {
if (!date) return null
return Math.ceil((date.getTime() - Date.now()) / 86400000)
}
/** 0–100 how much of the period has ELAPSED */
function calcProgress(start, end) {
if (!start || !end) return null
const total = end.getTime() - start.getTime()
if (total <= 0) return 100
const elapsed = Date.now() - start.getTime()
return Math.min(100, Math.max(0, Math.round((elapsed / total) * 100)))
}
/** Remaining portion (inverse of elapsed) */
function calcRemaining(start, end) {
const p = calcProgress(start, end)
return p === null ? null : 100 - p
}
function daysLabel(days) {
if (days == null || days === 0) return '—'
if (days % 365 === 0) {
const y = days / 365
return `${y} year${y !== 1 ? 's' : ''}`
}
if (days % 30 === 0) {
const m = days / 30
return `${m} month${m !== 1 ? 's' : ''}`
}
return `${days} day${days !== 1 ? 's' : ''}`
}
// ─── Glass surface styles ─────────────────────────────────────────────────────
const GLASS = {
background: 'rgba(28, 32, 38, 0.30)',
backdropFilter: 'var(--blur-modal)',
WebkitBackdropFilter: 'var(--blur-modal)',
border: '1px solid var(--color-border)',
boxShadow: 'var(--shadow-card), var(--shadow-md)',
borderRadius: 'var(--radius-xl)',
}
const GLASS_ELEVATED = {
background: 'rgba(28, 32, 38, 0.40)',
backdropFilter: 'var(--blur-modal)',
WebkitBackdropFilter: 'var(--blur-modal)',
border: '1px solid var(--color-border)',
boxShadow: 'var(--shadow-sm)',
borderRadius: 'var(--radius-lg)',
}
// ─── Primitives ───────────────────────────────────────────────────────────────
function SectionLabel({ children }) {
return (
<span style={{
fontSize: 'var(--font-size-xs)',
fontWeight: 'var(--font-weight-semibold)',
letterSpacing: 'var(--tracking-wide)',
textTransform: 'uppercase',
color: 'var(--color-text-muted)',
}}>
{children}
</span>
)
}
function InfoCell({ label, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
<SectionLabel>{label}</SectionLabel>
<span style={{
fontSize: 'var(--font-size-base)',
fontWeight: 'var(--font-weight-medium)',
color: 'var(--color-text-primary)',
marginTop: 'var(--space-1)',
}}>
{children ?? '—'}
</span>
</div>
)
}
function ProgressBar({ value, color = 'var(--color-primary)', label, showPct = true }) {
const pct = Math.max(0, Math.min(100, value ?? 0))
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{(label || showPct) && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{label && (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{label}
</span>
)}
{showPct && (
<span style={{
fontSize: 'var(--font-size-xs)',
fontWeight: 'var(--font-weight-semibold)',
color: 'var(--color-text-secondary)',
}}>
{pct}%
</span>
)}
</div>
)}
<div style={{
height: 6,
borderRadius: 'var(--radius-full)',
backgroundColor: 'var(--color-bg-island)',
overflow: 'hidden',
}}>
<div style={{
height: '100%',
width: `${pct}%`,
backgroundColor: color,
borderRadius: 'var(--radius-full)',
transition: 'width 0.6s ease',
boxShadow: `0 0 8px ${color}60`,
}} />
</div>
</div>
)
}
function Divider() {
return <div style={{ height: 1, backgroundColor: 'var(--color-border)', margin: 'var(--space-1) 0' }} />
}
function DetailRow({ label, value, valueColor }) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 'var(--space-4)', minHeight: 28 }}>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', flexShrink: 0 }}>
{label}
</span>
<span style={{
fontSize: 'var(--font-size-sm)',
fontWeight: 'var(--font-weight-medium)',
color: valueColor ?? (!value || value === '—' ? 'var(--color-text-muted)' : 'var(--color-text-primary)'),
textAlign: 'right',
}}>
{value ?? '—'}
</span>
</div>
)
}
// ─── Subscription Hero ────────────────────────────────────────────────────────
function SubscriptionHero({ sub, canEdit, onEditSubscription, deviceUsers }) {
const subscrStart = parseDate(sub?.subscrStart)
const subscrEnd = addDays(subscrStart, sub?.subscrDuration)
const daysLeft = daysUntil(subscrEnd)
const remaining = calcRemaining(subscrStart, subscrEnd)
const isActive = daysLeft === null || daysLeft > 0
const tier = sub?.subscrTier || null
const progressColor = daysLeft === null
? 'var(--color-primary)'
: daysLeft > 60
? 'var(--color-success)'
: daysLeft > 14
? 'var(--color-warning)'
: 'var(--color-danger)'
// Registered users = actual device users count passed from parent
const regUsers = Array.isArray(deviceUsers) ? deviceUsers.length : null
const maxUsers = sub?.maxUsers ?? null
const hwOutputs = sub?.hw_outputs ?? null
const maxOutputs = sub?.maxOutputs ?? null
return (
<div style={{
...GLASS,
padding: 'var(--space-6) var(--space-8)',
position: 'relative',
overflow: 'hidden',
borderLeft: '3px solid var(--color-primary)',
}}>
{/* Left accent glow */}
<div aria-hidden="true" style={{
position: 'absolute',
left: 0, top: 0, bottom: 0,
width: 140,
pointerEvents: 'none',
background: 'linear-gradient(90deg, rgba(192,193,255,0.06) 0%, transparent 100%)',
}} />
{/* Right ambient glow */}
<div aria-hidden="true" style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
background: 'radial-gradient(ellipse 60% 120% at 90% 50%, rgba(192,193,255,0.03) 0%, transparent 70%)',
}} />
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 'var(--space-6)', flexWrap: 'wrap' }}>
{/* ── Column 1: Tier ── */}
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', minWidth: 180, flexShrink: 0 }}>
<div style={{
width: 48, height: 48,
borderRadius: 'var(--radius-lg)',
backgroundColor: 'var(--color-primary-subtle)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
</div>
<div>
<div style={{
fontFamily: 'var(--font-family-display)',
fontSize: 'var(--font-size-lg)',
fontWeight: 'var(--font-weight-semibold)',
letterSpacing: 'var(--tracking-tight)',
color: 'var(--color-text-primary)',
textTransform: 'capitalize',
}}>
{tier ? `${tier} Tier` : 'No Subscription'}
</div>
<div style={{ marginTop: 4 }}>
<StatusBadge variant={isActive ? 'success' : 'danger'}>
{isActive ? 'Active License' : 'Expired'}
</StatusBadge>
</div>
</div>
</div>
{/* ── Column 2: Progress bar ── */}
<div style={{
flex: 1, minWidth: 220,
paddingLeft: 'var(--space-6)',
borderLeft: '1px solid var(--color-border)',
display: 'flex', flexDirection: 'column', gap: 'var(--space-3)',
}}>
{/* Top label row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<span style={{
fontSize: 'var(--font-size-sm)',
fontWeight: 'var(--font-weight-medium)',
color: 'var(--color-text-primary)',
}}>
{daysLeft === null
? 'No expiry set'
: daysLeft <= 0
? 'Subscription expired'
: `${daysLeft} days remaining`}
</span>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
{daysLabel(sub?.subscrDuration)} plan
</span>
</div>
{/* Progress bar */}
<ProgressBar value={remaining ?? 0} color={progressColor} showPct={false} />
{/* Bottom: inline Start / Renewal */}
<div style={{ display: 'flex', gap: 'var(--space-8)', alignItems: 'center' }}>
{subscrStart && (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
Start:{' '}
<span style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-medium)' }}>
{fmtDateMedium(subscrStart.toISOString())}
</span>
</span>
)}
{subscrEnd && (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
Renewal:{' '}
<span style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-medium)' }}>
{fmtDateMedium(subscrEnd.toISOString())}
</span>
</span>
)}
</div>
</div>
{/* ── Column 3: Users + Outputs + Edit — side by side ── */}
<div style={{
display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 'var(--space-6)',
paddingLeft: 'var(--space-6)',
borderLeft: '1px solid var(--color-border)',
flexShrink: 0,
}}>
{/* Users block */}
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<div style={{
width: 48, height: 48,
borderRadius: 'var(--radius-lg)',
backgroundColor: 'var(--color-primary-subtle)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<span style={{
fontSize: 'var(--font-size-xs)',
fontWeight: 'var(--font-weight-semibold)',
color: 'var(--color-text-secondary)',
}}>
Device Users
</span>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
Max:{' '}
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
{maxUsers ?? '—'}
</span>
{regUsers !== null && (
<>
{' '}<span style={{ color: 'var(--color-border-strong)' }}>|</span>{' '}
Reg:{' '}
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
{regUsers}
</span>
</>
)}
</span>
</div>
</div>
{/* Outputs block */}
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<div style={{
width: 48, height: 48,
borderRadius: 'var(--radius-lg)',
backgroundColor: 'var(--color-primary-subtle)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
<path d="M19.07 4.93a10 10 0 010 14.14M4.93 4.93a10 10 0 000 14.14" />
</svg>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<span style={{
fontSize: 'var(--font-size-xs)',
fontWeight: 'var(--font-weight-semibold)',
color: 'var(--color-text-secondary)',
}}>
Outputs
</span>
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
HW:{' '}
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
{hwOutputs ?? '—'}
</span>
{maxOutputs !== null && (
<>
{' '}<span style={{ color: 'var(--color-border-strong)' }}>|</span>{' '}
Max:{' '}
<span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
{maxOutputs}
</span>
</>
)}
</span>
</div>
</div>
{/* Edit pencil icon button */}
{canEdit && (
<button
type="button"
onClick={onEditSubscription}
aria-label="Edit subscription plan"
title="Edit subscription plan"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: 'var(--radius-md)',
border: 'none',
background: 'transparent',
color: 'var(--color-text-muted)',
cursor: 'pointer',
transition: 'background 0.15s, color 0.15s',
flexShrink: 0,
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'rgba(49,53,60,0.50)'
e.currentTarget.style.color = 'var(--color-text-primary)'
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'var(--color-text-muted)'
}}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
)}
</div>
</div>
</div>
)
}
// ─── Warranty Card ────────────────────────────────────────────────────────────
function WarrantyCard({ stats, canEdit, onEditWarranty }) {
const warrantyStart = parseDate(stats?.warrantyStart)
const warrantyEnd = addDays(warrantyStart, stats?.warrantyPeriod)
const daysLeft = daysUntil(warrantyEnd)
const remaining = calcRemaining(warrantyStart, warrantyEnd)
const isVoid = stats?.warrantyActive === false
const isExpired = !isVoid && daysLeft !== null && daysLeft <= 0
const isActive = !isVoid && !isExpired && (daysLeft === null || daysLeft > 0)
const statusLabel = isVoid ? 'Warranty Void' : isExpired ? 'Expired' : isActive ? 'Active' : '—'
const statusVariant = isVoid ? 'warning' : isExpired ? 'danger' : isActive ? 'success' : 'neutral'
const borderAccent = isVoid
? 'var(--color-warning)'
: isExpired
? 'var(--color-danger)'
: 'var(--color-success)'
const progressColor = isVoid || isExpired ? 'var(--color-danger)' : 'var(--color-success)'
return (
<div style={{
...GLASS,
padding: 'var(--space-6)',
borderLeft: `3px solid ${borderAccent}`,
position: 'relative',
overflow: 'hidden',
}}>
{/* Accent side glow */}
<div aria-hidden="true" style={{
position: 'absolute',
left: 0, top: 0, bottom: 0,
width: 140,
pointerEvents: 'none',
background: `linear-gradient(90deg, ${borderAccent}12 0%, transparent 100%)`,
}} />
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 'var(--space-4)', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<div style={{
width: 44, height: 44,
borderRadius: 'var(--radius-full)',
backgroundColor: `${borderAccent}18`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
{isActive ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={borderAccent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<polyline points="9 12 11 14 15 10" />
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke={borderAccent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
)}
</div>
<div>
<div style={{
fontFamily: 'var(--font-family-display)',
fontSize: 'var(--font-size-lg)',
fontWeight: 'var(--font-weight-bold)',
letterSpacing: 'var(--tracking-tight)',
color: borderAccent,
}}>
{statusLabel}
</div>
<div style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', marginTop: 3 }}>
{isVoid
? 'Warranty has been manually voided'
: isExpired
? warrantyEnd
? `Coverage ended on ${fmtDateLong(warrantyEnd.toISOString())}`
: 'Coverage has expired'
: warrantyEnd
? `Active until ${fmtDateMedium(warrantyEnd.toISOString())}`
: 'Warranty is currently active'}
</div>
</div>
</div>
{canEdit && (
<Button variant="ghost" size="sm" onClick={onEditWarranty}>
Edit Warranty
</Button>
)}
</div>
{/* Progress */}
<ProgressBar
value={remaining ?? 0}
color={progressColor}
label={
daysLeft === null
? 'Coverage period'
: daysLeft <= 0
? 'Coverage elapsed'
: `${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining`
}
/>
{/* Detail grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(130px, 1fr))',
gap: 'var(--space-4) var(--space-6)',
}}>
<InfoCell label="Start Date">
{warrantyStart ? fmtDateMedium(warrantyStart.toISOString()) : '—'}
</InfoCell>
<InfoCell label="Expiry Date">
{warrantyEnd ? fmtDateMedium(warrantyEnd.toISOString()) : '—'}
</InfoCell>
<InfoCell label="Coverage Period">
{daysLabel(stats?.warrantyPeriod)}
</InfoCell>
<InfoCell label="Status">
<StatusBadge variant={statusVariant}>{statusLabel}</StatusBadge>
</InfoCell>
</div>
</div>
</div>
)
}
// ─── Maintenance Card ─────────────────────────────────────────────────────────
function MaintenanceCard({ stats }) {
const maintainedOn = parseDate(stats?.maintainedOn)
const nextMaint = addDays(maintainedOn, stats?.maintainancePeriod)
const daysLeft = daysUntil(nextMaint)
const remaining = calcRemaining(maintainedOn, nextMaint)
const isOverdue = daysLeft !== null && daysLeft <= 0
const isDueSoon = daysLeft !== null && daysLeft > 0 && daysLeft <= 30
const statusVariant = isOverdue ? 'danger' : isDueSoon ? 'warning' : 'neutral'
const progressColor = isOverdue
? 'var(--color-danger)'
: isDueSoon
? 'var(--color-warning)'
: 'var(--color-info)'
return (
<div style={{ ...GLASS, padding: 'var(--space-6)', display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>
{/* Title */}
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z" />
</svg>
<span style={{
fontFamily: 'var(--font-family-display)',
fontSize: 'var(--font-size-md)',
fontWeight: 'var(--font-weight-semibold)',
letterSpacing: 'var(--tracking-tight)',
color: 'var(--color-text-primary)',
}}>
Maintenance Schedule
</span>
{(isOverdue || isDueSoon) && (
<StatusBadge variant={statusVariant}>
{isOverdue ? 'Overdue' : 'Due Soon'}
</StatusBadge>
)}
</div>
{/* Next service highlight */}
{nextMaint ? (
<div style={{
...GLASS_ELEVATED,
padding: 'var(--space-4)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--space-4)', flexWrap: 'wrap',
}}>
<div>
<SectionLabel>Next Scheduled Service</SectionLabel>
<div style={{
fontFamily: 'var(--font-family-display)',
fontSize: 'var(--font-size-xl)',
fontWeight: 'var(--font-weight-bold)',
letterSpacing: 'var(--tracking-tight)',
color: isOverdue ? 'var(--color-danger)' : isDueSoon ? 'var(--color-warning)' : 'var(--color-text-primary)',
marginTop: 'var(--space-1)',
}}>
{fmtDateLong(nextMaint.toISOString())}
</div>
</div>
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<SectionLabel>{isOverdue ? 'Overdue by' : 'In'}</SectionLabel>
<div style={{
fontSize: 'var(--font-size-lg)',
fontWeight: 'var(--font-weight-bold)',
color: isOverdue ? 'var(--color-danger)' : isDueSoon ? 'var(--color-warning)' : 'var(--color-text-secondary)',
marginTop: 'var(--space-1)',
}}>
{daysLeft != null ? `${Math.abs(daysLeft)} day${Math.abs(daysLeft) !== 1 ? 's' : ''}` : '—'}
</div>
</div>
</div>
) : (
<div style={{
...GLASS_ELEVATED,
padding: 'var(--space-4)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', fontStyle: 'italic' }}>
No maintenance schedule set
</span>
</div>
)}
{/* Progress bar */}
<ProgressBar
value={remaining ?? 0}
color={progressColor}
label="Progress to next service"
/>
{/* Detail rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
<DetailRow
label="Last Maintained"
value={maintainedOn ? fmtDateMedium(maintainedOn.toISOString()) : null}
/>
<Divider />
<DetailRow
label="Service Interval"
value={daysLabel(stats?.maintainancePeriod)}
/>
<Divider />
<DetailRow
label="Next Service"
value={nextMaint ? fmtDateMedium(nextMaint.toISOString()) : null}
valueColor={isOverdue ? 'var(--color-danger)' : isDueSoon ? 'var(--color-warning)' : undefined}
/>
</div>
</div>
)
}
// ─── Performance Metrics ──────────────────────────────────────────────────────
function MetricTile({ icon, label, value, accent = 'var(--color-primary)', tag, tagVariant = 'info' }) {
return (
<div style={{
...GLASS_ELEVATED,
padding: 'var(--space-5)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: 'var(--space-4)',
minHeight: 100,
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ color: accent, display: 'flex', alignItems: 'center' }}>
{icon}
</div>
{tag != null && (
<StatusBadge variant={tagVariant}>{tag}</StatusBadge>
)}
</div>
<div>
<div style={{
fontFamily: 'var(--font-family-display)',
fontSize: 'var(--font-size-2xl)',
fontWeight: 'var(--font-weight-bold)',
letterSpacing: 'var(--tracking-display)',
color: 'var(--color-text-primary)',
lineHeight: 1,
}}>
{value}
</div>
<div style={{
fontSize: 'var(--font-size-xs)',
color: 'var(--color-text-muted)',
fontWeight: 'var(--font-weight-medium)',
marginTop: 'var(--space-1)',
}}>
{label}
</div>
</div>
</div>
)
}
function PerformanceMetrics({ device, stats }) {
const totalWarnings = Number.isFinite(stats?.totalWarningsGiven) ? stats.totalWarningsGiven : null
const melodiesCount = Array.isArray(device?.device_melodies_all) ? device.device_melodies_all.length : null
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>
<span style={{
fontFamily: 'var(--font-family-display)',
fontSize: 'var(--font-size-md)',
fontWeight: 'var(--font-weight-semibold)',
letterSpacing: 'var(--tracking-tight)',
color: 'var(--color-text-primary)',
}}>
Device Performance
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 'var(--space-3)' }}>
{/* System Warnings */}
<MetricTile
accent={totalWarnings !== null && totalWarnings > 0 ? 'var(--color-danger)' : 'var(--color-success)'}
tag={totalWarnings !== null ? (totalWarnings === 0 ? 'Clean' : 'Flagged') : null}
tagVariant={totalWarnings === 0 ? 'success' : 'danger'}
value={totalWarnings !== null ? totalWarnings : '—'}
label="System Warnings"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
}
/>
{/* On-board Melodies */}
<MetricTile
accent="var(--color-info)"
tag={melodiesCount !== null ? 'Loaded' : null}
tagVariant="info"
value={melodiesCount !== null ? melodiesCount : '—'}
label="On-board Melodies"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>
}
/>
{/* Firmware Version */}
<MetricTile
accent="var(--color-primary)"
tag={device?.device_firmware_version ? 'Current' : null}
tagVariant="neutral"
value={device?.device_firmware_version ?? '—'}
label="Firmware Version"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="4" y="4" width="16" height="16" rx="2" />
<rect x="9" y="9" width="6" height="6" />
<path d="M15 2v2M9 2v2M15 20v2M9 20v2M20 9h2M20 14h2M2 9h2M2 14h2" />
</svg>
}
/>
{/* Total Melodies (favorites) */}
<MetricTile
accent="var(--color-warning)"
tag={Array.isArray(device?.device_melodies_favorites) && device.device_melodies_favorites.length > 0 ? 'Saved' : null}
tagVariant="warning"
value={Array.isArray(device?.device_melodies_favorites) ? device.device_melodies_favorites.length : '—'}
label="Favourite Melodies"
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" />
</svg>
}
/>
</div>
</div>
)
}
// ─── WarrantyTab ──────────────────────────────────────────────────────────────
export default function WarrantyTab({
device,
canEdit,
sub,
stats,
deviceUsers,
onEditSubscription,
onEditWarranty,
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)', width: '100%', maxWidth: 2000, margin: '0 auto' }}>
{/* Subscription hero — no section label above */}
<SubscriptionHero
sub={sub}
canEdit={canEdit}
onEditSubscription={onEditSubscription}
deviceUsers={deviceUsers}
/>
{/* Warranty card — no section label above */}
<WarrantyCard stats={stats} canEdit={canEdit} onEditWarranty={onEditWarranty} />
{/* Masonry: Maintenance + Metrics side by side */}
<div className="masonry-grid masonry-grid--2">
<MaintenanceCard stats={stats} />
<div style={{ ...GLASS, padding: 'var(--space-6)' }}>
<PerformanceMetrics device={device} stats={stats} />
</div>
</div>
</div>
)
}