const {useState,useEffect}=React; function App(){ const[sesion,setSesion]=useState(undefined); const[datos,setDatos]=useState(undefined); const[error,setError]=useState(""); useEffect(()=>{ sb.auth.getSession().then(({data})=>setSesion(data.session)); const{data:s}=sb.auth.onAuthStateChange((_e,x)=>setSesion(x)); return()=>s.subscription.unsubscribe(); },[]); async function cargar(){ const{data,error}=await sb.rpc("miauto_inicio"); if(error){setError(mensajeError(error));setDatos(null);return;} setError("");setDatos(data); } useEffect(()=>{if(sesion)cargar();else setDatos(undefined);},[sesion&&sesion.user.id]); if(sesion===undefined)return

Cargando…

; if(!sesion)return ; if(datos===undefined)return

Cargando tu auto…

; if(datos===null)return
{error}
; if(!datos.autos.length)return ; return ; } function Acceso(){ const[modo,setModo]=useState("registro"); const[correo,setCorreo]=useState("");const[clave,setClave]=useState(""); const[error,setError]=useState("");const[ocupado,setOcupado]=useState(false); async function enviar(e){ e.preventDefault();setError(""); if(modo==="registro"&&clave.length<10){setError("La contraseña debe tener al menos 10 caracteres.");return;} setOcupado(true); const r=modo==="entrar"?await sb.auth.signInWithPassword({email:correo.trim(),password:clave}):await sb.auth.signUp({email:correo.trim(),password:clave}); setOcupado(false); if(r.error)setError(mensajeError(r.error)); } return(

Upco Mi Auto

Tu auto al día, gratis: Hoy No Circula, verificación, seguro, servicio y tus papeles en el celular.

setCorreo(e.target.value)}/>
setClave(e.target.value)}/>
{error&&
{error}
}
); } function Segmento({opciones,valor,onCambio,etiqueta}){ return
{opciones.map(o=>)}
; } function useAseguradoras(){ const[l,setL]=useState([]); useEffect(()=>{sb.from("aseguradoras").select("id,nombre").order("nombre").then(({data})=>setL(data||[]));},[]); return l; } function Alta({onListo,primera,onCancelar}){ const aseg=useAseguradoras(); const[f,setF]=useState({placas:"",estado:"",holograma:null,km:"",marca:"",modelo:"",anio:"",aseguradora:"",vence:""}); const[error,setError]=useState("");const[ocupado,setOcupado]=useState(false); const c=k=>e=>setF({...f,[k]:e.target.value}); const dig=soloDigitos(f.placas).slice(-1); const engomado={"5":"amarillo","6":"amarillo","7":"rosa","8":"rosa","3":"rojo","4":"rojo","1":"verde","2":"verde","9":"azul","0":"azul"}[dig]; async function guardar(e){ e.preventDefault();setError(""); const anio=f.anio?parseInt(f.anio,10):null; if(anio&&(anio<1960||anio>new Date().getFullYear()+1)){setError("Revisa el año.");return;} setOcupado(true); const{data,error}=await sb.rpc("miauto_crear",{p_placas:f.placas,p_estado:f.estado||null,p_holograma:f.holograma,p_km:f.km?parseInt(soloDigitos(f.km),10):null, p_marca:f.marca,p_modelo:f.modelo,p_anio:anio,p_aseguradora:f.aseguradora||null,p_seguro_vence:f.vence||null,p_nombre:null}); setOcupado(false); if(error||!data.ok){setError(error?mensajeError(error):data.error);return;} onListo(); } return(
{onCancelar&&} Upco Mi Auto

{primera?"Agrega tu auto en un minuto":"Agregar otro auto"}

Con tus placas calculamos solos tu verificación y tu Hoy No Circula. Lo que no sepas, déjalo vacío.
{engomado&&Terminación {dig} · engomado {engomado}}
{const d=soloDigitos(e.target.value).slice(0,7);setF({...f,km:d?km(d):""});}} placeholder="61,240"/>
Holograma de tu verificación setF({...f,holograma:v})} opciones={[{v:"00",t:"00"},{v:"0",t:"0"},{v:"1",t:"1"},{v:"2",t:"2"},{v:"exento",t:"Exento"}]}/> Viene en la calcomanía del vidrio. Exento: eléctricos e híbridos.
setF({...f,anio:soloDigitos(e.target.value).slice(0,4)})} placeholder="2019"/>
{f.aseguradora&&
} {error&&
{error}
}
); } const ICONOS={ inicio:, guantera:<>, km:<>, asistencia:<> }; function Icono({n}){return ;} function Principal({datos,recargar,correo}){ const[vista,setVista]=useState("inicio"); const[idx,setIdx]=useState(0); const[hoja,setHoja]=useState(null); // {tipo:'servicio'|'verificacion'|'ajustes'|'alta'} const a=datos.autos[Math.min(idx,datos.autos.length-1)]; const recargarY=()=>{setHoja(null);recargar();}; if(hoja&&hoja.tipo==="alta")return {setIdx(datos.autos.length);recargarY();}} onCancelar={()=>setHoja(null)}/>; if(hoja&&hoja.tipo==="ajustes")return setHoja({tipo:"alta"})} onCerrar={()=>setHoja(null)} onGuardado={recargarY}/>; return(
Upco Mi Auto
{datos.autos.length>1&&({v:i,t:x.placas||x.nombre}))}/>}

