API v1 Laravel 12.69.3 JSON · español

Mi casa, bien pensada.

Antes de llamar al arquitecto, descubre qué casa te alcanza.

Este proyecto es el backend de Mi Casapp. La app móvil (Ionic) le hace un cuestionario corto a quien quiere construir: su presupuesto, para qué es la casa, cómo es su terreno y qué quiere tener. Esta API guarda las respuestas, calcula cuánto puede construir y arma un brief para la primera reunión con el arquitecto.

1Cuántos m² alcanzanCon el presupuesto, el tipo de obra y una reserva para imprevistos.
2Lo que agrega el terrenoMuros, nivelación, parqueadero, ventilación: los sobrecostos que nadie avisa.
3Escenarios y briefComparar alternativas y llevar un resumen claro al arquitecto.
URL base
https://mi-casapp.com/api/v1

Cómo calcula

Todo sale de una sola ecuación. Se despeja el área para saber cuánto alcanza, o se fija el área para saber cuánto cuesta.

total = (área × costo/m² + terreno + extras) × (1 + reserva)
área máxima = (presupuesto ÷ (1 + reserva) − terreno − extras) ÷ costo/m²
  • costo/m²: obra gris; si es casa terminada se suman obra blanca y acabados.
  • terreno: muro de contención, nivelación de laterales, parqueadero, lo que suma el clima (ventilación bajo el piso en cálido y húmedo) y la acometida de cada servicio que falte (luz, agua, alcantarillado), según las respuestas.
  • extras: claraboyas, piscina, doble altura, ventanales. Cada uno puede ser imprescindible o sacrificable.
  • Una tarifa por calibrar (sin valor todavía) se muestra como pendiente pero no suma.
  • Rango, nunca cifra exacta: el mismo cálculo se repite con precios bajos y altos (mínimo y máximo de cada precio, o su valor ± el margen general del panel). La API devuelve rango y cada partida trae monto_min y monto_max.
Ejemplo en vivo · Casa de la Familia, Puerto Quito, Pichincha
Con $94.000 se pueden construir 89 – 130 m² de obra gris. Tenía en mente 246,86 m².
Casa ($361 – $441/m²)$39.334 – $47.044
Terreno$14.137 – $17.279
Extras$20.558 – $25.127
Reserva (15 %)$12.261

Flujo de la app

Las pantallas de la app y el endpoint que usa cada una. Cada paso se guarda por separado, así la persona puede salir y retomar.

  1. Al abrirCatálogosGET /catalogos
  2. Paso 1Datos básicosPOST /proyectos
  3. Paso 2Uso de la casaPUT …/uso
  4. Paso 3Tu terrenoPUT …/terreno
  5. Paso 4Lo que quieresPUT …/deseos
  6. ResultadoCuánto alcanzaGET …/resultado
  7. CompararEscenariosGET …/escenarios
  8. CierreBriefPUT …/escenarioGET …/briefPOST …/brief/enlace
  9. OpcionalGuardar en una cuentaPOST /cuenta/registroGET /mis-proyectos

Convenciones

FormatoTodo es JSON. Las respuestas de un recurso vienen en data. Montos en USD y áreas en m², como números.
CuentasOpcionales. Sin cuenta, el id del proyecto (ULID de 26 caracteres) funciona como enlace privado: la app debe guardarlo en el teléfono. Al crear cuenta o iniciar sesión, la app manda esos ids y los proyectos pasan a la cuenta; desde ahí solo los abre su dueño.
TokenAuthorization: Bearer {token} (Sanctum). Es obligatorio en las rutas marcadas Requiere sesión; en las demás es opcional, pero si se envía, los proyectos nuevos nacen en la cuenta. Un proyecto de otra cuenta responde 404.
AdministraciónOpciones, tarifas, textos, colores, proyectos y usuarios se editan en /admin (solo administradores). Los cambios llegan a la API al instante. Primer administrador: php artisan mi-casapp:admin correo@ejemplo.com.
Error 401{ "message": "Necesitas iniciar sesión." } — falta el token o ya no es válido.
CORSPermitidos http://localhost:8100 (ionic serve), capacitor://localhost, ionic://localhost, http://localhost y https://localhost. Se cambian con CORS_ALLOWED_ORIGINS en .env.
NúmerosAceptan número o texto escrito a la ecuatoriana: "$94.000" → 94000, "246,86 m²" → 246.86.
Error 422Validación. { "message": "…", "errors": { "campo": ["mensaje en español"] } }
Error 404{ "message": "Proyecto no encontrado." }

Catálogos

GET /catalogos Probar con el ejemplo ↗

Opciones del cuestionario y tarifas

Todo lo que se administra en /admin: opciones activas del cuestionario (con sus reglas), tarifas con su precio por región, textos de la app y el tema de colores. Solo se listan las regiones con precios; ?region=sierra cambia el "valor" de cada tarifa (por defecto, la primera región). La app no tiene opciones ni textos fijos: arma sus pantallas con esto y aplica tema_css al abrir.

