App multiplataforma para gestión de visitas comerciales en campo — iOS, Android y PWA desde un único codebase TypeScript
Lunes 16 Jun · 4 pendientes
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonList, IonItem, IonLabel, IonBadge, IonSearchbar, IonRefresher, IonRefresherContent, IonFab, IonFabButton, IonIcon, IonSegment, IonSegmentButton, IonChip, IonSkeletonText } from "@ionic/react"; import { add, locationOutline, timeOutline } from "ionicons/icons"; import { useState, useEffect } from "react"; import { useVisitas } from "../hooks/useVisitas"; const VisitasHoy: React.FC = () => { const [filtro, setFiltro] = useState("todas"); const [busqueda, setBusqueda] = useState(""); const { visitas, loading, refetch } = useVisitas({ filtro, busqueda }); const handleRefresh = async (event: CustomEvent) => { await refetch(); event.detail.complete(); // Cierra el spinner del pull-to-refresh }; return ( <IonPage> <IonHeader> <IonToolbar> <IonTitle>📋 Visitas de hoy</IonTitle> </IonToolbar> <IonToolbar> <IonSearchbar value={busqueda} onIonInput={(e) => setBusqueda(e.detail.value ?? "")} placeholder="Buscar cliente o dirección..." /> </IonToolbar> <IonSegment value={filtro} onIonChange={(e) => setFiltro(e.detail.value)}> <IonSegmentButton value="todas">Todas</IonSegmentButton> <IonSegmentButton value="pendientes">Pendientes</IonSegmentButton> <IonSegmentButton value="completadas">Hechas</IonSegmentButton> </IonSegment> </IonHeader> <IonContent> <IonRefresher slot="fixed" onIonRefresh={handleRefresh}> <IonRefresherContent /> </IonRefresher> <IonList> {visitas.map((v) => ( <VisitaItem key={v.id} visita={v} /> ))} </IonList> <IonFab vertical="bottom" horizontal="end" slot="fixed"> <IonFabButton routerLink="/nueva-visita"> <IonIcon icon={add} /> </IonFabButton> </IonFab> </IonContent> </IonPage> ); };
import { Camera, CameraResultType } from "@capacitor/camera"; import { Geolocation } from "@capacitor/geolocation"; import { LocalNotifications } from "@capacitor/local-notifications"; // 📸 Foto del reporte post-visita export async function fotoVisita(): Promise<string> { const img = await Camera.getPhoto({ quality: 85, allowEditing: true, resultType: CameraResultType.Uri, }); return img.webPath!; } // 📍 Ubicación para ruta al cliente export async function miUbicacion() { const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true, }); return { lat: pos.coords.latitude, lng: pos.coords.longitude, }; } // 🔔 Recordatorio 30min antes de la visita export async function recordatorioVisita( cliente: string, fecha: Date ) { await LocalNotifications.schedule({ notifications: [{ title: `Visita en 30 min: ${cliente}`, body: "Toca para ver la ruta y los datos del cliente", id: Date.now(), schedule: { at: new Date(fecha.getTime() - 1800000) }, }], }); }
| Plugin | Propósito en Cultiva Field | iOS | Android | PWA |
|---|---|---|---|---|
| @capacitor/camera | Foto de evidencia en reporte post-visita | ✓ | ✓ | ✓ |
| @capacitor/geolocation | Calcular ruta al cliente + check-in automático | ✓ | ✓ | ✓ |
| @capacitor/local-notifications | Recordatorio 30 min antes de cada visita | ✓ | ✓ | Service Worker |
| @capacitor/haptics | Feedback táctil al confirmar visita completada | ✓ | ✓ | No disponible |
| @capacitor/preferences | Persistir sesión y preferencias offline | ✓ | ✓ | ✓ |
| @capacitor/network | Detectar conexión para activar modo offline/sync | ✓ | ✓ | ✓ |
/* Paleta CULTIVA IA — se aplica a todos los componentes Ionic automáticamente */ :root { --ion-color-primary: #4f46e5; --ion-color-primary-rgb: 79, 70, 229; --ion-color-primary-shade: #463ec9; --ion-color-primary-tint: #6158e8; --ion-color-secondary: #06b6d4; /* Cian acento */ --ion-color-success: #22c55e; /* Visita completada */ --ion-color-warning: #f59e0b; /* Visita urgente */ --ion-color-danger: #ef4444; /* Sin contactar */ --ion-font-family: 'Inter', system-ui, sans-serif; --ion-border-radius: 12px; } /* Dark mode — detectado automáticamente desde preferencia del sistema */ @media (prefers-color-scheme: dark) { :root { --ion-background-color: #0f172a; --ion-text-color: #e2e8f0; --ion-card-background: #1e293b; } }