{nombreAuto(a)}

{a.placas&&{a.placas}} {a.km!=null?km(a.km)+" km · "+(a.km_fecha?hace(a.km_fecha):""):"Sin kilometraje"}
{vista==="inicio"&&} {vista==="km"&&{recargar();setVista("inicio");}} onServicio={()=>setHoja({tipo:"servicio"})}/>} {vista==="guantera"&&} {vista==="asistencia"&&} {hoja&&hoja.tipo==="servicio"&&setHoja(null)} onGuardado={recargarY}/>} {hoja&&hoja.tipo==="verificacion"&&setHoja(null)} onGuardado={recargarY}/>}
); } function Inicio({a,onIr,onHoja}){ const seg=estadoSeguro(a),ver=estadoVerificacion(a.verificacion),ser=estadoServicio(a.mantenimiento); const refrendo=(a.documentos||[]).find(d=>d.tipo==="refrendo"); const kmViejo=!a.km_fecha||-diasHasta(a.km_fecha)>=30; return(
{a.circula_zmvm&&a.holograma&&(a.hoy_no_circula ?
Hoy no circulasEn CDMX y zona metropolitana, de 5:00 a 22:00.
:
Hoy sí circulas{a.manana_no_circula?"Mañana no circulas: planea tu día.":"Mañana también."}
)} {a.circula_zmvm&&!a.holograma&&} {kmViejo&&} Lo que viene
Seguro{a.seguro&&a.seguro.aseguradora?" · "+a.seguro.aseguradora:""}
{seg.d}
{seg.t}
{seg.c==="off"&&}
Servicio
{ser.d}
{ser.t}
{(ser.c==="off"||ser.c==="bad"||ser.c==="warn")&&}
Verificación
{ver.d}
{ver.t}
{(ver.c==="warn"||ver.c==="bad")&&} {ver.c==="off"&&a.verificacion&&a.verificacion.motivo==="sin_estado"&&}
{refrendo&&refrendo.fecha_vencimiento ?(()=>{const n=diasHasta(refrendo.fecha_vencimiento);return
Refrendo / tenencia
{n<0?"Venció el ":"Pagar antes del "}{fechaLarga(refrendo.fecha_vencimiento)}
{n<0?"Vencido":n<=30?n+" días":"Al día"}
;})() :
Refrendo / tenencia
Guarda tu comprobante y su fecha en la guantera
}
); } function Kilometraje({a,onGuardado,onServicio}){ const[v,setV]=useState("");const[error,setError]=useState("");const[ocupado,setOcupado]=useState(false); const m=a.mantenimiento||{}; async function guardar(e){ e.preventDefault();setError(""); const n=parseInt(soloDigitos(v),10); if(isNaN(n)){setError("Escribe lo que marca el tablero.");return;} if(a.km!=null&&n-a.km>20000&&!window.__confirmadoKm){window.__confirmadoKm=true;setError("Son "+km(n-a.km)+" km más que la vez pasada. Si es correcto, toca Guardar otra vez.");return;} setOcupado(true); const{data,error}=await sb.rpc("miauto_registrar_km",{p_vehiculo:a.id,p_km:n}); setOcupado(false);window.__confirmadoKm=false; if(error||!data.ok){setError(error?mensajeError(error):data.error);return;} onGuardado(); } return(

¿Cuántos km marca hoy?

Con una lectura al mes calculamos cuánto usas tu auto y cuándo te toca servicio.

{const d=soloDigitos(e.target.value).slice(0,7);setV(d?km(d):"");}}/> {a.km!=null&&La vez pasada: {km(a.km)} km, {a.km_fecha?hace(a.km_fecha):""}}
{error&&
{error}
}
Con tus lecturas
Usas tu auto{m.km_dia?m.km_dia+" km al día":"Falta otra lectura"}
Próximo servicio{m.proximo_km?km(m.proximo_km)+" km":"—"}
Te toca aprox.{m.fecha_estimada?fechaLarga(m.fecha_estimada):"—"}
); } function Asistencia({a}){ const s=a.seguro; const digitos=t=>String(t||"").replace(/[^\d*#+]/g,""); return(

¿Qué pasó?

Emergencia: llamar al 911911 {s?
{s.aseguradora}{s.poliza?" · póliza "+s.poliza:""} {s.tel_siniestros&&Choque o robo: aseguradora{s.tel_siniestros}} {s.tel_siniestros_cdmx&&Desde CDMX{s.tel_siniestros_cdmx}} Grúa, batería, ponchadura{s.tel_asistencia_vial||s.tel_siniestros} {s.marcacion_corta&&Desde el celular también: {s.marcacion_corta}} {s.indicacion&&Al llamar elige la {s.indicacion}.} {s.nota&&{s.nota}}
:
Registra tu aseguradora en Ajustes y aquí tendrás su teléfono de cabina y de asistencia vial a un toque.
}
Si chocaste
  • Si hay lesionados, llama primero al 911.
  • No muevas el auto hasta que te lo indique el ajustador o la autoridad.
  • No firmes ni aceptes acuerdos con nadie. Espera al ajustador.
  • Toma fotos de los daños, las placas del otro auto y el lugar.

Teléfonos tomados del sitio oficial de cada aseguradora. Sin señal, las llamadas funcionan igual.

); } const DOCS=[{tipo:"poliza",t:"Póliza",vence:true},{tipo:"tarjeta_circulacion",t:"Tarjeta de circulación",vence:"opcional"},{tipo:"verificacion",t:"Verificación",vence:false},{tipo:"refrendo",t:"Refrendo / tenencia",vence:true},{tipo:"factura",t:"Factura",vence:false}]; function Guantera({a,flotaId,onCambio}){ const[abierto,setAbierto]=useState(null); const[miniaturas,setMiniaturas]=useState({}); useEffect(()=>{(async()=>{ const m={}; for(const d of a.documentos||[]){if(d.archivo_path&&/\.(jpe?g|png|webp)$/i.test(d.archivo_path)){const{data}=await sb.storage.from(d.archivo_bucket||"fleet-documentos").createSignedUrl(d.archivo_path,600);if(data)m[d.tipo]=data.signedUrl;}} setMiniaturas(m); })();},[a.id,(a.documentos||[]).length]); return(

Guantera digital

Una foto de cada papel y ya. Si te los piden, los tienes en el celular.

{DOCS.map(cat=>{ const d=(a.documentos||[]).find(x=>x.tipo===cat.tipo); const fecha=cat.tipo==="poliza"?a.seguro_vence:d&&d.fecha_vencimiento; const det=fecha?(diasHasta(fecha)<0?"Venció "+fechaCorta(fecha):"Hasta "+fechaCorta(fecha)):d&&d.archivo_path?"Guardado":"Agregar foto"; return ; })}
{abierto&&x.tipo===abierto.tipo)} onCerrar={()=>setAbierto(null)} onGuardado={()=>{setAbierto(null);onCambio();}}/>}
); } function Hoja({titulo,onCerrar,children}){ useEffect(()=>{const f=e=>{if(e.key==="Escape")onCerrar();};window.addEventListener("keydown",f);return()=>window.removeEventListener("keydown",f);},[]); return
{if(e.target===e.currentTarget)onCerrar();}}>

