Sergei ec63a2c1e2 Add admin panel, optimized API, OTP auth, migrations
Admin Panel (Next.js):
- Dashboard with stats
- Users list with relationships (watches/watched_by)
- User detail pages
- Deployments list and detail pages
- Devices, Orders, Subscriptions pages
- OTP-based admin authentication

Backend Optimizations:
- Fixed N+1 query problem in admin APIs
- Added pagination support
- Added .range() and count support to Supabase wrapper
- Optimized batch queries with lookup maps

Database:
- Added migrations for schema evolution
- New tables: push_tokens, notification_settings
- Updated access model

iOS Build Scripts:
- build-ios.sh, clear-apple-cache.sh
- EAS configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-20 11:05:39 -08:00

164 lines
3.9 KiB
JavaScript

'use client';
import { useEffect, useState } from 'react';
import AdminLayout from '../../components/AdminLayout';
import { getBeneficiaries } from '../../lib/api';
export default function BeneficiariesPage() {
const [beneficiaries, setBeneficiaries] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadBeneficiaries();
}, []);
const loadBeneficiaries = async () => {
try {
const data = await getBeneficiaries();
setBeneficiaries(data.beneficiaries || []);
} catch (err) {
console.error('Failed to load beneficiaries:', err);
} finally {
setLoading(false);
}
};
return (
<AdminLayout>
<h1 style={styles.title}>Beneficiaries</h1>
<p style={styles.subtitle}>Users being monitored (elderly relatives)</p>
{loading ? (
<p style={styles.loading}>Loading...</p>
) : beneficiaries.length === 0 ? (
<div style={styles.empty}>
<p>No beneficiaries yet</p>
<p style={styles.emptyHint}>
Beneficiaries appear when caretakers add someone to monitor
</p>
</div>
) : (
<div style={styles.grid}>
{beneficiaries.map((ben) => (
<div key={ben.id} style={styles.card}>
<div style={styles.avatar}>
{getInitials(ben.first_name, ben.last_name)}
</div>
<div style={styles.cardContent}>
<h3 style={styles.name}>
{ben.first_name || ben.last_name
? `${ben.first_name || ''} ${ben.last_name || ''}`.trim()
: 'Unknown'}
</h3>
<p style={styles.email}>{ben.email}</p>
{(ben.address_city || ben.address_country) && (
<p style={styles.location}>
{[ben.address_city, ben.address_country].filter(Boolean).join(', ')}
</p>
)}
{ben.phone && (
<p style={styles.phone}>{ben.phone}</p>
)}
</div>
</div>
))}
</div>
)}
<div style={styles.stats}>
<span>Total: {beneficiaries.length} beneficiaries</span>
</div>
</AdminLayout>
);
}
function getInitials(first, last) {
const f = first?.[0] || '';
const l = last?.[0] || '';
return (f + l).toUpperCase() || '?';
}
const styles = {
title: {
fontSize: '24px',
fontWeight: '600',
marginBottom: '4px',
},
subtitle: {
fontSize: '14px',
color: 'var(--text-muted)',
marginBottom: '24px',
},
loading: {
color: 'var(--text-muted)',
},
empty: {
background: 'white',
padding: '48px',
borderRadius: '12px',
textAlign: 'center',
},
emptyHint: {
fontSize: '14px',
color: 'var(--text-muted)',
marginTop: '8px',
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '16px',
},
card: {
background: 'white',
borderRadius: '12px',
padding: '20px',
display: 'flex',
gap: '16px',
},
avatar: {
width: '48px',
height: '48px',
borderRadius: '50%',
background: 'var(--surface)',
color: 'var(--primary)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '16px',
fontWeight: '600',
flexShrink: 0,
},
cardContent: {
flex: 1,
minWidth: 0,
},
name: {
fontSize: '16px',
fontWeight: '600',
marginBottom: '4px',
},
email: {
fontSize: '13px',
color: 'var(--text-secondary)',
marginBottom: '8px',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
location: {
fontSize: '13px',
color: 'var(--text-muted)',
},
phone: {
fontSize: '13px',
color: 'var(--text-muted)',
marginTop: '4px',
},
stats: {
marginTop: '24px',
fontSize: '13px',
color: 'var(--text-muted)',
},
};