Documentation Index Fetch the complete documentation index at: https://mintlify.com/joaoelian204/Portal-Ciudadano-Manta-web/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Reports System enables citizens to report municipal issues (potholes, broken streetlights, etc.) with photos and location data. Administrators can review, prioritize, and update report statuses in real-time.
Report Categories
Alumbrado Street lighting issues
Baches Potholes and road damage
Limpieza Cleaning and waste issues
Agua Water supply problems
Alcantarillado Sewage and drainage issues
Parques Parks and recreation areas
Señalización Traffic signs and signals
Seguridad Public safety concerns
Data Model
Report Structure
type ReporteCategoria =
| "alumbrado"
| "baches"
| "limpieza"
| "agua"
| "alcantarillado"
| "parques"
| "seƱalizacion"
| "seguridad"
| "ruido"
| "otro" ;
type ReporteEstado =
| "pendiente" // Initial state
| "en_revision" // Admin is reviewing
| "en_proceso" // Accepted and being resolved
| "resuelto" // Completed successfully
| "rechazado" ; // Rejected or duplicate
type ReportePrioridad = "baja" | "media" | "alta" | "urgente" ;
interface Reporte {
id : string ;
usuario_id : string ;
categoria : ReporteCategoria ;
descripcion : string ;
latitud : number ;
longitud : number ;
direccion : string ;
imagen_url ?: string ; // Supabase Storage URL
estado : ReporteEstado ;
prioridad ?: ReportePrioridad ;
notas_admin ?: string ; // Admin notes
fecha_resolucion ?: string ;
created_at : string ;
updated_at : string ;
}
Reports Store (reportes.store.ts)
State Management
import { defineStore } from "pinia" ;
import { ref } from "vue" ;
import { supabase } from "../lib/supabase" ;
import type { Database } from "../types/database.types" ;
type Reporte = Database [ "public" ][ "Tables" ][ "reportes" ][ "Row" ];
type InsertReporte = Database [ "public" ][ "Tables" ][ "reportes" ][ "Insert" ];
type UpdateReporte = Database [ "public" ][ "Tables" ][ "reportes" ][ "Update" ];
export const useReportesStore = defineStore ( "reportes" , () => {
const reportes = ref < Reporte []>([]);
const reporteActual = ref < Reporte | null >( null );
const loading = ref ( false );
const error = ref < string | null >( null );
// ... actions
});
Creating Reports
const crearReporte = async ( reporte : Omit < InsertReporte , "usuario_id" >) => {
loading . value = true ;
error . value = null ;
try {
const authStore = useAuthStore ();
console . log ( 'š Usuario autenticado:' , authStore . user ?. id );
if ( ! authStore . user ) throw new Error ( "Usuario no autenticado" );
const dataToInsert = {
... reporte ,
usuario_id: authStore . user . id ,
};
console . log ( 'š Datos a insertar:' , dataToInsert );
const { data , error : insertError } = await supabase
. from ( "reportes" )
. insert ( dataToInsert )
. select ()
. single ();
console . log ( 'š Respuesta de Supabase:' , { data , error: insertError });
if ( insertError ) {
console . error ( 'ā Error detallado de Supabase:' , {
message: insertError . message ,
details: insertError . details ,
hint: insertError . hint ,
code: insertError . code
});
throw insertError ;
}
reportes . value . unshift ( data );
return { success: true , data };
} catch ( err : any ) {
error . value = err . message ?? String ( err );
console . error ( "ā Error creando reporte:" , err );
return { success: false , error: err . message ?? String ( err ) };
} finally {
loading . value = false ;
}
};
Image Upload
Reports can include photos uploaded to Supabase Storage:
const subirImagen = async ( file : File , reporteId : string ) => {
loading . value = true ;
error . value = null ;
try {
const fileExt = file . name . split ( "." ). pop ();
const fileName = ` ${ reporteId } - ${ Date . now () } . ${ fileExt } ` ;
const filePath = `reportes/ ${ fileName } ` ;
const { error : uploadError } = await supabase . storage
. from ( "imagenes" )
. upload ( filePath , file );
if ( uploadError ) throw uploadError ;
const publicUrlData = supabase . storage
. from ( "imagenes" )
. getPublicUrl ( filePath );
const publicUrl = publicUrlData . data ?. publicUrl ?? null ;
return { success: true , url: publicUrl };
} catch ( err : any ) {
error . value = err . message ?? String ( err );
console . error ( "ā Error subiendo imagen:" , err );
return { success: false , error: err . message ?? String ( err ) };
} finally {
loading . value = false ;
}
};
Updating Report Status (Admin)
const actualizarReporte = async (
id : string ,
updates : Omit < UpdateReporte , "id" | "usuario_id" >
) => {
loading . value = true ;
error . value = null ;
try {
const { data , error : updateError } = await supabase
. from ( "reportes" )
. update ( updates )
. eq ( "id" , id )
. select ()
. single ();
if ( updateError ) throw updateError ;
// Update in local list
const index = reportes . value . findIndex (( r ) => r . id === id );
if ( index !== - 1 ) {
reportes . value [ index ] = data ;
}
if ( reporteActual . value ?. id === id ) {
reporteActual . value = data ;
}
return { success: true , data };
} catch ( err : any ) {
error . value = err . message ?? String ( err );
console . error ( "ā Error actualizando reporte:" , err );
return { success: false , error: err . message ?? String ( err ) };
} finally {
loading . value = false ;
}
};
Fetching Reports with Filters
const fetchReportes = async ( filtros ?: {
usuario_id? : string ;
estado ?: Reporte [ "estado" ];
categoria ?: Reporte [ "categoria" ];
}) => {
loading . value = true ;
error . value = null ;
try {
let query = supabase
. from ( "reportes" )
. select ( "*" )
. order ( "created_at" , { ascending: false });
if ( filtros ?. usuario_id ) {
query = query . eq ( "usuario_id" , filtros . usuario_id );
}
if ( filtros ?. estado ) {
query = query . eq ( "estado" , filtros . estado );
}
if ( filtros ?. categoria ) {
query = query . eq ( "categoria" , filtros . categoria );
}
const { data , error : fetchError } = await query ;
if ( fetchError ) throw fetchError ;
reportes . value = data || [];
return { success: true , data };
} catch ( err : any ) {
error . value = err . message ?? String ( err );
console . error ( "ā Error obteniendo reportes:" , err );
return { success: false , error: err . message ?? String ( err ) };
} finally {
loading . value = false ;
}
};
Real-Time Updates
The system supports real-time updates using Supabase Realtime:
const subscribeToReportes = () => {
const authStore = useAuthStore ();
return supabase
. channel ( "reportes-changes" )
. on (
"postgres_changes" ,
{
event: "*" ,
schema: "public" ,
table: "reportes" ,
filter: authStore . isCiudadano ()
? `usuario_id=eq. ${ authStore . user ?. id } `
: undefined ,
},
( payload ) => {
console . log ( "š” Cambio en reportes:" , payload );
if ( payload . eventType === "INSERT" ) {
reportes . value . unshift ( payload . new as Reporte );
} else if ( payload . eventType === "UPDATE" ) {
const index = reportes . value . findIndex (
( r ) => r . id === payload . new . id
);
if ( index !== - 1 ) {
reportes . value [ index ] = payload . new as Reporte ;
}
} else if ( payload . eventType === "DELETE" ) {
reportes . value = reportes . value . filter (
( r ) => r . id !== payload . old . id
);
}
}
)
. subscribe ();
};
Geolocation Integration
Reports include precise location data using the MapSelector component. See Geolocation System for details.
Location Data Structure
interface IUbicacion {
latitud : number ;
longitud : number ;
direccion : string ; // Human-readable address
barrio ?: string ;
sector ?: string ;
referencias ?: string ; // Optional landmarks
}
Usage Examples
Creating a Report
< script setup lang = "ts" >
import { ref } from "vue" ;
import { useReportesStore } from "@/stores/reportes.store" ;
import MapSelector from "@/components/maps/MapSelector.vue" ;
const reportesStore = useReportesStore ();
const nuevoReporte = ref ({
categoria: "baches" as const ,
descripcion: "" ,
latitud: 0 ,
longitud: 0 ,
direccion: "" ,
});
const ubicacion = ref ({
latitud: - 0.9536 ,
longitud: - 80.7217 ,
direccion: "" ,
});
const imagenFile = ref < File | null >( null );
const enviarReporte = async () => {
// Update coordinates from map
nuevoReporte . value . latitud = ubicacion . value . latitud ;
nuevoReporte . value . longitud = ubicacion . value . longitud ;
nuevoReporte . value . direccion = ubicacion . value . direccion ;
// Create report
const result = await reportesStore . crearReporte ( nuevoReporte . value );
if ( result . success && imagenFile . value ) {
// Upload image if provided
const uploadResult = await reportesStore . subirImagen (
imagenFile . value ,
result . data . id
);
if ( uploadResult . success ) {
// Update report with image URL
await reportesStore . actualizarReporte ( result . data . id , {
imagen_url: uploadResult . url ,
});
}
}
};
</ script >
< template >
< form @ submit . prevent = " enviarReporte " >
< select v-model = " nuevoReporte . categoria " >
< option value = "alumbrado" > Alumbrado </ option >
< option value = "baches" > Baches </ option >
< option value = "limpieza" > Limpieza </ option >
<!-- ... more categories ... -->
</ select >
< textarea
v-model = " nuevoReporte . descripcion "
placeholder = "Describe el problema..."
/ >
< input
type = "file"
accept = "image/*"
@ change = " imagenFile = $event . target . files [ 0 ] "
/>
< MapSelector
v-model : ubicacion = " ubicacion "
: required = " true "
/>
< button type = "submit" : disabled = " reportesStore . loading " >
Enviar Reporte
</ button >
</ form >
</ template >
Admin Report Management
< script setup lang = "ts" >
import { ref , onMounted } from "vue" ;
import { useReportesStore } from "@/stores/reportes.store" ;
const reportesStore = useReportesStore ();
const filtroEstado = ref < string >( "" );
onMounted ( async () => {
await reportesStore . fetchReportes ();
reportesStore . subscribeToReportes ();
});
const cambiarEstado = async ( reporteId : string , nuevoEstado : string ) => {
await reportesStore . actualizarReporte ( reporteId , {
estado: nuevoEstado as any ,
});
};
const cambiarPrioridad = async ( reporteId : string , nuevaPrioridad : string ) => {
await reportesStore . actualizarReporte ( reporteId , {
prioridad: nuevaPrioridad as any ,
});
};
</ script >
< template >
< div >
< select v-model = " filtroEstado " @ change = " fetchReportes ({ estado: filtroEstado }) " >
< option value = "" > Todos </ option >
< option value = "pendiente" > Pendientes </ option >
< option value = "en_revision" > En Revisión </ option >
< option value = "en_proceso" > En Proceso </ option >
< option value = "resuelto" > Resueltos </ option >
< option value = "rechazado" > Rechazados </ option >
</ select >
< div v-for = " reporte in reportesStore . reportes " : key = " reporte . id " >
< h3 > {{ reporte . categoria }} </ h3 >
< p > {{ reporte . descripcion }} </ p >
< img v-if = " reporte . imagen_url " : src = " reporte . imagen_url " />
< p > š {{ reporte . direccion }} </ p >
< select
: value = " reporte . estado "
@ change = " cambiarEstado ( reporte . id , $event . target . value ) "
>
< option value = "pendiente" > Pendiente </ option >
< option value = "en_revision" > En Revisión </ option >
< option value = "en_proceso" > En Proceso </ option >
< option value = "resuelto" > Resuelto </ option >
< option value = "rechazado" > Rechazado </ option >
</ select >
< select
: value = " reporte . prioridad "
@ change = " cambiarPrioridad ( reporte . id , $event . target . value ) "
>
< option value = "baja" > Baja </ option >
< option value = "media" > Media </ option >
< option value = "alta" > Alta </ option >
< option value = "urgente" > Urgente </ option >
</ select >
</ div >
</ div >
</ template >
Report State Workflow
The report lifecycle follows ISO/IEC 25010 principles (maximum 5 states):
Pendiente (Initial)
New report submitted by citizen. Awaiting admin review.
En Revisión
Administrator is evaluating the report for validity and priority.
En Proceso
Report accepted and assigned for resolution. Work in progress.
Resuelto (Terminal)
Issue successfully resolved. Citizen can be notified.
Rechazado (Terminal)
Report rejected (duplicate, invalid, or out of scope).
Best Practices
Compress images before upload to reduce storage costs and improve load times. Recommended max size: 1200px width, 85% JPEG quality.
Always validate that latitude and longitude are within Mantaās boundaries (-1.1 to -0.8, -80.9 to -80.5) before accepting reports.
Subscribe to changes when the component mounts and unsubscribe when it unmounts to prevent memory leaks.
Log detailed errors (code, message, details) to help debug Supabase issues. Use structured logging for better observability.
Storage Configuration
Configure Supabase Storage bucket policies: -- Allow authenticated users to upload images
CREATE POLICY "Users can upload report images"
ON storage . objects FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'imagenes' AND ( storage . foldername ( name ))[1] = 'reportes' );
-- Allow public read access
CREATE POLICY "Public read access for report images"
ON storage . objects FOR SELECT
TO public
USING (bucket_id = 'imagenes' AND ( storage . foldername ( name ))[1] = 'reportes' );
Geolocation MapSelector component and location services
Supabase Storage File upload and management documentation