Respuesta
{
  "regiones":    [{ "clave": "sierra", "nombre": "Sierra", "descripcion": "Provincias de la Sierra, también en sus zonas cálidas…" }],
  "region":      "sierra",
  "climas":      [{ "clave": "calido_humedo", "nombre": "Cálido y húmedo", "descripcion": "…",
                   "regiones": ["costa", "sierra", "oriente"], "tarifas": ["ventilacion_piso"] }, …],
  "alcances":    [{ "clave": "obra_gris", "nombre": "Obra gris", "suma_acabados": false }, …],
  "formas":      [{ "clave": "con_quiebres", "nombre": "Con quiebres", "descripcion": "…", "recargo_pct": 6 }, …],
  "niveles":     [{ "clave": "medio", "nombre": "Medio", "descripcion": "Porcelanato…", "tarifa": "acabados_m2" }, …],
  "usos":        [{ "clave": "descanso_alquiler", "nombre": "Descanso y alquiler", "consejo": "…" }, …],
  "cuidadores":  [{ "clave": "cuidador", "nombre": "Un cuidador" }, …],
  "superficies": [
    { "clave": "varios_niveles", "nombre": "Varios niveles", "con_muro": true, "con_laterales": true,
      "muro_sugerido_ml": 8, "laterales_sugerido_m2": 40 }, …
  ],
  "servicios": [{ "clave": "luz", "nombre": "Luz", "tarifa": "acometida_luz" }, …],
  "prioridades": [{ "clave": "imprescindible", "nombre": "Imprescindible" }, …],
  "extras": [
    { "clave": "claraboyas", "nombre": "Claraboyas", "impacto": "alto", "por_unidad": true, "solo_con_acabados": false,
      "tarifa": "claraboya_m2", "cantidad_sugerida": 1, "m2_sugerido": 1.5,
      "etiqueta_cantidad": "Cuántas", "etiqueta_m2": "m² de vidrio c/u", "sufijo_precio": "/ m² de vidrio" }, …
  ],
  "parqueadero_sugerido_m2": 18,
  "tarifas": [
    { "clave": "muro_ml", "nombre": "Muro de contención", "grupo": "terreno", "unidad": "ml",
      "valor": 1576, "por_calibrar": false, "valores": { "sierra": 1576 } }, …
  ],
  "textos": {
    "portada": { "eslogan": "Mi casa, bien pensada.", "titulo": "…", "descripcion": "…", "beneficio_1": "…" },
    "cuestionario": { "datos_titulo": "Tu proyecto", "terreno_descripcion": "…", "parqueadero_pregunta": "…" },
    "brief": { "aviso": "Estimado para planificar, no es una cotización.", … }
  },
  "google": { "web_client_id": "….apps.googleusercontent.com", "ios_client_id": "…" },
  "tema_css": ":root { --ion-color-primary: #c2410c; … } @media (prefers-color-scheme: dark) { … }"
}

Guía educativa: lista de fichas

Fichas activas, sin el texto. pantallas dice en qué pantallas de la app se enlaza cada una ("Aprende").

Respuesta
{
  "data": [
    { "clave": "obra_gris", "etapa": "planificar", "tema": "Conceptos", "titulo": "Obra gris, obra blanca y acabados",
      "resumen": "Las tres capas de una casa y qué incluye cada una.", "pantallas": ["datos", "resultado"] }, …
  ]
}
GET /guia/{clave} Probar con el ejemplo ↗

Guía educativa: una ficha

La ficha completa. contenido_html sale del Markdown del panel, sin HTML propio ni enlaces peligrosos.

Respuesta
{ "data": { "clave": "obra_gris", "titulo": "…", "contenido_html": "<p>…</p>", "actualizada": "2026-10-11", … } }

Cuestionario

POST /proyectos

Paso 1 · Crear el proyecto

Responde 201 con el Proyecto. Guarda data.id en el dispositivo: es la llave del proyecto. Para editar este paso después: PUT /proyectos/{id}/datos con los mismos campos.

CampoTipoReglas
nombrestringRequerido, máx. 120
ubicacionstringRequerido, máx. 160. El lugar, para el brief
regionclave de /catalogos regionesRequerido. Define los precios; solo las regiones con precios
presupuestonúmeroRequerido, 1.000 – 100.000.000
alcanceobra_gris | terminadaRequerido
nivelclave de /catalogos nivelesOpcional (medio por defecto). Cuenta en casa terminada
area_deseadanúmero | nullOpcional, 10 – 100.000 m²
Cuerpo
{
  "nombre": "Casa de la Familia",
  "ubicacion": "Puerto Quito, Pichincha",
  "region": "sierra",
  "presupuesto": 94000,
  "alcance": "obra_gris",
  "area_deseada": 246.86
}
Respuesta
{ "data": Proyecto }
PUT /proyectos/{id}/uso

Paso 2 · Uso de la casa

CampoTipoReglas
usopermanente | descanso | descanso_alquiler | alquilerRequerido
cuidadornadie | cuidador | administradorRequerido
personasentero | nullOpcional, 1 – 30. Sugiere dormitorios y baños
alquilerno | a_veces | principalOpcional. Distinto de "no" activa el retorno del alquiler
precio_noche, noches_anio, gastos_anionúmero | nullPara el retorno. Con alquiler "no" se borran
ampliarno | otro_piso | a_los_ladosOpcional. otro_piso activa el escenario por pisos
Cuerpo
{ "uso": "descanso_alquiler", "cuidador": "cuidador", "personas": 6, "alquiler": "a_veces", "precio_noche": 120, "noches_anio": 60, "gastos_anio": 2200, "ampliar": "no" }
Respuesta
{ "data": Proyecto }
PUT /proyectos/{id}/terreno

