Frontend Hotfixes & Backend improvements #24

Merged
Ichitux merged 1 commits from react_app_fixes into main 2026-07-08 23:03:10 +00:00
8 changed files with 73 additions and 31 deletions
+1
View File
@@ -0,0 +1 @@
{"pid":864463,"startedAt":1783550197179}
+32 -2
View File
@@ -151,8 +151,9 @@ export async function getMedicineDetails(nregistro) {
return JSON.parse(cachedData); return JSON.parse(cachedData);
} }
// Consultar la API de CIMA // Intentar obtener de la API de CIMA (endpoint individual puede no existir)
console.log(`🌐 Fetching medicine details from CIMA: ${nregistro}`); console.log(`🌐 Fetching medicine details from CIMA: ${nregistro}`);
try {
const response = await axios.get(`${CIMA_API_BASE_URL}/medicamento/${nregistro}`, { const response = await axios.get(`${CIMA_API_BASE_URL}/medicamento/${nregistro}`, {
timeout: 5000 timeout: 5000
}); });
@@ -176,9 +177,38 @@ export async function getMedicineDetails(nregistro) {
presentations: med.presentaciones || [] presentations: med.presentaciones || []
}; };
// Guardar en caché (TTL más largo para detalles específicos)
await redisClient.setEx(cacheKey, CACHE_TTL * 24, JSON.stringify(medicineDetails)); await redisClient.setEx(cacheKey, CACHE_TTL * 24, JSON.stringify(medicineDetails));
return medicineDetails;
}
} catch (apiError) {
console.log(`⚠️ Individual medicine endpoint failed, trying search fallback`);
}
// Fallback: buscar por nregistro usando el endpoint de búsqueda
const searchResponse = await axios.get(`${CIMA_API_BASE_URL}/medicamentos`, {
params: { nregistro },
timeout: 5000
});
if (searchResponse.data?.resultados?.length > 0) {
const med = searchResponse.data.resultados[0];
const medicineDetails = {
id: med.nregistro,
nregistro: med.nregistro,
name: med.nombre,
active_ingredient: med.vtm?.nombre || null,
dosage: med.dosis || null,
form: med.formaFarmaceutica?.nombre || null,
formSimplified: med.formaFarmaceuticaSimplificada?.nombre || null,
laboratory: med.labtitular,
prescription: med.cpresc,
commercialized: med.comerc,
generic: med.generico,
photos: med.fotos || [],
docs: med.docs || []
};
await redisClient.setEx(cacheKey, CACHE_TTL * 24, JSON.stringify(medicineDetails));
return medicineDetails; return medicineDetails;
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import MapView, { Marker } from 'react-native-maps';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { getPharmacies } from '../../services/pharmacies'; import { getPharmacies } from '../../services/pharmacies';
import { LoadingSpinner } from '../../components/LoadingSpinner'; import { LoadingSpinner } from '../../components/LoadingSpinner';
import { colors, spacing } from '../../constants/theme'; import { colors, spacing, borderRadius } from '../../constants/theme';
import { Pharmacy } from '../../types'; import { Pharmacy } from '../../types';
export default function MapScreen() { export default function MapScreen() {
+2 -2
View File
@@ -97,10 +97,10 @@ const bgStyles = StyleSheet.create({
flex: 1, flex: 1,
}, },
backgroundImage: { backgroundImage: {
opacity: 0.5, opacity: 0.8,
}, },
overlay: { overlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255, 252, 245, 0.48)', backgroundColor: 'rgba(255, 252, 245, 0.2)',
}, },
}); });
+7 -6
View File
@@ -56,17 +56,18 @@ export default function MedicineDetailScreen() {
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.container}>
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.name}>{medicine.nombre}</Text> <Text style={styles.name}>{medicine.name}</Text>
<StockBadge stock={medicine.stock} /> {medicine.stock != null && <StockBadge stock={medicine.stock} />}
</View> </View>
<View style={styles.infoSection}> <View style={styles.infoSection}>
<InfoRow label="Principio activo" value={medicine.principioActivo} /> <InfoRow label="Principio activo" value={medicine.active_ingredient} />
<InfoRow label="Laboratorio" value={medicine.laboratorio} /> <InfoRow label="Laboratorio" value={medicine.laboratory} />
<InfoRow label="Forma farmacéutica" value={medicine.formaFarmaceutica} /> <InfoRow label="Forma farmacéutica" value={medicine.form} />
<InfoRow label="Dosificación" value={medicine.dosage} />
<InfoRow <InfoRow
label="Precio" label="Precio"
value={medicine.precio ? `${medicine.precio.toFixed(2)}` : 'No disponible'} value={medicine.precio != null ? `${medicine.precio.toFixed(2)}` : 'No disponible'}
/> />
<InfoRow label="Registro" value={medicine.nregistro} /> <InfoRow label="Registro" value={medicine.nregistro} />
</View> </View>
@@ -29,27 +29,27 @@ export function MedicineCard({ medicine }: MedicineCardProps) {
> >
<View style={styles.header}> <View style={styles.header}>
<Text style={[styles.name, isTablet && styles.nameTablet]} numberOfLines={2}> <Text style={[styles.name, isTablet && styles.nameTablet]} numberOfLines={2}>
{medicine.nombre} {medicine.name}
</Text> </Text>
<StockBadge stock={medicine.stock} /> {medicine.stock != null && <StockBadge stock={medicine.stock} />}
</View> </View>
<Text style={styles.principioActivo} numberOfLines={1}> <Text style={styles.principioActivo} numberOfLines={1}>
{medicine.principioActivo} {medicine.active_ingredient}{medicine.dosage ? ` - ${medicine.dosage}` : ''}
</Text> </Text>
<View style={styles.footer}> <View style={styles.footer}>
<View style={styles.priceContainer}> <View style={styles.priceContainer}>
<Ionicons name="pricetag" size={14} color={colors.textSecondary} /> <Ionicons name="pricetag" size={14} color={colors.textSecondary} />
<Text style={[styles.price, isTablet && styles.priceTablet]}> <Text style={[styles.price, isTablet && styles.priceTablet]}>
{medicine.precio ? `${medicine.precio.toFixed(2)}` : 'Sin precio'} {medicine.precio != null ? `${medicine.precio.toFixed(2)}` : 'Sin precio'}
</Text> </Text>
</View> </View>
<View style={styles.labContainer}> <View style={styles.labContainer}>
<Ionicons name="business" size={14} color={colors.textSecondary} /> <Ionicons name="business" size={14} color={colors.textSecondary} />
<Text style={[styles.laboratorio, isTablet && styles.laboratorioTablet]} numberOfLines={1}> <Text style={[styles.laboratorio, isTablet && styles.laboratorioTablet]} numberOfLines={1}>
{medicine.laboratorio} {medicine.laboratory}
</Text> </Text>
</View> </View>
</View> </View>
+3 -1
View File
@@ -11,8 +11,10 @@ const ENV = {
const environment = Constants.expoConfig?.extra?.environment || (__DEV__ ? 'development' : 'production'); const environment = Constants.expoConfig?.extra?.environment || (__DEV__ ? 'development' : 'production');
const envBaseUrl = process.env.EXPO_PUBLIC_API_URL;
export const config = { export const config = {
API_BASE_URL: ENV[environment as keyof typeof ENV]?.API_BASE_URL || ENV.development.API_BASE_URL, API_BASE_URL: envBaseUrl || ENV[environment as keyof typeof ENV]?.API_BASE_URL || ENV.development.API_BASE_URL,
SEARCH_DEBOUNCE_MS: 300, SEARCH_DEBOUNCE_MS: 300,
CACHE_STALE_TIME: 5 * 60 * 1000, // 5 minutes CACHE_STALE_TIME: 5 * 60 * 1000, // 5 minutes
CACHE_CACHE_TIME: 30 * 60 * 1000, // 30 minutes CACHE_CACHE_TIME: 30 * 60 * 1000, // 30 minutes
+15 -7
View File
@@ -1,12 +1,20 @@
export interface Medicine { export interface Medicine {
id: string;
nregistro: string; nregistro: string;
nombre: string; name: string;
principioActivo: string; active_ingredient: string;
laboratorio: string; dosage: string;
formaFarmaceutica: string; form: string;
precio: number | null; formSimplified: string;
stock: number; laboratory: string;
disponibilidad: 'disponible' | 'sin_stock' | 'bajo_stock'; prescription: string;
commercialized: boolean;
generic: boolean;
photos: string[];
docs: { tipo: number; url: string; urlHtml?: string; secc: boolean; fecha: number }[];
// Optional pharmacy-specific fields (only present in pharmacy_medicines joins)
precio?: number | null;
stock?: number;
} }
export interface Pharmacy { export interface Pharmacy {