{titulo}

{children}
; } function HojaDocumento({a,flotaId,cat,doc,onCerrar,onGuardado}){ const[archivo,setArchivo]=useState(null); const[fecha,setFecha]=useState(cat.tipo==="poliza"?(a.seguro_vence||""):(doc&&(cat.tipo==="verificacion"?doc.fecha_documento:doc.fecha_vencimiento))||""); const[error,setError]=useState("");const[ocupado,setOcupado]=useState(false); async function ver(){ const{data,error}=await sb.storage.from(doc.archivo_bucket||"fleet-documentos").createSignedUrl(doc.archivo_path,300); if(error){setError(mensajeError(error));return;} window.open(data.signedUrl,"_blank","noopener"); } async function guardar(){ setError("");setOcupado(true); try{ let ruta=doc?doc.archivo_path:null,bucket=doc?doc.archivo_bucket:"fleet-documentos"; if(archivo){ const b=await comprimir(archivo); const ext=b.type==="application/pdf"?"pdf":"jpg"; const nueva=flotaId+"/"+crypto.randomUUID()+"."+ext; const{error:eu}=await sb.storage.from("fleet-documentos").upload(nueva,b,{upsert:false,contentType:b.type||"image/jpeg"}); if(eu)throw eu; if(ruta&&bucket==="fleet-documentos")await sb.storage.from("fleet-documentos").remove([ruta]); ruta=nueva;bucket="fleet-documentos"; } const fila={flota_id:flotaId,vehiculo_id:a.id,tipo:cat.tipo,archivo_path:ruta,archivo_bucket:bucket||"fleet-documentos", fecha_vencimiento:cat.tipo==="verificacion"?null:(fecha||null),fecha_documento:cat.tipo==="verificacion"?(fecha||null):null,updated_at:new Date().toISOString()}; const r=doc?await sb.from("documentos_flota").update(fila).eq("id",doc.id):await sb.from("documentos_flota").insert(fila); if(r.error)throw r.error; if(cat.tipo==="poliza")await sb.from("vehiculos").update({seguro_vence:fecha||null}).eq("id",a.id); onGuardado(); }catch(e){setError(mensajeError(e));} finally{setOcupado(false);} } return {doc&&doc.archivo_path&&}
setArchivo(e.target.files[0]||null)} style={{height:"auto",padding:10}}/>
{cat.vence!==false&&
setFecha(e.target.value)}/>
} {cat.tipo==="verificacion"&&
setFecha(e.target.value)}/>
} {error&&
{error}
}
; } function HojaVerificacion({a,flotaId,onCerrar,onGuardado}){ const[holo,setHolo]=useState(a.holograma&&a.holograma!=="exento"?a.holograma:null); const[error,setError]=useState("");const[ocupado,setOcupado]=useState(false); async function guardar(){ setOcupado(true);setError(""); const prev=(a.documentos||[]).find(d=>d.tipo==="verificacion"); const fila={flota_id:flotaId,vehiculo_id:a.id,tipo:"verificacion",fecha_documento:hoyMX(),fecha_vencimiento:null,updated_at:new Date().toISOString(),notas:holo?"Holograma "+holo:null}; const r=prev?await sb.from("documentos_flota").update(fila).eq("id",prev.id):await sb.from("documentos_flota").insert(fila); if(!r.error&&holo)await sb.from("vehiculos").update({holograma:holo}).eq("id",a.id); setOcupado(false); if(r.error){setError(mensajeError(r.error));return;} onGuardado(); } return

No necesitas el certificado digital. Dinos qué holograma te dieron y listo: calculamos sola tu siguiente verificación.

Si quieres, después guarda una foto del certificado en tu guantera. {error&&
{error}
}
; } function HojaServicio({a,onCerrar,onGuardado}){ const[f,setF]=useState({fecha:hoyMX(),km:a.km!=null?km(a.km):"",costo:"",taller:"",tipo:"preventivo"}); const[error,setError]=useState("");const[ocupado,setOcupado]=useState(false); async function guardar(){ setError(""); const k=f.km?parseInt(soloDigitos(f.km),10):null; if(f.tipo==="preventivo"&&k==null){setError("Pon el kilometraje: con él calculamos el siguiente servicio.");return;} setOcupado(true); const{data,error}=await sb.rpc("fleet_servicio_registrar",{p_vehiculo:a.id,p_fecha:f.fecha,p_km:k,p_tipo:f.tipo,p_descripcion:null,p_taller:f.taller, p_costo:f.costo?Number(String(f.costo).replace(/[^\d.]/g,"")):null,p_factura_path:null,p_incidente:null,p_regresa_a_servicio:false}); setOcupado(false); if(error||!data.ok){setError(error?mensajeError(error):data.error);return;} onGuardado(); } return setF({...f,tipo:v})} opciones={[{v:"preventivo",t:"Servicio"},{v:"correctivo",t:"Reparación"},{v:"llantas",t:"Llantas"}]}/> {f.tipo==="preventivo"?"Aceite, filtros, afinación. Con esto empieza la cuenta para el siguiente.":"Se guarda en tu historial y en tus gastos."}
setF({...f,fecha:e.target.value})}/>
{const d=soloDigitos(e.target.value).slice(0,7);setF({...f,km:d?km(d):""});}}/>
setF({...f,costo:e.target.value})} placeholder="$1,850"/>
setF({...f,taller:e.target.value})}/>
{error&&
{error}
}
; } function Ajustes({a,correo,puedeAgregar,onAgregar,onCerrar,onGuardado}){ const aseg=useAseguradoras(); const[f,setF]=useState({placas:a.placas||"",estado:a.estado||"",holograma:a.holograma||null,marca:a.marca||"",modelo:a.modelo||"",anio:a.anio||"", aseguradora:(a.seguro&&a.seguro.en_catalogo&&aseg.find(x=>x.nombre===a.seguro.aseguradora)||{}).id||"",poliza:(a.seguro&&a.seguro.poliza)||"",vence:a.seguro_vence||"",intervalo:"10000"}); useEffect(()=>{if(aseg.length&&!f.aseguradora&&a.seguro&&a.seguro.en_catalogo){const x=aseg.find(z=>z.nombre===a.seguro.aseguradora);if(x)setF(v=>({...v,aseguradora:x.id}));}},[aseg.length]); const[error,setError]=useState("");const[ok,setOk]=useState("");const[ocupado,setOcupado]=useState(false); const c=k=>e=>setF({...f,[k]:e.target.value}); async function guardar(e){ e.preventDefault();setError("");setOk("");setOcupado(true); const fila={placas:f.placas.trim().toUpperCase().replace(/\s+/g,"")||null,estado:f.estado||null,holograma:f.holograma||null, circula_zmvm:f.estado==="Ciudad de México"||f.estado==="Estado de México",marca:f.marca.trim()||null,modelo:f.modelo.trim()||null, anio:f.anio?parseInt(f.anio,10):null,seguro_aseguradora_id:f.aseguradora||null,seguro_poliza:f.poliza.trim()||null,seguro_vence:f.vence||null}; const{error}=await sb.from("vehiculos").update(fila).eq("id",a.id); setOcupado(false); if(error){setError(mensajeError(error));return;} onGuardado(); } return(

Ajustes

{correo}
Tu auto
HologramasetF({...f,holograma:v})} opciones={[{v:"00",t:"00"},{v:"0",t:"0"},{v:"1",t:"1"},{v:"2",t:"2"},{v:"exento",t:"Exento"}]}/>
setF({...f,anio:soloDigitos(e.target.value).slice(0,4)})}/>
Tu seguro
{error&&
{error}
}{ok&&
{ok}
} Cuenta {puedeAgregar&&}
); } ReactDOM.createRoot(document.getElementById("root")).render();