Paso 3 · Terreno

Si la persona no midió su terreno, manda null en las cantidades y se usan las sugeridas del catálogo. Lo que no aplica a la superficie elegida se guarda en 0.

CampoTipoReglas
superficieplana | pendiente_leve | varios_nivelesRequerido
muro_mlnúmero | nullSolo cuenta con varios_niveles. null = sugerido (8 ml)
laterales_m2número | nullNo cuenta con plana. null = sugerido
parqueaderobooleanRequerido
parqueadero_m2número | nullnull = 18 m²
climaclave de /catalogos climasRequerido. Uno de los climas de la región del proyecto. Suma sus tarifas (calido_humedo: ventilación bajo el piso)
clima_humedobooleanSolo si no se manda clima (v1): true = calido_humedo, false = templado
servicios_faltantesarrayClaves de los servicios que faltan (catálogo servicios); cada uno suma su acometida. [] = tiene todos
serviciosbooleanSolo si no se manda servicios_faltantes (v1): false = faltan todos
acceso_camionesbooleanOpcional (true). false suma el recargo por acarreo
fuera_ciudadbooleanOpcional (false). true suma el recargo por transporte
suelo_conocidobooleanOpcional (true). false suma el estudio de suelos
cerramientobooleanOpcional (false)
cerramiento_mlnúmero | nullnull = sugerido (60 ml)
Cuerpo
{
  "superficie": "varios_niveles",
  "muro_ml": 8,
  "laterales_m2": 40,
  "parqueadero": true,
  "parqueadero_m2": 18,
  "clima": "calido_humedo",
  "servicios_faltantes": ["alcantarillado"]
}
Respuesta
{ "data": Proyecto }
PUT /proyectos/{id}/deseos

Paso 4 · Lo que quieres

Un extra que no se envía queda en "no". cantidad y m2 (m² por unidad: de vidrio, de piso…) solo aplican a los extras con por_unidad: true.

CampoTipoReglas
dormitoriosenteroRequerido, 0 – 20
banosenteroRequerido, 0 – 20
estudiosenteroRequerido, 0 – 10
pisosenteroOpcional, 1 – 3 (máximo en el panel). Con 2 o más aparece el escenario por pisos
formaclave de /catalogos formas | nullOpcional. Suma su recargo por complejidad a cada m²
extras.{clave}.prioridadno | imprescindible | sacrificableOpcional
extras.{clave}.cantidadentero | null1 – 50
extras.{clave}.m2número | null0,1 – 100
Cuerpo
{
  "dormitorios": 4, "banos": 4, "estudios": 1,
  "extras": {
    "claraboyas": { "prioridad": "imprescindible", "cantidad": 4, "m2": 1.87 },
    "piscina":    { "prioridad": "sacrificable" }
  }
}
Respuesta
{ "data": Proyecto }
GET /proyectos/{id} Probar con el ejemplo ↗

Retomar un proyecto

data.pasos_completos dice qué pasos ya respondió, para saber dónde retomar el cuestionario.

Respuesta
{ "data": Proyecto }
DELETE /proyectos/{id}

Borrar un proyecto

Borra el proyecto para siempre. Si está en una cuenta, solo lo puede borrar su dueño.

Respuesta
{ "message": "Proyecto eliminado." }

Resultados

GET /proyectos/{id}/resultado Probar con el ejemplo ↗

Cuánto alcanza

"ajustado" es la casa más grande que permite el presupuesto. "original" es el área que la persona tenía en mente (null si no la dio). En casa terminada, "por_nivel" da el área (y el costo del área en mente) en cada nivel de acabados, a la vez; en obra gris viene vacío.

Respuesta
{
  "data": {
    "ajustado": Analisis,
    "original": Analisis | null,
    "area_sin_extras": 165.08,
    "area_sin_extras_rango": [145.6, 187.6],
    "retorno": { "ingreso_anual": 7200, "gastos_anuales": 2200, "neto_anual": 5000, "costo": [94000, 94000], "anios": [18.8, 18.8] },
    "recargos": [{ "concepto": "Transporte fuera de la ciudad", "pct": 8 }],
    "por_nivel": [
      { "clave": "economico", "nombre": "Económico", "elegido": false, "area": [62.4, 91.2], "total_original": [227054, 277510] },
      { "clave": "medio", "nombre": "Medio", "elegido": true, "area": [53.8, 78.1], "total_original": [257637, 314890] }, …
    ]
  }
}
GET /proyectos/{id}/escenarios Probar con el ejemplo ↗

Escenarios para comparar

Solo aparecen los escenarios que aplican: original y plano necesitan area_deseada; sin_sacrificables solo si sacrificar algo libera área. "palancas" ordena qué ahorra más (con reserva incluida). "por_etapas": el mismo diseño (área en mente o la que alcanza) hasta obra gris, obra blanca o casa terminada. "por_pisos": un piso ahora, con la estructura preparada, y el resto después (null en casas de un piso).

