Ionic 7 React 18 Capacitor 5

Cultiva Field

App multiplataforma para gestión de visitas comerciales en campo — iOS, Android y PWA desde un único codebase TypeScript

CULTIVA IA — Agencia de Automatización
Stack: Ionic + React + Capacitor + Supabase
Modo offline + Sync automático
Vista previa & Arquitectura
9:41 ▲▲▲ 🔋

📋 Visitas de hoy

Lunes 16 Jun · 4 pendientes

Panadería Ortega SL
📍 C/ Gran Vía 12, Madrid
✓ 9:30
Tech Soluciones 360
📍 Av. Diagonal 540, BCN
⚡ 12:00
Clínica Dental Mira
📍 C/ Serrano 89, Madrid
15:30
Restaurante La Huerta
📍 C/ Mayor 5, Toledo
17:00
📋 Visitas
👥 Clientes
📊 Métricas
⚙️ Config
Arquitectura de capas
UI / Vista
IonPage IonList IonCard IonFab IonSearchbar
↕ React Hooks + Zustand store
Lógica / Servicios
useVisitas useClientes SyncService AuthService
↕ Capacitor plugin bridge
APIs Nativas
Camera Geolocation Notifications Haptics
↕ WebView ↔ Native Bridge
Plataformas
iOS (Swift) Android (Kotlin) PWA Electron
Componente principal — Home.tsx
src/pages/VisitasHoy.tsx
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>
  );
};
Servicios nativos con Capacitor
src/services/native.ts
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) },
    }],
  });
}
Setup del proyecto
1
Instalar Ionic CLI globalmente
npm install -g @ionic/cli
2
Crear el proyecto con template "tabs"
ionic start cultiva-field tabs --type=react
3
Añadir Capacitor y plugins nativos
npm install @capacitor/camera @capacitor/geolocation @capacitor/local-notifications
4
Desplegar en plataformas nativas
ionic cap add ios && ionic cap add android
5
Build + sync + abrir en Xcode/Android Studio
ionic cap sync && ionic cap open ios
6
Live reload en dispositivo físico
ionic cap run ios --livereload --external
Plugins Capacitor configurados
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 ✓✓✓
Funcionalidades clave implementadas
📴
Modo Offline + Sync
Los reportes se guardan localmente con IndexedDB. Al recuperar conexión, SyncService los sube a Supabase de forma transparente.
🗺️
Geolocalización
Check-in automático al llegar al cliente. Cálculo de ruta con Mapbox. Historial de posiciones del comercial durante el día.
📸
Reporte con Foto
El comercial toma foto de la visita, añade notas de voz o texto y firma del cliente. Todo queda enlazado al CRM de Supabase.
🔔
Notificaciones Push
Recordatorios automáticos 30 min antes de cada visita. Alertas cuando el manager asigna una visita urgente al comercial.
🎨
UI Adaptativa
Los componentes Ionic se adaptan automáticamente al estilo iOS o Material Design de Android. Un único código, dos looks nativos.
⚡
PWA de fallback
Si el comercial no puede instalar la app nativa, accede desde el navegador con la misma UX. Service Worker garantiza carga offline.
Distribución en plataformas
🍎
iOS
App Store
✓ Xcode build
🤖
Android
Google Play
✓ Android Studio
🌐
PWA
Web / Escritorio
✓ Por defecto
🖥️
Electron
Desktop nativo
⚡ Fase 2
Theming — Variables CSS personalizadas
src/theme/variables.css
/* 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;
  }
}