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 News System enables administrators to publish location-targeted announcements and news articles to specific parishes (parroquias) and neighborhoods (barrios) in Manta. Citizens automatically receive news relevant to their registered location.
Architecture
Data Model
interface Noticia {
id : string ; // UUID
titulo : string ; // Title
contenido : string ; // Content (supports markdown)
imagen_url ?: string ; // Optional image URL
administrador_id : string ; // Creator admin ID
// Geolocation targeting
parroquia_destino ?: string | null ; // Target parish (null = global)
barrio_destino ?: string | null ; // Target neighborhood
created_at : string ;
updated_at : string ;
}
Targeting Logic
The system implements a three-tier targeting hierarchy:
Global News
When parroquia_destino is null, the news is visible to all users citywide.
Parish-Level News
When parroquia_destino is set but barrio_destino is null, the news is visible to all users in that parish.
Neighborhood-Level News
When both parroquia_destino and barrio_destino are set, the news is visible only to users in that specific neighborhood.
News Store (noticias.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 Noticia = Database [ "public" ][ "Tables" ][ "noticias" ][ "Row" ];
type InsertNoticia = Database [ "public" ][ "Tables" ][ "noticias" ][ "Insert" ];
export const useNoticiasStore = defineStore ( "noticias" , () => {
const noticias = ref < Noticia []>([]);
const noticiaActual = ref < Noticia | null >( null );
const loading = ref ( false );
const error = ref < string | null >( null );
// ... actions
});
Fetching News for Citizens
Citizens receive news based on their registered location:
const fetchNoticiasUsuario = async ( parroquia : string , barrio ?: string ) => {
if ( loading . value ) {
console . log ( "β³ Ya hay una carga en progreso, retornando datos actuales" );
return { success: true , data: noticias . value };
}
loading . value = true ;
error . value = null ;
try {
let query = supabase
. from ( "noticias" )
. select ( "*" )
. order ( "created_at" , { ascending: false });
if ( barrio ) {
// User has neighborhood: show global + parish + neighborhood news
query = query . or (
`parroquia_destino.is.null,` +
`and(parroquia_destino.eq. ${ parroquia } ,barrio_destino.is.null),` +
`and(parroquia_destino.eq. ${ parroquia } ,barrio_destino.eq. ${ barrio } )`
);
} else {
// User only has parish: show global + parish news (no specific neighborhood)
query = query . or (
`parroquia_destino.is.null,` +
`and(parroquia_destino.eq. ${ parroquia } ,barrio_destino.is.null)`
);
}
const { data , error : fetchError } = await query ;
if ( fetchError ) throw fetchError ;
noticias . value = data || [];
console . log ( `β
${ noticias . value . length } noticias cargadas para el usuario` );
return { success: true , data: noticias . value };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
Fetching All News (Admin)
Administrators can view all news regardless of targeting:
const fetchTodasLasNoticias = async () => {
if ( loading . value ) {
return { success: true , data: noticias . value };
}
loading . value = true ;
error . value = null ;
try {
console . log ( "π€ Administrador: cargando TODAS las noticias sin filtros" );
const { data , error : fetchError } = await supabase
. from ( "noticias" )
. select ( "*" )
. order ( "created_at" , { ascending: false });
if ( fetchError ) throw fetchError ;
noticias . value = data || [];
console . log ( `β
${ noticias . value . length } noticias cargadas (TODAS - Admin)` );
return { success: true , data: noticias . value };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
Creating News (Admin)
Only administrators can create news articles:
const crearNoticia = async ( noticia : Omit < InsertNoticia , "administrador_id" >) => {
loading . value = true ;
error . value = null ;
try {
const authStore = useAuthStore ();
if ( ! authStore . user ) throw new Error ( "Usuario no autenticado" );
// Verify user is an administrator
const { data : admin } = await supabase
. from ( "administradores" )
. select ( "id" )
. eq ( "id" , authStore . user . id )
. single ();
if ( ! admin ) throw new Error ( "Usuario no es administrador" );
const { data , error : insertError } = await supabase
. from ( "noticias" )
. insert ({
... noticia ,
administrador_id: authStore . user . id ,
})
. select ()
. single ();
if ( insertError ) throw insertError ;
noticias . value . unshift ( data );
return { success: true , data };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
Updating News
const actualizarNoticia = async (
id : string ,
updates : Partial < Omit < InsertNoticia , "administrador_id" >>
) => {
loading . value = true ;
error . value = null ;
try {
const { data , error : updateError } = await supabase
. from ( "noticias" )
. update ({ ... updates , updated_at: new Date (). toISOString () })
. eq ( "id" , id )
. select ()
. single ();
if ( updateError ) throw updateError ;
// Update in local list
const index = noticias . value . findIndex (( n ) => n . id === id );
if ( index !== - 1 ) {
noticias . value [ index ] = data ;
}
return { success: true , data };
} catch ( err : any ) {
error . value = err . message ;
return { success: false , error: err . message };
} finally {
loading . value = false ;
}
};
Geolocation Filtering
Query Builder Pattern
The system uses Supabaseβs query builder with OR conditions:
// Example: User in "Tarqui" parish, "Jocay" neighborhood
const query = supabase
. from ( "noticias" )
. select ( "*" )
. or (
// Global news (no location targeting)
`parroquia_destino.is.null,` +
// Parish-wide news for Tarqui
`and(parroquia_destino.eq.Tarqui,barrio_destino.is.null),` +
// Specific news for Jocay neighborhood in Tarqui
`and(parroquia_destino.eq.Tarqui,barrio_destino.eq.Jocay)`
)
. order ( "created_at" , { ascending: false });
Filtering Examples
Global News
Parish News
Neighborhood News
User Query
-- Visible to all users
SELECT * FROM noticias
WHERE parroquia_destino IS NULL
ORDER BY created_at DESC ;
-- Visible to all users in Tarqui parish
SELECT * FROM noticias
WHERE parroquia_destino = 'Tarqui'
AND barrio_destino IS NULL
ORDER BY created_at DESC ;
-- Visible only to users in Jocay neighborhood, Tarqui parish
SELECT * FROM noticias
WHERE parroquia_destino = 'Tarqui'
AND barrio_destino = 'Jocay'
ORDER BY created_at DESC ;
-- All news for a user in Jocay, Tarqui
SELECT * FROM noticias
WHERE parroquia_destino IS NULL -- Global
OR (parroquia_destino = 'Tarqui' AND barrio_destino IS NULL ) -- Parish
OR (parroquia_destino = 'Tarqui' AND barrio_destino = 'Jocay' ) -- Neighborhood
ORDER BY created_at DESC ;
Usage Examples
In Vue Components
< script setup lang = "ts" >
import { onMounted } from "vue" ;
import { useNoticiasStore } from "@/stores/noticias.store" ;
import { useAuthStore } from "@/stores/auth.store" ;
const noticiasStore = useNoticiasStore ();
const authStore = useAuthStore ();
onMounted ( async () => {
const usuario = authStore . usuario ;
if ( authStore . isAdministrador ()) {
// Fetch all news for admin
await noticiasStore . fetchTodasLasNoticias ();
} else if ( authStore . isCiudadano () && usuario ) {
// Fetch location-filtered news for citizen
await noticiasStore . fetchNoticiasUsuario (
usuario . parroquia ,
usuario . barrio
);
}
});
</ script >
< template >
< div v-if = " noticiasStore . loading " > Cargando noticias... </ div >
< div v-else >
< article v-for = " noticia in noticiasStore . noticias " : key = " noticia . id " >
< h2 > {{ noticia . titulo }} </ h2 >
< img v-if = " noticia . imagen_url " : src = " noticia . imagen_url " />
< p > {{ noticia . contenido }} </ p >
<!-- Show targeting info for admins -->
< div v-if = " authStore . isAdministrador () " >
< span v-if = " ! noticia . parroquia_destino " > π Global </ span >
< span v-else-if = " noticia . barrio_destino " >
π {{ noticia . parroquia_destino }} - {{ noticia . barrio_destino }}
</ span >
< span v-else >
π {{ noticia . parroquia_destino }}
</ span >
</ div >
</ article >
</ div >
</ template >
Concurrent Fetch Prevention
The store prevents concurrent API calls to improve performance:
if ( loading . value ) {
console . log ( "β³ Ya hay una carga en progreso, retornando datos actuales" );
return { success: true , data: noticias . value };
}
Query Optimization
The Supabase query uses indexed columns (parroquia_destino, barrio_destino) for optimal performance. Make sure to add indexes in your database: CREATE INDEX idx_noticias_parroquia ON noticias(parroquia_destino);
CREATE INDEX idx_noticias_barrio ON noticias(parroquia_destino, barrio_destino);
CREATE INDEX idx_noticias_created ON noticias(created_at DESC );
Best Practices
Always order by created_at DESC to show newest news first. This is critical for user experience.
Store images in Supabase Storage and reference them via imagen_url. Optimize images before upload (max 1200px width, compressed).
Concurrent Request Handling
Use the loading flag to prevent duplicate API calls when components mount simultaneously.
Always handle errors gracefully and provide user feedback. The storeβs error state can be used to display error messages.
Reports System Similar geolocation-based targeting for citizen reports
Supabase Queries Advanced query patterns and filtering