Respuesta
{
  "data": {
    "escenarios": [
      { "clave": "original", "titulo": "Diseño original",
        "detalle": "246,86 m² · terreno con varios niveles", "elegido": false, "analisis": Analisis },
      { "clave": "plano", "titulo": "Mismo diseño, terreno plano", … },
      { "clave": "ajustado", "titulo": "Ajustado a tu presupuesto", "elegido": true, … }
    ],
    "palancas": [
      { "concepto": "un terreno plano", "ahorro": 16109.2 },
      { "concepto": "quitar 4 claraboyas", "ahorro": 3268.76 }
    ],
    "por_etapas": {
      "area": 246.86,
      "etapas": [
        { "clave": "gris", "nombre": "Obra gris", "total": [142247.89, 173858.54], "diferencia": [48247.89, 79858.54], "dentro_del_presupuesto": false },
        { "clave": "blanca", "nombre": "Obra blanca", … }, { "clave": "terminada", "nombre": "Casa terminada", … }
      ]
    },
    "por_pisos": {
      "pisos": 2, "area": 246.86,
      "ahora": { "area": 123.43, "total": [96191, 117567], "dentro_del_presupuesto": false, "partidas_extras": [ … ] },
      "despues": { "area": 123.43, "total": [51174, 62546] },
      "junto": { "total": [142248, 173859] },
      "sobrecosto": [5117, 6254]
    }
  }
}
PUT /proyectos/{id}/escenario

Elegir el escenario del brief

CampoTipoReglas
escenariostringUna de las claves que devolvió /escenarios
Cuerpo
{ "escenario": "ajustado" }
Respuesta
{ "data": Proyecto }
PUT /proyectos/{id}/cotizaciones/{clave}

Cotización real de una partida

Reemplaza el estimado por un monto real, sin rango. {clave}: "partida:{tarifa o extra}" (partida:muro_ml, partida:piscina, partida:acometida_agua…) o "grupo:{grupo}" (grupo:obra_gris, grupo:acabados…). Un grupo de la casa se cotiza para un área y vale como precio por m² (monto ÷ área), así el área que alcanza se sigue calculando. Cada partida y grupo del análisis trae "clave" y "cotizado". DELETE en la misma ruta vuelve al estimado.

CampoTipoReglas
montonúmeroRequerido. Acepta "$6.776"
areanúmeroRequerido en grupos: los m² que cubre la cotización
notastring | nullOpcional: de quién es la cotización
Cuerpo
{ "monto": 36000, "area": 100, "nota": "Constructora XYZ" }
Respuesta
{ "data": Proyecto }  // cotizaciones: { "grupo:obra_gris": { "monto": 36000, "area": 100, "nota": "…" } }
PUT /proyectos/{id}/etapa

Pasar a otra etapa

Cuando la persona decide pasar a la siguiente etapa (p. ej. de planificar a diseñar). Solo etapas disponibles (/catalogos etapas); para salir de planificar hay que haber terminado el cuestionario. Al reabrir el proyecto, la app entra por su etapa.

CampoTipoReglas
etapaclave de /catalogos etapasRequerido. Solo las disponibles
Cuerpo
{ "etapa": "disenar" }
Respuesta
{ "data": Proyecto }
GET /proyectos/{id}/brief Probar con el ejemplo ↗

Brief para el arquitecto

Los textos ya vienen armados en español; la app solo los acomoda en pantalla o en PDF.

Respuesta
{
  "data": {
    "proyecto": Proyecto,
    "lugar": "Puerto Quito",
    "escenario": { "clave": "ajustado", "titulo": "Ajustado a tu presupuesto",
                   "detalle": "~158 m² · mismo terreno" },
    "analisis": Analisis,
    "area_sin_extras": 165.08,
    "extras_con_precio": "4 claraboyas",
    "programa": "4 dormitorios · 4 baños · estudio · parqueadero",
    "terreno": "Varios niveles: muro de contención y laterales. Zona cálida y húmeda: …",
    "prioridades": {
      "imprescindible": "claraboyas",
      "sacrificable": "piscina",
      "recomendacion": "Acabados resistentes y de bajo mantenimiento."
    },
    "preguntas": [
      "¿El diseño cabe en ~158 m² sin perder dormitorios?",
      "¿Qué solución propone para los desniveles del terreno?",
      …
    ],
    "aviso": "Estimado para planificar, no es una cotización."
  }
}
POST /proyectos/{id}/brief/enlace

Enlace al PDF del brief

Devuelve un enlace público al PDF, para abrirlo o compartirlo (WhatsApp, correo). El enlace no contiene el id del proyecto y no permite editarlo; es siempre el mismo y el PDF sale con los datos del momento. Se abre sin token: GET /brief/{codigo}.pdf (fuera de /api/v1).

Respuesta
{
  "data": {
    "url": "http://localhost/mi-casapp/public/brief/l03utov2RXWRRbziAz3RMYRKCSgS5tpCaH7rhkFx.pdf",
    "nombre": "Brief · Casa de la Familia"
  }
}

Cuenta

POST /cuenta/registro

Crear cuenta

Responde 201 con el token de sesión. Manda en "proyectos" los ids anónimos guardados en el teléfono: pasan a la cuenta (los que ya tienen dueño se ignoran). Guarda el token de forma segura y envíalo como Authorization: Bearer {token}.

