Files
FarmaFinder/apps/frontend-mobile/app/(tabs)/alerts.tsx
T
Antoni Nuñez Romeu 9949b85001
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Backend Tests (push) Has been cancelled
Refactor frontend-mobile
2026-07-07 18:08:44 +02:00

285 lines
7.8 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../constants/theme';
import { LoadingSpinner } from '../../components/LoadingSpinner';
interface NotificationItem {
scope: string;
id: number;
medicine_name?: string;
medicine_nregistro?: string;
pharmacy_name?: string;
pharmacy_id?: number;
pharmacy_address?: string;
created_at?: string;
}
export default function AlertsScreen() {
const [items, setItems] = useState<NotificationItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
useEffect(() => {
loadNotifications();
}, []);
async function loadNotifications() {
setIsLoading(true);
setError(null);
try {
const res = await fetch('/api/notifications/mine', { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const merged = [
...(data.pharmacy || []),
...(data.global || []),
].sort((a: NotificationItem, b: NotificationItem) =>
(b.created_at || '').localeCompare(a.created_at || '')
);
setItems(merged);
} catch (err: any) {
setError(err.message || 'No se pudieron cargar las notificaciones');
} finally {
setIsLoading(false);
}
}
async function handleDelete(item: NotificationItem) {
const key = `${item.scope}:${item.id}`;
Alert.alert(
'Eliminar notificación',
`¿Eliminar la notificación de ${item.medicine_name || item.medicine_nregistro}?`,
[
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Eliminar',
style: 'destructive',
onPress: async () => {
setDeletingId(key);
try {
const res = await fetch('/api/notifications/mine', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ scope: item.scope, id: item.id }),
});
if (!res.ok && res.status !== 204) throw new Error(`HTTP ${res.status}`);
setItems(prev => prev.filter(i => !(i.scope === item.scope && i.id === item.id)));
} catch (err: any) {
Alert.alert('Error', err.message || 'No se pudo eliminar');
} finally {
setDeletingId(null);
}
},
},
]
);
}
function renderItem({ item }: { item: NotificationItem }) {
const key = `${item.scope}:${item.id}`;
return (
<View style={styles.item}>
<View style={styles.itemContent}>
<Text style={styles.itemName} numberOfLines={2}>
{item.medicine_name || item.medicine_nregistro}
</Text>
<View style={styles.itemMeta}>
<View style={styles.chip}>
<Ionicons
name={item.scope === 'pharmacy' ? 'medical' : 'globe'}
size={12}
color={colors.primary}
/>
<Text style={styles.chipText}>
{item.scope === 'pharmacy'
? item.pharmacy_name || `Farmacia #${item.pharmacy_id}`
: 'Cualquier farmacia'}
</Text>
</View>
{item.pharmacy_address && (
<Text style={styles.itemAddress} numberOfLines={1}>
{item.pharmacy_address}
</Text>
)}
</View>
</View>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleDelete(item)}
disabled={deletingId === key}
>
{deletingId === key ? (
<Ionicons name="hourglass" size={18} color={colors.danger} />
) : (
<Ionicons name="trash-outline" size={18} color={colors.danger} />
)}
</TouchableOpacity>
</View>
);
}
if (isLoading) {
return <LoadingSpinner message="Cargando notificaciones..." />;
}
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Notificaciones Guardadas</Text>
<Text style={styles.subtitle}>
Recibe avisos cuando medicamentos sin stock se repongan
</Text>
</View>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity onPress={loadNotifications} style={styles.retryButton}>
<Text style={styles.retryText}>Reintentar</Text>
</TouchableOpacity>
</View>
)}
{!error && items.length === 0 && (
<View style={styles.emptyContainer}>
<Ionicons name="notifications-off-outline" size={64} color={colors.border} />
<Text style={styles.emptyTitle}>Sin notificaciones</Text>
<Text style={styles.emptyText}>
Toca la campana en una farmacia sin stock para recibir notificaciones cuando se reponga.
</Text>
</View>
)}
{!error && items.length > 0 && (
<FlatList
data={items}
keyExtractor={(item) => `${item.scope}:${item.id}`}
renderItem={renderItem}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
header: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.md,
},
title: {
fontSize: 22,
fontWeight: 'bold',
color: colors.text,
},
subtitle: {
fontSize: 14,
color: colors.textSecondary,
marginTop: spacing.xs,
lineHeight: 20,
},
list: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
gap: spacing.sm,
},
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.card,
borderRadius: borderRadius.lg,
padding: spacing.md,
...shadows.card,
},
itemContent: {
flex: 1,
gap: spacing.xs,
},
itemName: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
lineHeight: 22,
},
itemMeta: {
gap: spacing.xs,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
backgroundColor: colors.primaryContainer,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.sm,
paddingVertical: 3,
alignSelf: 'flex-start',
},
chipText: {
fontSize: 12,
fontWeight: '600',
color: colors.onPrimaryContainer,
},
itemAddress: {
fontSize: 12,
color: colors.textSecondary,
},
deleteButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.dangerContainer,
alignItems: 'center',
justifyContent: 'center',
marginLeft: spacing.sm,
},
errorContainer: {
margin: spacing.lg,
padding: spacing.md,
backgroundColor: colors.dangerContainer,
borderRadius: borderRadius.lg,
alignItems: 'center',
gap: spacing.sm,
},
errorText: {
fontSize: 14,
color: colors.danger,
textAlign: 'center',
},
retryButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
},
retryText: {
fontSize: 14,
fontWeight: '600',
color: colors.primary,
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: spacing.xl,
gap: spacing.md,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text,
},
emptyText: {
fontSize: 14,
color: colors.textSecondary,
textAlign: 'center',
lineHeight: 20,
},
});