Files
bellsystems-cp/frontend/src/pages/bellcloud/devices/tabs/WarrantyTab.jsx
T

858 lines
33 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
)
}