CampoTipoReglas
nombrestringRequerido, máx. 120
emailstringRequerido, correo válido y único
passwordstringRequerido, mínimo 8 caracteres
password_confirmationstringIgual a password
dispositivostring | nullNombre de la sesión, ej. "iPhone de Ana"
proyectosstring[] | nullIds de proyectos anónimos, máx. 50
Cuerpo
{
  "nombre": "Ana",
  "email": "ana@ejemplo.com",
  "password": "casa-propia-2026",
  "password_confirmation": "casa-propia-2026",
  "dispositivo": "iPhone de Ana",
  "proyectos": ["01m4ek3w61f0qgcera381sj3xr"]
}
Respuesta
{
  "data": {
    "token": "1|je6dJnC1BOAeMpmA1f5nnl…",
    "usuario": { "id": 1, "nombre": "Ana", "email": "ana@ejemplo.com",
                 "creado": "2026-10-08T20:50:58+00:00" },
    "proyectos_reclamados": 1
  }
}
POST /cuenta/login

Iniciar sesión

Igual que el registro: devuelve un token y reclama los proyectos anónimos que mandes. Si el correo o la contraseña fallan responde 422 en errors.email. Máximo 10 intentos por minuto.

CampoTipoReglas
emailstringRequerido
passwordstringRequerido
dispositivostring | nullOpcional
proyectosstring[] | nullOpcional
Cuerpo
{ "email": "ana@ejemplo.com", "password": "casa-propia-2026", "proyectos": [] }
Respuesta
{ "data": { "token": "…", "usuario": { … }, "proyectos_reclamados": 0 } }
POST /cuenta/google

Entrar con Google

Con el ID token que Google da en el teléfono. El backend lo verifica con Google (que sea para esta app, vigente y con correo verificado). Si ya hay una cuenta con ese correo, se vincula; si no, se crea (201). Reclama los proyectos como el login. 404 si Google no está configurado (/catalogos google = null).

CampoTipoReglas
id_tokenstringRequerido
dispositivostring | nullOpcional
proyectosstring[] | nullOpcional
Cuerpo
{ "id_token": "eyJhbGciOi…", "proyectos": [] }
Respuesta
{ "data": { "token": "…", "usuario": { "email": "ana@gmail.com", "con_password": false, "con_google": true, … }, "proyectos_reclamados": 1 } }
GET /cuenta Requiere sesión

Mi cuenta

Sirve para comprobar al abrir la app si el token guardado sigue vigente (401 = hay que volver a iniciar sesión).

Respuesta
{ "data": { "id": 1, "nombre": "Ana", "email": "ana@ejemplo.com", "creado": "…" } }
GET /mis-proyectos Requiere sesión

Mis proyectos

Los proyectos de la cuenta, del más reciente al más antiguo.

Respuesta
{ "data": [Proyecto, …] }
POST /mis-proyectos/reclamar Requiere sesión

Pasar proyectos anónimos a la cuenta

Para proyectos creados sin sesión en este teléfono cuando la persona ya tenía la sesión abierta en otro lado.

CampoTipoReglas
proyectosstring[]Requerido, máx. 50
Cuerpo
{ "proyectos": ["01m4ek3w61f0qgcera381sj3xr"] }
Respuesta
{ "data": { "proyectos_reclamados": 1 } }
POST /cuenta/logout Requiere sesión

Cerrar sesión

Revoca solo el token de este dispositivo. Después, la app debe borrar el token guardado.

Respuesta
{ "message": "Sesión cerrada." }
DELETE /cuenta Requiere sesión

Eliminar la cuenta

Borra la cuenta, todos sus proyectos y todas sus sesiones. Pide la contraseña para confirmar. La App Store exige que una app con cuentas permita eliminarlas desde la app.

CampoTipoReglas
passwordstringLa contraseña actual (cuentas con contraseña)
confirmacion"ELIMINAR"En cuentas creadas con Google, que no tienen contraseña
Cuerpo
{ "password": "casa-propia-2026" }
Respuesta
{ "message": "Cuenta eliminada." }

Objetos

Proyecto

Lo que devuelven todos los pasos del cuestionario. Siempre trae los cuatro extras del catálogo; los que no quiere tienen prioridad "no". guardado_en_cuenta: false significa que es anónimo y la app debe guardar su id en el teléfono.

{
  "id": "01m4ek3w61f0qgcera381sj3xr",
  "nombre": "Casa de la Familia",
  "ubicacion": "Puerto Quito, Pichincha",
  "region": "sierra",
  "presupuesto": 94000,
  "alcance": "obra_gris",
  "nivel": "medio",
  "area_deseada": 246.86,
  "uso": "descanso_alquiler",
  "cuidador": "cuidador",
  "terreno": {
    "superficie": "varios_niveles", "muro_ml": 8, "laterales_m2": 40,
    "parqueadero": true, "parqueadero_m2": 18, "clima": "calido_humedo", "clima_humedo": true, "servicios": true,
    "servicios_faltantes": []
  },
  "programa": { "dormitorios": 4, "banos": 4, "estudios": 1 },
  "extras": {
    "claraboyas":   { "prioridad": "imprescindible", "cantidad": 4, "m2": 1.87 },
    "piscina":      { "prioridad": "sacrificable", "cantidad": 1, "m2": 1.5 },
    "doble_altura": { "prioridad": "no", "cantidad": 1, "m2": 1.5 },
    "ventanales":   { "prioridad": "no", "cantidad": 1, "m2": 1.5 }
  },
  "escenario": "ajustado",
  "cotizaciones": {},
  "etapa": "planificar",
  "guardado_en_cuenta": false,
  "pasos_completos": { "datos": true, "uso": true, "terreno": true, "deseos": true },
  "creado": "2026-10-08T20:30:00+00:00",
  "actualizado": "2026-10-08T20:31:00+00:00"
}

