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;
  }
}