// odontox-app/components/notifications/NotificationsList.tsx
import { useEffect, useState } from 'react';
import {
View, Text, FlatList, RefreshControl, SafeAreaView,
Pressable, StyleSheet, ActivityIndicator,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Colors, Typography, Spacing } from '@/constants/theme';
import { papi } from '@/lib/api';
interface AppNotification {
id: string;
type: string;
title: string;
message: string;
priority: 'high' | 'medium' | 'low';
isRead: boolean;
actionUrl: string | null;
createdAt: string;
entityType: string | null;
entityId: string | null;
}
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
return `${Math.floor(hrs / 24)}d ago`;
}
const TYPE_ICON: Record<string, string> = {
appointment: 'calendar-outline',
payment: 'card-outline',
patient: 'person-outline',
system: 'information-circle-outline',
reminder: 'alarm-outline',
referral: 'git-branch-outline',
};
export function NotificationsList() {
const [items, setItems] = useState<AppNotification[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
async function load(isRefresh = false) {
if (isRefresh) setRefreshing(true); else setLoading(true);
try {
const res = await papi.get<{ data: AppNotification[] }>('/notifications?limit=50');
setItems(res.data ?? []);
} catch {}
finally { setLoading(false); setRefreshing(false); }
}
async function markRead(id: string) {
try {
await papi.patch(`/notifications/${id}/read`, {});
setItems(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n));
} catch {}
}
useEffect(() => { load(); }, []);
if (loading) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator color={Colors.brand} />
</View>
);
}
return (
<SafeAreaView style={{ flex: 1, backgroundColor: Colors.background }}>
<View style={s.header}>
<Text style={s.title}>Notifications</Text>
</View>
<FlatList
data={items}
keyExtractor={i => i.id}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={() => load(true)} tintColor={Colors.brand} />
}
contentContainerStyle={{ padding: Spacing.lg, gap: Spacing.sm, paddingBottom: 40 }}
ListEmptyComponent={
<View style={{ alignItems: 'center', paddingVertical: Spacing['2xl'] }}>
<Ionicons name="notifications-off-outline" size={40} color={Colors.textMuted} />
<Text style={{ color: Colors.textMuted, marginTop: Spacing.md }}>No notifications yet</Text>
</View>
}
renderItem={({ item }) => (
<Pressable
onPress={() => markRead(item.id)}
style={({ pressed }) => [s.row, !item.isRead && s.rowUnread, pressed && { opacity: 0.7 }]}
>
<View style={[s.iconWrap, { backgroundColor: item.priority === 'high' ? '#FEE2E2' : Colors.brandSurface }]}>
<Ionicons
name={(TYPE_ICON[item.type] ?? 'notifications-outline') as any}
size={18}
color={item.priority === 'high' ? Colors.error : Colors.brand}
/>
</View>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Text style={[s.notifTitle, !item.isRead && s.notifTitleUnread]} numberOfLines={1}>
{item.title}
</Text>
<Text style={s.time}>{timeAgo(item.createdAt)}</Text>
</View>
<Text style={s.message} numberOfLines={2}>{item.message}</Text>
</View>
{!item.isRead && <View style={s.dot} />}
</Pressable>
)}
/>
</SafeAreaView>
);
}
const s = StyleSheet.create({
header: {
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.md,
paddingBottom: Spacing.sm,
},
title: { fontSize: Typography.xl, fontWeight: '700', color: Colors.text },
row: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: Spacing.md,
backgroundColor: Colors.surface,
borderRadius: 12,
padding: Spacing.md,
borderWidth: 1,
borderColor: Colors.border,
},
rowUnread: { borderColor: Colors.brand + '40', backgroundColor: Colors.brandSurface + '30' },
iconWrap: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
notifTitle: { fontSize: Typography.sm, fontWeight: '500', color: Colors.textSecondary, flex: 1, marginRight: 8 },
notifTitleUnread: { fontWeight: '700', color: Colors.text },
message: { fontSize: Typography.xs, color: Colors.textMuted, marginTop: 2, lineHeight: 16 },
time: { fontSize: Typography.xs, color: Colors.textMuted, flexShrink: 0 },
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: Colors.brand,
alignSelf: 'center',
flexShrink: 0,
},
});