Analisis

El cálculo de una casa de cierta área. "diferencia" es total − presupuesto: positivo significa que se pasa. Una partida con "monto": null está por calibrar: se muestra, pero no suma. Los valores sueltos son el cálculo central; lo que se muestra al usuario es "rango" ([mínimo, máximo] con precios bajos y altos) y monto_min / monto_max de cada partida. "grupos" reparte la casa (desglose.casa) en grupos de partidas con los % del panel: preliminares, obra gris, instalaciones y, en casa terminada, obra blanca y acabados.

{
  "presupuesto": 94000,
  "area": 107.82,
  "area_maxima": 107.82,
  "costo_m2": 400.58,
  "total": 94000,
  "diferencia": 0,
  "dentro_del_presupuesto": true,
  "desglose": { "casa": 43188.73, "terreno": 15708, "extras": 22842.4, "reserva": 12260.87, "reserva_pct": 15 },
  "proporciones": { "casa": 45.95, "terreno": 16.71, "extras": 24.3, "reserva": 13.04 },
  "partidas_terreno": [
    { "concepto": "Muro de contención (8 ml)", "monto": 12608, "monto_min": 11347.2, "monto_max": 13868.8 },
    { "concepto": "Nivelación de laterales (40 m²)", "monto": 1400, "monto_min": 1260, "monto_max": 1540 },
    { "concepto": "Parqueadero con grava (18 m²)", "monto": 828, "monto_min": 745.2, "monto_max": 910.8 },
    { "concepto": "Ventilación bajo el piso", "monto": 872, "monto_min": 784.8, "monto_max": 959.2 }
  ],
  "partidas_extras": [
    { "concepto": "4 claraboyas", "monto": 2842.4, "monto_min": 2558.16, "monto_max": 3126.64 },
    { "concepto": "Piscina", "monto": 20000, "monto_min": 18000, "monto_max": 22000 }
  ],
  "grupos": [
    { "clave": "preliminares", "concepto": "Preliminares", "partidas": "Estudio de suelos, diseño estructural, permisos…", "monto": 1727.55, "monto_min": 1573.35, "monto_max": 1881.75 }, …
  ],
  "pendientes": [],
  "rango": {
    "area": [89.27, 130.49],
    "costo_m2": [360.52, 440.64],
    "total": [94000, 94000],
    "diferencia": [0, 0],
    "casa": [39333.69, 47043.77],
    "terreno": [14137.2, 17278.8],
    "extras": [20558.16, 25126.64],
    "reserva": [12260.87, 12260.87]
  }
}

Tarifas vigentes

Leídas de la base en este momento, con un precio por región (tabla precios). Se calibran en el panel /admin. Una región sin precio de obra gris no se ofrece en la app.

GrupoConceptoClaveUnidadCostaSierraOriente
Construcción Casa en obra gris obra_gris_m2 USD / m² $401 $401 $401
Obra blanca y acabados · medio acabados_m2 % 40 % 40 % 40 %
Obra blanca y acabados · económico acabados_economico % 30 % 30 % 30 %
Obra blanca y acabados · alto acabados_alto % 50 % 50 % 50 %
Estructura preparada para crecer preparacion_pisos % 10 % 10 % 10 %
General Reserva de imprevistos reserva_pct % 15 % 15 % 15 %
Terreno Muro de contención muro_ml USD / m lineal $1.576 $1.576 $1.576
Nivelación de laterales laterales_m2 USD / m² $35 $35 $35
Parqueadero con grava parqueadero_m2 USD / m² $46 $46 $46
Ventilación bajo el piso ventilacion_piso USD global $872 $872 $872
Acometida de luz acometida_luz USD global $2.000 $2.000 $2.000
Acometida de agua acometida_agua USD global $3.000 $3.000 $3.000
Alcantarillado o pozo séptico acometida_alcantarillado USD global $2.500 $2.500 $2.500
Acarreo manual (sin acceso para camiones) acarreo_pct % por calibrar 5 % por calibrar
Transporte fuera de la ciudad transporte_pct % por calibrar 8 % por calibrar
Cerramiento perimetral cerramiento_ml USD / m lineal por calibrar por calibrar por calibrar
Estudio de suelos estudio_suelos USD global por calibrar por calibrar por calibrar
Elementos especiales Claraboyas claraboya_m2 USD / m² de vidrio $380 $380 $380
Piscina piscina USD global $20.000 $20.000 $20.000
Doble altura doble_altura USD / m² $225 $225 $225
Ventanales grandes ventanales USD / m² de vidrio $37 $37 $37

Marca

Hoja de marca v1. Para la app Ionic: el logo, los colores con sus variables de tema y las reglas de uso. Los valores salen de config/marca.php.

Logo y variantes
Principal · apilado
Horizontal · encabezados y web
Invertido · sobre fondos oscuros
Ícono de app
Ícono mínimo, 40 px Ícono mínimo, 24 px
Ícono mínimo · bajo 32 px, solo el techo · favicon.svg
Blanco · sobre naranja
Colores
Naranja obra #C2410C · primary Marca, "app", botón principal
Carbón #1E1D1A · dark Texto, logo, botones
Concreto #F3F1EC · light Fondo de la app
Pizarra #2F4858 · secondary Datos, gráficos, consejos
Semáforo rojo #B42318 · danger Encarece mucho, sobre presupuesto
Semáforo ámbar #B54708 · warning Encarece moderadamente
Semáforo verde #067647 · success Dentro del presupuesto
Colores en modo oscuro · variante invertida
Aa
Naranja obra #F07A3E · primary
Aa
Pizarra #8DB3CB · secondary
Aa
Semáforo verde #4CC38A · success
Aa
Semáforo ámbar #F2A54A · warning
Aa
Semáforo rojo #F0857A · danger
Aa
Concreto #F3F1EC · dark
Aa
Superficie #2A2925 · light

Fondo #1E1D1A · superficies #2A2925 · texto #FFFFFF. Sobre los colores va texto Carbón ("Aa").

Tipografía · Google Fonts, uso comercial libre
Bricolage Grotesque · Bold 700 · Logo y titulares Mi casa, bien pensada.
Figtree · Regular 400, Medium 500, SemiBold 600 · Textos e interfaz Responde unas preguntas sobre tu presupuesto, tu terreno y para qué es la casa. Sales con un brief claro para tu primera reunión.
Reglas de uso del logo
  • Sí: El techo siempre cubre toda la palabra "casapp".
  • Sí: "app" va en naranja (o en el color único de la variante).
  • Sí: Deja alrededor del logo un margen al menos igual a la altura de la "c".
  • No: No cambies el color de "app" a otro que no sea de la paleta.
  • No: No separes el techo de la palabra ni lo muevas sobre "mi".
  • No: No estires, inclines ni agregues sombras o degradados.

Tema para Ionic

Pega esto en src/theme/variables.scss y carga las fuentes en index.html (Bricolage Grotesque 700 y Figtree 400/500/600 desde Google Fonts). Con eso, color="primary" es Naranja obra y los semáforos quedan en danger, warning y success. Incluye el modo oscuro, que se activa solo según la configuración del teléfono. Todos los colores tienen contraste suficiente con su texto (4,5:1 o más).

:root {
  /* Mi Casapp · hoja de marca v1 */

  /* Naranja obra */
  --ion-color-primary: #c2410c;
  --ion-color-primary-rgb: 194, 65, 12;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #ab390b;
  --ion-color-primary-tint: #c85424;

  /* Pizarra */
  --ion-color-secondary: #2f4858;
  --ion-color-secondary-rgb: 47, 72, 88;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255, 255, 255;
  --ion-color-secondary-shade: #293f4d;
  --ion-color-secondary-tint: #445a69;

  /* Semáforo verde */
  --ion-color-success: #067647;
  --ion-color-success-rgb: 6, 118, 71;
  --ion-color-success-contrast: #ffffff;
  --ion-color-success-contrast-rgb: 255, 255, 255;
  --ion-color-success-shade: #05683e;
  --ion-color-success-tint: #1f8459;

  /* Semáforo ámbar */
  --ion-color-warning: #b54708;
  --ion-color-warning-rgb: 181, 71, 8;
  --ion-color-warning-contrast: #ffffff;
  --ion-color-warning-contrast-rgb: 255, 255, 255;
  --ion-color-warning-shade: #9f3e07;
  --ion-color-warning-tint: #bc5921;

  /* Semáforo rojo */
  --ion-color-danger: #b42318;
  --ion-color-danger-rgb: 180, 35, 24;
  --ion-color-danger-contrast: #ffffff;
  --ion-color-danger-contrast-rgb: 255, 255, 255;
  --ion-color-danger-shade: #9e1f15;
  --ion-color-danger-tint: #bc392f;

  /* Carbón */
  --ion-color-dark: #1e1d1a;
  --ion-color-dark-rgb: 30, 29, 26;
  --ion-color-dark-contrast: #ffffff;
  --ion-color-dark-contrast-rgb: 255, 255, 255;
  --ion-color-dark-shade: #1a1a17;
  --ion-color-dark-tint: #353431;

  /* Concreto */
  --ion-color-light: #f3f1ec;
  --ion-color-light-rgb: 243, 241, 236;
  --ion-color-light-contrast: #1e1d1a;
  --ion-color-light-contrast-rgb: 30, 29, 26;
  --ion-color-light-shade: #d6d4d0;
  --ion-color-light-tint: #f4f2ee;

  --ion-background-color: #f3f1ec;
  --ion-background-color-rgb: 243, 241, 236;
  --ion-text-color: #1e1d1a;
  --ion-text-color-rgb: 30, 29, 26;
  --ion-font-family: 'Figtree', system-ui, sans-serif;
}

/* Modo oscuro: variante invertida del logo (Carbón, blanco y naranja). */
@media (prefers-color-scheme: dark) {
  :root,
  :root.ios,
  :root.md {

    /* Naranja obra */
    --ion-color-primary: #f07a3e;
    --ion-color-primary-rgb: 240, 122, 62;
    --ion-color-primary-contrast: #1e1d1a;
    --ion-color-primary-contrast-rgb: 30, 29, 26;
    --ion-color-primary-shade: #d36b37;
    --ion-color-primary-tint: #f28751;

    /* Pizarra */
    --ion-color-secondary: #8db3cb;
    --ion-color-secondary-rgb: 141, 179, 203;
    --ion-color-secondary-contrast: #1e1d1a;
    --ion-color-secondary-contrast-rgb: 30, 29, 26;
    --ion-color-secondary-shade: #7c9eb3;
    --ion-color-secondary-tint: #98bbd0;

    /* Semáforo verde */
    --ion-color-success: #4cc38a;
    --ion-color-success-rgb: 76, 195, 138;
    --ion-color-success-contrast: #1e1d1a;
    --ion-color-success-contrast-rgb: 30, 29, 26;
    --ion-color-success-shade: #43ac79;
    --ion-color-success-tint: #5ec996;

    /* Semáforo ámbar */
    --ion-color-warning: #f2a54a;
    --ion-color-warning-rgb: 242, 165, 74;
    --ion-color-warning-contrast: #1e1d1a;
    --ion-color-warning-contrast-rgb: 30, 29, 26;
    --ion-color-warning-shade: #d59141;
    --ion-color-warning-tint: #f3ae5c;

    /* Semáforo rojo */
    --ion-color-danger: #f0857a;
    --ion-color-danger-rgb: 240, 133, 122;
    --ion-color-danger-contrast: #1e1d1a;
    --ion-color-danger-contrast-rgb: 30, 29, 26;
    --ion-color-danger-shade: #d3756b;
    --ion-color-danger-tint: #f29187;

    /* Concreto */
    --ion-color-dark: #f3f1ec;
    --ion-color-dark-rgb: 243, 241, 236;
    --ion-color-dark-contrast: #1e1d1a;
    --ion-color-dark-contrast-rgb: 30, 29, 26;
    --ion-color-dark-shade: #d6d4d0;
    --ion-color-dark-tint: #f4f2ee;

    /* Superficie */
    --ion-color-light: #2a2925;
    --ion-color-light-rgb: 42, 41, 37;
    --ion-color-light-contrast: #ffffff;
    --ion-color-light-contrast-rgb: 255, 255, 255;
    --ion-color-light-shade: #252421;
    --ion-color-light-tint: #3f3e3b;

    --ion-background-color: #1e1d1a;
    --ion-background-color-rgb: 30, 29, 26;
    --ion-text-color: #ffffff;
    --ion-text-color-rgb: 255, 255, 255;
    --ion-toolbar-background: #1e1d1a;
    --ion-item-background: #2a2925;
    --ion-card-background: #2a2925;

    /* Escalones entre fondo y texto (bordes, inputs, separadores). */
    --ion-background-color-step-50: #292825;
    --ion-background-color-step-100: #353431;
    --ion-background-color-step-150: #403f3c;
    --ion-background-color-step-200: #4b4a48;
    --ion-background-color-step-250: #565653;
    --ion-background-color-step-300: #62615f;
    --ion-background-color-step-350: #6d6c6a;
    --ion-background-color-step-400: #787776;
    --ion-background-color-step-450: #838381;
    --ion-background-color-step-500: #8f8e8d;
    --ion-background-color-step-550: #9a9998;
    --ion-background-color-step-600: #a5a5a3;
    --ion-background-color-step-650: #b0b0af;
    --ion-background-color-step-700: #bcbbba;
    --ion-background-color-step-750: #c7c7c6;
    --ion-background-color-step-800: #d2d2d1;
    --ion-background-color-step-850: #dddddd;
    --ion-background-color-step-900: #e9e8e8;
    --ion-background-color-step-950: #f4f4f4;
    --ion-text-color-step-50: #f4f4f4;
    --ion-text-color-step-100: #e9e8e8;
    --ion-text-color-step-150: #dddddd;
    --ion-text-color-step-200: #d2d2d1;
    --ion-text-color-step-250: #c7c7c6;
    --ion-text-color-step-300: #bcbbba;
    --ion-text-color-step-350: #b0b0af;
    --ion-text-color-step-400: #a5a5a3;
    --ion-text-color-step-450: #9a9998;
    --ion-text-color-step-500: #8f8e8d;
    --ion-text-color-step-550: #838381;
    --ion-text-color-step-600: #787776;
    --ion-text-color-step-650: #6d6c6a;
    --ion-text-color-step-700: #62615f;
    --ion-text-color-step-750: #565653;
    --ion-text-color-step-800: #4b4a48;
    --ion-text-color-step-850: #403f3c;
    --ion-text-color-step-900: #353431;
    --ion-text-color-step-950: #292825;
  }
}

Desarrollo local

Con XAMPP en macOS. Usa siempre el PHP de XAMPP (8.2): las dependencias están fijadas a esa versión.

  1. Crear la base mi_casapp (utf8mb4) en MySQL de XAMPP.
  2. Instalar y configurar:
    cp .env.example .env
    composer install
    /Applications/XAMPP/xamppfiles/bin/php artisan key:generate
    /Applications/XAMPP/xamppfiles/bin/php artisan migrate --seed
  3. Abrir https://mi-casapp.com. El seeder crea las tarifas de Puerto Quito y el proyecto de ejemplo.
  4. Correr los tests:
    /Applications/XAMPP/xamppfiles/bin/php artisan test
  5. Desde Ionic en un teléfono o emulador, localhost es el propio dispositivo: como URL base usa la IP de tu Mac en la red local (por ejemplo http://192.168.1.20/mi-casapp/public/api/v1).