Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

8
old/supabase/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local

357
old/supabase/config.toml Normal file
View File

@ -0,0 +1,357 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "old"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
[api.tls]
# Enable HTTPS endpoints locally using a self-signed certificate.
enabled = false
# Paths to self-signed certificate pair.
# cert_path = "../certs/my-cert.pem"
# key_path = "../certs/my-key.pem"
[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 17
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100
# [db.vault]
# secret_key = "env(SECRET_VALUE)"
[db.migrations]
# If disabled, migrations will be skipped during a db push or reset.
enabled = true
# Specifies an ordered list of schema files that describe your database.
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
[db.network_restrictions]
# Enable management of network restrictions.
enabled = false
# List of IPv4 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
allowed_cidrs = ["0.0.0.0/0"]
# List of IPv6 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
allowed_cidrs_v6 = ["::/0"]
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# admin_email = "admin@email.com"
# sender_name = "Admin"
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = true
# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).
# jwt_issuer = ""
# Path to JWT signing key. DO NOT commit your signing keys file to git.
# signing_keys_path = "./signing_keys.json"
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""
[auth.rate_limit]
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
email_sent = 2
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
sms_sent = 30
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
anonymous_users = 30
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
token_refresh = 150
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
sign_in_sign_ups = 30
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
token_verifications = 30
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
web3 = 30
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
# [auth.captcha]
# enabled = true
# provider = "hcaptcha"
# secret = ""
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600
# Use a production-ready SMTP server
# [auth.email.smtp]
# enabled = true
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"
# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
# Uncomment to customize notification email template
# [auth.email.notification.password_changed]
# enabled = true
# subject = "Your password has been changed"
# content_path = "./templates/password_changed_notification.html"
[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }}"
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"
# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
# [auth.hook.before_user_created]
# enabled = true
# uri = "pg-functions://postgres/auth/before-user-created-hook"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
# Multi-factor-authentication is available to Supabase Pro plan.
[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10
# Control MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = false
verify_enabled = false
# Configure MFA via Phone Messaging
[auth.mfa.phone]
enroll_enabled = false
verify_enabled = false
otp_length = 6
template = "Your code is {{ .Code }}"
max_frequency = "5s"
# Configure MFA via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
email_optional = false
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
[auth.web3.solana]
enabled = false
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"
# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"
# Use Clerk as a third-party provider alongside Supabase Auth.
[auth.third_party.clerk]
enabled = false
# Obtain from https://clerk.com/setup/supabase
# domain = "example.clerk.accounts.dev"
# OAuth server configuration
[auth.oauth_server]
# Enable OAuth server functionality
enabled = false
# Path for OAuth consent flow UI
authorization_url_path = "/oauth/consent"
# Allow dynamic client registration
allow_dynamic_registration = false
[edge_runtime]
enabled = true
# Supported request policies: `oneshot`, `per_worker`.
# `per_worker` (default) — enables hot reload during local development.
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
policy = "per_worker"
# Port to attach the Chrome inspector for debugging edge functions.
inspector_port = 8083
# The Deno major version to use.
deno_version = 2
# [edge_runtime.secrets]
# secret_key = "env(SECRET_VALUE)"
[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"

View File

@ -0,0 +1,445 @@
-- Location: supabase/migrations/20241019215951_sibu_transportation_system.sql
-- Schema Analysis: Fresh project with no existing database objects
-- Integration Type: Complete new schema for SIBU transportation system
-- Dependencies: New transportation management module
-- 1. Extensions & Custom Types
CREATE TYPE public.route_status AS ENUM ('active', 'inactive', 'maintenance');
CREATE TYPE public.stop_type AS ENUM ('terminal', 'regular', 'express_only');
CREATE TYPE public.bus_schedule_type AS ENUM ('weekday', 'weekend', 'holiday');
-- 2. Core Tables (no foreign keys)
-- Routes table - stores bus route information
CREATE TABLE public.routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE, -- e.g., "Boquete>David", "David>Boquete"
description TEXT,
origin_city TEXT NOT NULL,
destination_city TEXT NOT NULL,
distance_km DECIMAL(6,2),
estimated_duration_minutes INTEGER,
status public.route_status DEFAULT 'active'::public.route_status,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Bus stops table
CREATE TABLE public.bus_stops (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
latitude DECIMAL(10,8) NOT NULL,
longitude DECIMAL(11,8) NOT NULL,
city TEXT NOT NULL,
address TEXT,
stop_type public.stop_type DEFAULT 'regular'::public.stop_type,
has_shelter BOOLEAN DEFAULT false,
has_seating BOOLEAN DEFAULT false,
is_accessible BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- 3. Dependent Tables (with foreign keys)
-- Route stops - junction table connecting routes to their stops
CREATE TABLE public.route_stops (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
route_id UUID REFERENCES public.routes(id) ON DELETE CASCADE,
stop_id UUID REFERENCES public.bus_stops(id) ON DELETE CASCADE,
stop_order INTEGER NOT NULL, -- Order of stop in the route (1, 2, 3...)
travel_time_minutes INTEGER, -- Time from previous stop
is_pickup_point BOOLEAN DEFAULT true,
is_dropoff_point BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Bus schedules table
CREATE TABLE public.bus_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
route_id UUID REFERENCES public.routes(id) ON DELETE CASCADE,
departure_time TIME NOT NULL,
frequency_minutes INTEGER DEFAULT 30, -- How often bus runs (every 30 minutes)
schedule_type public.bus_schedule_type DEFAULT 'weekday'::public.bus_schedule_type,
is_active BOOLEAN DEFAULT true,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Real-time bus tracking (for future implementation)
CREATE TABLE public.bus_tracking (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
route_id UUID REFERENCES public.routes(id) ON DELETE CASCADE,
current_stop_id UUID REFERENCES public.bus_stops(id) ON DELETE SET NULL,
next_stop_id UUID REFERENCES public.bus_stops(id) ON DELETE SET NULL,
estimated_arrival_time TIMESTAMPTZ,
delay_minutes INTEGER DEFAULT 0,
bus_number TEXT,
is_active BOOLEAN DEFAULT true,
last_updated TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- 4. Indexes for Performance
CREATE INDEX idx_routes_origin_destination ON public.routes(origin_city, destination_city);
CREATE INDEX idx_routes_status ON public.routes(status);
CREATE INDEX idx_bus_stops_city ON public.bus_stops(city);
CREATE INDEX idx_bus_stops_location ON public.bus_stops(latitude, longitude);
CREATE INDEX idx_route_stops_route_id ON public.route_stops(route_id);
CREATE INDEX idx_route_stops_stop_id ON public.route_stops(stop_id);
CREATE INDEX idx_route_stops_order ON public.route_stops(route_id, stop_order);
CREATE INDEX idx_bus_schedules_route_id ON public.bus_schedules(route_id);
CREATE INDEX idx_bus_schedules_departure_time ON public.bus_schedules(departure_time);
CREATE INDEX idx_bus_tracking_route_id ON public.bus_tracking(route_id);
CREATE INDEX idx_bus_tracking_active ON public.bus_tracking(is_active);
-- 5. Helper Functions (MUST BE BEFORE RLS POLICIES)
CREATE OR REPLACE FUNCTION public.calculate_next_bus_arrival(
p_route_id UUID,
p_stop_id UUID,
p_current_time TIME DEFAULT CURRENT_TIME
)
RETURNS TABLE(
next_departure TIME,
estimated_arrival_time TIMESTAMPTZ,
minutes_until_arrival INTEGER
)
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
AS $$
DECLARE
avg_speed_kmh DECIMAL := 65.0; -- Average bus speed
stop_dwell_time INTEGER := 2; -- Minutes per stop
route_distance DECIMAL;
stop_position INTEGER;
time_to_stop INTEGER;
next_schedule_time TIME;
BEGIN
-- Get the next scheduled departure
SELECT bs.departure_time INTO next_schedule_time
FROM public.bus_schedules bs
WHERE bs.route_id = p_route_id
AND bs.is_active = true
AND bs.departure_time > p_current_time
ORDER BY bs.departure_time ASC
LIMIT 1;
-- If no more schedules today, get first schedule tomorrow
IF next_schedule_time IS NULL THEN
SELECT bs.departure_time INTO next_schedule_time
FROM public.bus_schedules bs
WHERE bs.route_id = p_route_id
AND bs.is_active = true
ORDER BY bs.departure_time ASC
LIMIT 1;
END IF;
-- Get stop position in route and calculate travel time
SELECT rs.stop_order INTO stop_position
FROM public.route_stops rs
WHERE rs.route_id = p_route_id AND rs.stop_id = p_stop_id;
-- Calculate estimated travel time to this stop (simplified calculation)
time_to_stop := (stop_position - 1) * stop_dwell_time + (stop_position * 3); -- ~3 min between stops
RETURN QUERY SELECT
next_schedule_time,
(CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL)::TIMESTAMPTZ,
EXTRACT(EPOCH FROM (
(CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL) - NOW()
))::INTEGER / 60;
END;
$$;
CREATE OR REPLACE FUNCTION public.get_route_stops_ordered(p_route_id UUID)
RETURNS TABLE(
stop_id UUID,
stop_name TEXT,
latitude DECIMAL,
longitude DECIMAL,
stop_order INTEGER,
travel_time_minutes INTEGER
)
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT
bs.id,
bs.name,
bs.latitude,
bs.longitude,
rs.stop_order,
rs.travel_time_minutes
FROM public.route_stops rs
JOIN public.bus_stops bs ON rs.stop_id = bs.id
WHERE rs.route_id = p_route_id
ORDER BY rs.stop_order;
$$;
-- 6. Enable RLS
ALTER TABLE public.routes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.bus_stops ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.route_stops ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.bus_schedules ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.bus_tracking ENABLE ROW LEVEL SECURITY;
-- 7. RLS Policies (Pattern 4: Public Read for Transportation Data)
-- All transportation data is publicly readable
CREATE POLICY "public_can_read_routes" ON public.routes
FOR SELECT TO public USING (true);
CREATE POLICY "public_can_read_bus_stops" ON public.bus_stops
FOR SELECT TO public USING (true);
CREATE POLICY "public_can_read_route_stops" ON public.route_stops
FOR SELECT TO public USING (true);
CREATE POLICY "public_can_read_bus_schedules" ON public.bus_schedules
FOR SELECT TO public USING (true);
CREATE POLICY "public_can_read_bus_tracking" ON public.bus_tracking
FOR SELECT TO public USING (true);
-- Admin policies for data management (for future admin features)
CREATE POLICY "admin_manage_routes" ON public.routes
FOR ALL TO authenticated USING (false) WITH CHECK (false); -- Disabled for now
CREATE POLICY "admin_manage_bus_stops" ON public.bus_stops
FOR ALL TO authenticated USING (false) WITH CHECK (false); -- Disabled for now
-- 8. Triggers for updated_at timestamps
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$;
CREATE TRIGGER update_routes_updated_at
BEFORE UPDATE ON public.routes
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_bus_stops_updated_at
BEFORE UPDATE ON public.bus_stops
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- 9. Mock Data for SIBU Transportation System
DO $$
DECLARE
-- Route IDs
boquete_david_id UUID := gen_random_uuid();
david_boquete_id UUID := gen_random_uuid();
palmira_david_id UUID := gen_random_uuid();
david_palmira_id UUID := gen_random_uuid();
caldera_david_id UUID := gen_random_uuid();
david_caldera_id UUID := gen_random_uuid();
-- Stop IDs for Boquete area
terminal_boquete_id UUID := gen_random_uuid();
centro_boquete_id UUID := gen_random_uuid();
parque_boquete_id UUID := gen_random_uuid();
hospital_boquete_id UUID := gen_random_uuid();
escuela_boquete_id UUID := gen_random_uuid();
mercado_boquete_id UUID := gen_random_uuid();
-- Stop IDs for David area
terminal_david_id UUID := gen_random_uuid();
centro_david_id UUID := gen_random_uuid();
hospital_david_id UUID := gen_random_uuid();
mall_david_id UUID := gen_random_uuid();
-- Stop IDs for Palmira
centro_palmira_id UUID := gen_random_uuid();
escuela_palmira_id UUID := gen_random_uuid();
-- Stop IDs for Caldera
puerto_caldera_id UUID := gen_random_uuid();
centro_caldera_id UUID := gen_random_uuid();
BEGIN
-- Insert Routes
INSERT INTO public.routes (id, name, description, origin_city, destination_city, distance_km, estimated_duration_minutes, status) VALUES
(boquete_david_id, 'Boquete>David', 'Ruta desde Boquete hacia David con paradas principales', 'Boquete', 'David', 38.5, 45, 'active'),
(david_boquete_id, 'David>Boquete', 'Ruta desde David hacia Boquete con paradas principales', 'David', 'Boquete', 38.5, 45, 'active'),
(palmira_david_id, 'Palmira>David', 'Ruta desde Palmira hacia David', 'Palmira', 'David', 25.2, 35, 'active'),
(david_palmira_id, 'David>Palmira', 'Ruta desde David hacia Palmira', 'David', 'Palmira', 25.2, 35, 'active'),
(caldera_david_id, 'Caldera>David', 'Ruta desde Caldera hacia David', 'Caldera', 'David', 42.8, 50, 'active'),
(david_caldera_id, 'David>Caldera', 'Ruta desde David hacia Caldera', 'David', 'Caldera', 42.8, 50, 'active');
-- Insert Bus Stops
INSERT INTO public.bus_stops (id, name, latitude, longitude, city, address, stop_type, has_shelter, has_seating) VALUES
-- Boquete stops
(terminal_boquete_id, 'Terminal de Boquete', 8.7697, -82.4328, 'Boquete', 'Centro de Boquete', 'terminal', true, true),
(centro_boquete_id, 'Centro de Boquete', 8.7720, -82.4315, 'Boquete', 'Calle Central', 'regular', true, true),
(parque_boquete_id, 'Parque Central Boquete', 8.7705, -82.4340, 'Boquete', 'Junto al Parque Central', 'regular', false, true),
(hospital_boquete_id, 'Hospital de Boquete', 8.7680, -82.4350, 'Boquete', 'Hospital Regional', 'regular', true, true),
(escuela_boquete_id, 'Escuela Primaria Boquete', 8.7740, -82.4300, 'Boquete', 'Zona Educativa', 'regular', false, false),
(mercado_boquete_id, 'Mercado Municipal Boquete', 8.7715, -82.4365, 'Boquete', 'Mercado Central', 'regular', true, true),
-- David stops
(terminal_david_id, 'Terminal de David', 8.4177, -82.4270, 'David', 'Terminal de Transporte', 'terminal', true, true),
(centro_david_id, 'Centro de David', 8.4194, -82.4255, 'David', 'Parque Cervantes', 'regular', true, true),
(hospital_david_id, 'Hospital Chiriquí', 8.4156, -82.4289, 'David', 'Complejo Hospitalario', 'regular', true, true),
(mall_david_id, 'Chiriquí Mall', 8.4089, -82.4178, 'David', 'Centro Comercial', 'regular', true, true),
-- Palmira stops
(centro_palmira_id, 'Centro de Palmira', 8.3544, -82.3611, 'Palmira', 'Plaza Central', 'regular', true, true),
(escuela_palmira_id, 'Escuela de Palmira', 8.3567, -82.3598, 'Palmira', 'Zona Escolar', 'regular', false, true),
-- Caldera stops
(puerto_caldera_id, 'Puerto de Caldera', 8.2456, -81.7234, 'Caldera', 'Zona Portuaria', 'terminal', true, true),
(centro_caldera_id, 'Centro de Caldera', 8.2478, -81.7198, 'Caldera', 'Centro del Pueblo', 'regular', true, false);
-- Insert Route Stops (Boquete > David)
INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES
(boquete_david_id, terminal_boquete_id, 1, 0, true, false),
(boquete_david_id, centro_boquete_id, 2, 3, true, true),
(boquete_david_id, parque_boquete_id, 3, 2, true, true),
(boquete_david_id, hospital_boquete_id, 4, 3, true, true),
(boquete_david_id, mall_david_id, 5, 35, true, true),
(boquete_david_id, centro_david_id, 6, 5, true, true),
(boquete_david_id, terminal_david_id, 7, 3, false, true);
-- Insert Route Stops (David > Boquete)
INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES
(david_boquete_id, terminal_david_id, 1, 0, true, false),
(david_boquete_id, centro_david_id, 2, 3, true, true),
(david_boquete_id, mall_david_id, 3, 5, true, true),
(david_boquete_id, hospital_boquete_id, 4, 35, true, true),
(david_boquete_id, parque_boquete_id, 5, 3, true, true),
(david_boquete_id, centro_boquete_id, 6, 2, true, true),
(david_boquete_id, terminal_boquete_id, 7, 3, false, true);
-- Insert Route Stops (Palmira > David)
INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES
(palmira_david_id, centro_palmira_id, 1, 0, true, false),
(palmira_david_id, escuela_palmira_id, 2, 2, true, true),
(palmira_david_id, hospital_david_id, 3, 25, true, true),
(palmira_david_id, centro_david_id, 4, 5, true, true),
(palmira_david_id, terminal_david_id, 5, 3, false, true);
-- Insert Route Stops (David > Palmira)
INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES
(david_palmira_id, terminal_david_id, 1, 0, true, false),
(david_palmira_id, centro_david_id, 2, 3, true, true),
(david_palmira_id, hospital_david_id, 3, 5, true, true),
(david_palmira_id, escuela_palmira_id, 4, 25, true, true),
(david_palmira_id, centro_palmira_id, 5, 2, false, true);
-- Insert Route Stops (Caldera > David)
INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES
(caldera_david_id, puerto_caldera_id, 1, 0, true, false),
(caldera_david_id, centro_caldera_id, 2, 3, true, true),
(caldera_david_id, hospital_david_id, 3, 40, true, true),
(caldera_david_id, centro_david_id, 4, 5, true, true),
(caldera_david_id, terminal_david_id, 5, 3, false, true);
-- Insert Route Stops (David > Caldera)
INSERT INTO public.route_stops (route_id, stop_id, stop_order, travel_time_minutes, is_pickup_point, is_dropoff_point) VALUES
(david_caldera_id, terminal_david_id, 1, 0, true, false),
(david_caldera_id, centro_david_id, 2, 3, true, true),
(david_caldera_id, hospital_david_id, 3, 5, true, true),
(david_caldera_id, centro_caldera_id, 4, 40, true, true),
(david_caldera_id, puerto_caldera_id, 5, 3, false, true);
-- Insert Bus Schedules
INSERT INTO public.bus_schedules (route_id, departure_time, frequency_minutes, schedule_type, is_active) VALUES
-- Boquete > David (every 30 minutes from 5:00 to 20:00)
(boquete_david_id, '05:00:00', 30, 'weekday', true),
(boquete_david_id, '05:30:00', 30, 'weekday', true),
(boquete_david_id, '06:00:00', 30, 'weekday', true),
(boquete_david_id, '06:30:00', 30, 'weekday', true),
(boquete_david_id, '07:00:00', 30, 'weekday', true),
(boquete_david_id, '07:30:00', 30, 'weekday', true),
(boquete_david_id, '08:00:00', 30, 'weekday', true),
(boquete_david_id, '08:30:00', 30, 'weekday', true),
(boquete_david_id, '09:00:00', 30, 'weekday', true),
(boquete_david_id, '09:30:00', 30, 'weekday', true),
(boquete_david_id, '10:00:00', 30, 'weekday', true),
(boquete_david_id, '10:30:00', 30, 'weekday', true),
(boquete_david_id, '11:00:00', 30, 'weekday', true),
(boquete_david_id, '11:30:00', 30, 'weekday', true),
(boquete_david_id, '12:00:00', 30, 'weekday', true),
(boquete_david_id, '12:30:00', 30, 'weekday', true),
(boquete_david_id, '13:00:00', 30, 'weekday', true),
(boquete_david_id, '13:30:00', 30, 'weekday', true),
(boquete_david_id, '14:00:00', 30, 'weekday', true),
(boquete_david_id, '14:30:00', 30, 'weekday', true),
(boquete_david_id, '15:00:00', 30, 'weekday', true),
(boquete_david_id, '15:30:00', 30, 'weekday', true),
(boquete_david_id, '16:00:00', 30, 'weekday', true),
(boquete_david_id, '16:30:00', 30, 'weekday', true),
(boquete_david_id, '17:00:00', 30, 'weekday', true),
(boquete_david_id, '17:30:00', 30, 'weekday', true),
(boquete_david_id, '18:00:00', 30, 'weekday', true),
(boquete_david_id, '18:30:00', 30, 'weekday', true),
(boquete_david_id, '19:00:00', 30, 'weekday', true),
(boquete_david_id, '19:30:00', 30, 'weekday', true),
(boquete_david_id, '20:00:00', 30, 'weekday', true),
-- David > Boquete (every 30 minutes from 5:30 to 20:30)
(david_boquete_id, '05:30:00', 30, 'weekday', true),
(david_boquete_id, '06:00:00', 30, 'weekday', true),
(david_boquete_id, '06:30:00', 30, 'weekday', true),
(david_boquete_id, '07:00:00', 30, 'weekday', true),
(david_boquete_id, '07:30:00', 30, 'weekday', true),
(david_boquete_id, '08:00:00', 30, 'weekday', true),
(david_boquete_id, '08:30:00', 30, 'weekday', true),
(david_boquete_id, '09:00:00', 30, 'weekday', true),
(david_boquete_id, '09:30:00', 30, 'weekday', true),
(david_boquete_id, '10:00:00', 30, 'weekday', true),
(david_boquete_id, '10:30:00', 30, 'weekday', true),
(david_boquete_id, '11:00:00', 30, 'weekday', true),
(david_boquete_id, '11:30:00', 30, 'weekday', true),
(david_boquete_id, '12:00:00', 30, 'weekday', true),
(david_boquete_id, '12:30:00', 30, 'weekday', true),
(david_boquete_id, '13:00:00', 30, 'weekday', true),
(david_boquete_id, '13:30:00', 30, 'weekday', true),
(david_boquete_id, '14:00:00', 30, 'weekday', true),
(david_boquete_id, '14:30:00', 30, 'weekday', true),
(david_boquete_id, '15:00:00', 30, 'weekday', true),
(david_boquete_id, '15:30:00', 30, 'weekday', true),
(david_boquete_id, '16:00:00', 30, 'weekday', true),
(david_boquete_id, '16:30:00', 30, 'weekday', true),
(david_boquete_id, '17:00:00', 30, 'weekday', true),
(david_boquete_id, '17:30:00', 30, 'weekday', true),
(david_boquete_id, '18:00:00', 30, 'weekday', true),
(david_boquete_id, '18:30:00', 30, 'weekday', true),
(david_boquete_id, '19:00:00', 30, 'weekday', true),
(david_boquete_id, '19:30:00', 30, 'weekday', true),
(david_boquete_id, '20:00:00', 30, 'weekday', true),
(david_boquete_id, '20:30:00', 30, 'weekday', true),
-- Palmira routes (every 60 minutes)
(palmira_david_id, '06:00:00', 60, 'weekday', true),
(palmira_david_id, '07:00:00', 60, 'weekday', true),
(palmira_david_id, '08:00:00', 60, 'weekday', true),
(palmira_david_id, '12:00:00', 60, 'weekday', true),
(palmira_david_id, '17:00:00', 60, 'weekday', true),
(palmira_david_id, '18:00:00', 60, 'weekday', true),
(david_palmira_id, '08:00:00', 60, 'weekday', true),
(david_palmira_id, '14:00:00', 60, 'weekday', true),
(david_palmira_id, '18:00:00', 60, 'weekday', true),
-- Caldera routes (every 45 minutes)
(caldera_david_id, '05:30:00', 45, 'weekday', true),
(caldera_david_id, '06:15:00', 45, 'weekday', true),
(caldera_david_id, '07:00:00', 45, 'weekday', true),
(caldera_david_id, '07:45:00', 45, 'weekday', true),
(caldera_david_id, '16:00:00', 45, 'weekday', true),
(caldera_david_id, '17:00:00', 45, 'weekday', true),
(david_caldera_id, '09:00:00', 45, 'weekday', true),
(david_caldera_id, '15:00:00', 45, 'weekday', true),
(david_caldera_id, '18:30:00', 45, 'weekday', true);
RAISE NOTICE 'SIBU Transportation System mock data created successfully!';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Error creating mock data: %', SQLERRM;
END $$;

View File

@ -0,0 +1,362 @@
-- Fix existing SIBU transportation schema to match required structure
-- This migration updates the existing schema rather than recreating it
-- 1. First, let's add missing columns to routes table if they don't exist
DO $$
BEGIN
-- Add missing columns to routes table
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'description') THEN
ALTER TABLE public.routes ADD COLUMN description TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'origin_city') THEN
ALTER TABLE public.routes ADD COLUMN origin_city TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'destination_city') THEN
ALTER TABLE public.routes ADD COLUMN destination_city TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'distance_km') THEN
ALTER TABLE public.routes ADD COLUMN distance_km DECIMAL(6,2);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'estimated_duration_minutes') THEN
ALTER TABLE public.routes ADD COLUMN estimated_duration_minutes INTEGER;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'status') THEN
-- Create enum type if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'route_status') THEN
CREATE TYPE public.route_status AS ENUM ('active', 'inactive', 'maintenance');
END IF;
ALTER TABLE public.routes ADD COLUMN status public.route_status DEFAULT 'active'::public.route_status;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'created_at') THEN
ALTER TABLE public.routes ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'routes' AND column_name = 'updated_at') THEN
ALTER TABLE public.routes ADD COLUMN updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
END IF;
END $$;
-- 2. Add missing columns to stops table
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'city') THEN
ALTER TABLE public.stops ADD COLUMN city TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'address') THEN
ALTER TABLE public.stops ADD COLUMN address TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'stop_type') THEN
-- Create enum type if it doesn't exist
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'stop_type') THEN
CREATE TYPE public.stop_type AS ENUM ('terminal', 'regular', 'express_only');
END IF;
ALTER TABLE public.stops ADD COLUMN stop_type public.stop_type DEFAULT 'regular'::public.stop_type;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'has_shelter') THEN
ALTER TABLE public.stops ADD COLUMN has_shelter BOOLEAN DEFAULT false;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'has_seating') THEN
ALTER TABLE public.stops ADD COLUMN has_seating BOOLEAN DEFAULT false;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'is_accessible') THEN
ALTER TABLE public.stops ADD COLUMN is_accessible BOOLEAN DEFAULT false;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'created_at') THEN
ALTER TABLE public.stops ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'stops' AND column_name = 'updated_at') THEN
ALTER TABLE public.stops ADD COLUMN updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
END IF;
END $$;
-- 3. Add missing columns to route_stops table
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'id') THEN
ALTER TABLE public.route_stops ADD COLUMN id UUID DEFAULT gen_random_uuid();
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'stop_order') THEN
ALTER TABLE public.route_stops ADD COLUMN stop_order INTEGER;
-- Update stop_order from existing seq column
UPDATE public.route_stops SET stop_order = seq WHERE stop_order IS NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'travel_time_minutes') THEN
ALTER TABLE public.route_stops ADD COLUMN travel_time_minutes INTEGER;
-- Calculate travel time from dwell_sec (convert seconds to minutes)
UPDATE public.route_stops SET travel_time_minutes = COALESCE(dwell_sec / 60, 0) WHERE travel_time_minutes IS NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'is_pickup_point') THEN
ALTER TABLE public.route_stops ADD COLUMN is_pickup_point BOOLEAN DEFAULT true;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'is_dropoff_point') THEN
ALTER TABLE public.route_stops ADD COLUMN is_dropoff_point BOOLEAN DEFAULT true;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'route_stops' AND column_name = 'created_at') THEN
ALTER TABLE public.route_stops ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
END IF;
END $$;
-- 4. Create bus_schedules table based on existing timetable
CREATE TABLE IF NOT EXISTS public.bus_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
route_id TEXT REFERENCES public.routes(id) ON DELETE CASCADE,
departure_time TIME NOT NULL,
frequency_minutes INTEGER DEFAULT 30,
schedule_type TEXT DEFAULT 'weekday',
is_active BOOLEAN DEFAULT true,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- 5. Migrate data from timetable to bus_schedules
INSERT INTO public.bus_schedules (route_id, departure_time, frequency_minutes, schedule_type, is_active)
SELECT
route_id,
departure_time,
30 as frequency_minutes,
'weekday' as schedule_type,
true as is_active
FROM public.timetable
WHERE NOT EXISTS (
SELECT 1 FROM public.bus_schedules bs
WHERE bs.route_id = timetable.route_id
AND bs.departure_time = timetable.departure_time
);
-- 6. Update existing route data with proper values
UPDATE public.routes SET
description = CASE
WHEN name = 'Boquete David' THEN 'Ruta desde Boquete hacia David con paradas principales'
WHEN name = 'David Boquete' THEN 'Ruta desde David hacia Boquete con paradas principales'
ELSE 'Ruta de transporte público'
END,
origin_city = CASE
WHEN direction = 'outbound' AND name LIKE 'Boquete%' THEN 'Boquete'
WHEN direction = 'inbound' AND name LIKE 'David%' THEN 'David'
ELSE SPLIT_PART(name, ' ', 1)
END,
destination_city = CASE
WHEN direction = 'outbound' AND name LIKE '%David' THEN 'David'
WHEN direction = 'inbound' AND name LIKE '%Boquete' THEN 'Boquete'
ELSE SPLIT_PART(name, ' ', 2)
END,
distance_km = 38.5,
estimated_duration_minutes = 45,
status = 'active'::public.route_status
WHERE description IS NULL;
-- 7. Add additional routes for Panama SIBU system
INSERT INTO public.routes (id, name, description, color, direction, origin_city, destination_city, distance_km, estimated_duration_minutes, status) VALUES
('palmira-david-out', 'Palmira>David', 'Ruta desde Palmira hacia David', '#FEE715', 'outbound', 'Palmira', 'David', 25.2, 35, 'active'),
('david-palmira-in', 'David>Palmira', 'Ruta desde David hacia Palmira', '#FEE715', 'inbound', 'David', 'Palmira', 25.2, 35, 'active'),
('caldera-david-out', 'Caldera>David', 'Ruta desde Caldera hacia David', '#FEE715', 'outbound', 'Caldera', 'David', 42.8, 50, 'active'),
('david-caldera-in', 'David>Caldera', 'Ruta desde David hacia Caldera', '#FEE715', 'inbound', 'David', 'Caldera', 42.8, 50, 'active')
ON CONFLICT (id) DO NOTHING;
-- 8. Add missing bus stops for complete Panama routes
INSERT INTO public.stops (name, lat, lng, city, address, stop_type, has_shelter, has_seating) VALUES
-- Palmira stops
('Centro de Palmira', 8.3544, -82.3611, 'Palmira', 'Plaza Central', 'regular', true, true),
('Escuela de Palmira', 8.3567, -82.3598, 'Palmira', 'Zona Escolar', 'regular', false, true),
-- Caldera stops
('Puerto de Caldera', 8.2456, -81.7234, 'Caldera', 'Zona Portuaria', 'terminal', true, true),
('Centro de Caldera', 8.2478, -81.7198, 'Caldera', 'Centro del Pueblo', 'regular', true, false),
-- Additional David stops
('Terminal de David', 8.4177, -82.4270, 'David', 'Terminal de Transporte', 'terminal', true, true),
('Centro de David', 8.4194, -82.4255, 'David', 'Parque Cervantes', 'regular', true, true),
('Hospital Chiriquí', 8.4156, -82.4289, 'David', 'Complejo Hospitalario', 'regular', true, true),
('Chiriquí Mall', 8.4089, -82.4178, 'David', 'Centro Comercial', 'regular', true, true),
-- Additional Boquete stops
('Terminal de Boquete', 8.7697, -82.4328, 'Boquete', 'Centro de Boquete', 'terminal', true, true),
('Centro de Boquete', 8.7720, -82.4315, 'Boquete', 'Calle Central', 'regular', true, true),
('Parque Central Boquete', 8.7705, -82.4340, 'Boquete', 'Junto al Parque Central', 'regular', false, true),
('Hospital de Boquete', 8.7680, -82.4350, 'Boquete', 'Hospital Regional', 'regular', true, true),
('Escuela Primaria Boquete', 8.7740, -82.4300, 'Boquete', 'Zona Educativa', 'regular', false, false),
('Mercado Municipal Boquete', 8.7715, -82.4365, 'Boquete', 'Mercado Central', 'regular', true, true)
ON CONFLICT (name, lat, lng) DO NOTHING;
-- 9. Helper Functions
CREATE OR REPLACE FUNCTION public.calculate_next_bus_arrival(
p_route_id TEXT,
p_stop_id UUID,
p_current_time TIME DEFAULT CURRENT_TIME
)
RETURNS TABLE(
next_departure TIME,
estimated_arrival_time TIMESTAMPTZ,
minutes_until_arrival INTEGER
)
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
AS $$
DECLARE
stop_position INTEGER;
time_to_stop INTEGER;
next_schedule_time TIME;
BEGIN
-- Get the next scheduled departure
SELECT bs.departure_time INTO next_schedule_time
FROM public.bus_schedules bs
WHERE bs.route_id = p_route_id
AND bs.is_active = true
AND bs.departure_time > p_current_time
ORDER BY bs.departure_time ASC
LIMIT 1;
-- If no more schedules today, get first schedule tomorrow
IF next_schedule_time IS NULL THEN
SELECT bs.departure_time INTO next_schedule_time
FROM public.bus_schedules bs
WHERE bs.route_id = p_route_id
AND bs.is_active = true
ORDER BY bs.departure_time ASC
LIMIT 1;
END IF;
-- Get stop position in route and calculate travel time
SELECT rs.stop_order INTO stop_position
FROM public.route_stops rs
WHERE rs.route_id = p_route_id AND rs.stop_id = p_stop_id;
-- Calculate estimated travel time to this stop (simplified calculation)
time_to_stop := COALESCE((stop_position - 1) * 2 + (stop_position * 3), 5); -- ~3 min between stops
RETURN QUERY SELECT
next_schedule_time,
(CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL)::TIMESTAMPTZ,
EXTRACT(EPOCH FROM (
(CURRENT_DATE + next_schedule_time + (time_to_stop || ' minutes')::INTERVAL) - NOW()
))::INTEGER / 60;
END;
$$;
CREATE OR REPLACE FUNCTION public.get_route_stops_ordered(p_route_id TEXT)
RETURNS TABLE(
stop_id UUID,
stop_name TEXT,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
stop_order INTEGER,
travel_time_minutes INTEGER
)
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT
s.id,
s.name,
s.lat,
s.lng,
rs.stop_order,
rs.travel_time_minutes
FROM public.route_stops rs
JOIN public.stops s ON rs.stop_id = s.id
WHERE rs.route_id = p_route_id
ORDER BY rs.stop_order;
$$;
-- 10. Enable RLS if not already enabled
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'routes' AND rowsecurity = true) THEN
ALTER TABLE public.routes ENABLE ROW LEVEL SECURITY;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stops' AND rowsecurity = true) THEN
ALTER TABLE public.stops ENABLE ROW LEVEL SECURITY;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'route_stops' AND rowsecurity = true) THEN
ALTER TABLE public.route_stops ENABLE ROW LEVEL SECURITY;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'bus_schedules' AND rowsecurity = true) THEN
ALTER TABLE public.bus_schedules ENABLE ROW LEVEL SECURITY;
END IF;
END $$;
-- 11. Create RLS Policies (only if they don't exist)
DO $$
BEGIN
-- Routes policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'routes' AND policyname = 'public_can_read_routes') THEN
CREATE POLICY "public_can_read_routes" ON public.routes FOR SELECT TO public USING (true);
END IF;
-- Stops policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'stops' AND policyname = 'public_can_read_stops') THEN
CREATE POLICY "public_can_read_stops" ON public.stops FOR SELECT TO public USING (true);
END IF;
-- Route stops policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'route_stops' AND policyname = 'public_can_read_route_stops') THEN
CREATE POLICY "public_can_read_route_stops" ON public.route_stops FOR SELECT TO public USING (true);
END IF;
-- Bus schedules policies
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'bus_schedules' AND policyname = 'public_can_read_bus_schedules') THEN
CREATE POLICY "public_can_read_bus_schedules" ON public.bus_schedules FOR SELECT TO public USING (true);
END IF;
END $$;
-- 12. Create updated_at triggers
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_routes_updated_at') THEN
CREATE TRIGGER update_routes_updated_at
BEFORE UPDATE ON public.routes
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_stops_updated_at') THEN
CREATE TRIGGER update_stops_updated_at
BEFORE UPDATE ON public.stops
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
END IF;
END $$;
-- 13. Create missing indexes for performance
CREATE INDEX IF NOT EXISTS idx_routes_origin_destination ON public.routes(origin_city, destination_city);
CREATE INDEX IF NOT EXISTS idx_routes_status ON public.routes(status);
CREATE INDEX IF NOT EXISTS idx_stops_city ON public.stops(city);
CREATE INDEX IF NOT EXISTS idx_bus_schedules_route_id ON public.bus_schedules(route_id);
CREATE INDEX IF NOT EXISTS idx_bus_schedules_departure_time ON public.bus_schedules(departure_time);
-- 14. Final success message (wrapped in DO block to avoid syntax error)
DO $$
BEGIN
RAISE NOTICE 'SIBU Transportation System schema updated successfully!';
END $$;

View File

@ -0,0 +1,77 @@
-- Location: supabase/migrations/20250126225029_taxi_module.sql
-- Schema Analysis: Existing transportation system with routes, stops, timetable
-- Integration Type: NEW_MODULE - Adding taxi directory functionality
-- Dependencies: No direct references to existing tables (standalone module)
-- Create tables for taxi directory functionality
CREATE TABLE public.taxis (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
phone TEXT NOT NULL,
district TEXT NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Create favorites table (conditional user relationship)
CREATE TABLE public.favorite_taxis (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
taxi_id UUID REFERENCES public.taxis(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Essential indexes for performance
CREATE INDEX idx_taxis_district ON public.taxis(district);
CREATE INDEX idx_taxis_is_active ON public.taxis(is_active);
CREATE INDEX idx_taxis_name ON public.taxis(name);
CREATE INDEX idx_favorite_taxis_user_id ON public.favorite_taxis(user_id);
CREATE INDEX idx_favorite_taxis_taxi_id ON public.favorite_taxis(taxi_id);
-- Unique constraint for user-taxi favorites
CREATE UNIQUE INDEX idx_favorite_taxis_unique ON public.favorite_taxis(user_id, taxi_id);
-- Enable RLS for security
ALTER TABLE public.taxis ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.favorite_taxis ENABLE ROW LEVEL SECURITY;
-- RLS Policies using Pattern 4: Public Read, Private Write
CREATE POLICY "public_can_read_taxis"
ON public.taxis
FOR SELECT
TO public
USING (true);
-- Pattern 2: Simple User Ownership for favorites
CREATE POLICY "users_manage_own_favorite_taxis"
ON public.favorite_taxis
FOR ALL
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- Allow anonymous users to read favorites (for local storage fallback)
CREATE POLICY "public_can_read_favorite_taxis"
ON public.favorite_taxis
FOR SELECT
TO public
USING (true);
-- Sample taxi data for different districts
DO $$
BEGIN
INSERT INTO public.taxis (name, phone, district, is_active) VALUES
('Taxi Central Boquete', '+507 720-1234', 'Boquete', true),
('Taxi Flores David', '+507 775-5678', 'David', true),
('Taxi Montaña Verde', '+507 720-9101', 'Boquete', true),
('Taxi Ciudad David', '+507 775-1122', 'David', true),
('Taxi Volcán Express', '+507 771-3344', 'Volcán', true),
('Taxi Dolega Rápido', '+507 721-5566', 'Dolega', true),
('Taxi Bugaba Seguro', '+507 772-7788', 'Bugaba', true),
('Taxi Renacimiento', '+507 773-9900', 'Renacimiento', true),
('Taxi Alanje Centro', '+507 774-2233', 'Alanje', true),
('Taxi Boquerón', '+507 775-4455', 'Boquerón', true),
('Taxi Los Naranjos', '+507 720-6677', 'Boquete', true),
('Taxi San Lorenzo', '+507 775-8899', 'David', true);
END $$;

View File

@ -0,0 +1,97 @@
-- Location: supabase/migrations/20251031165808_enhance_taxi_directory.sql
-- Schema Analysis: Existing taxis table with district column, needs shift column and terminology update
-- Integration Type: PARTIAL_EXISTS - Extending existing taxi module
-- Dependencies: Existing taxis and favorite_taxis tables
-- 1. Create shift enum type for taxi shifts
CREATE TYPE public.taxi_shift AS ENUM ('day', 'evening', 'night');
-- 2. Add shift column to existing taxis table
ALTER TABLE public.taxis
ADD COLUMN shift public.taxi_shift DEFAULT 'day'::public.taxi_shift;
-- 3. Rename district column to corregimiento for terminology consistency
ALTER TABLE public.taxis
RENAME COLUMN district TO corregimiento;
-- 4. Update existing indexes to match new column names
DROP INDEX IF EXISTS idx_taxis_district;
CREATE INDEX idx_taxis_corregimiento ON public.taxis(corregimiento);
CREATE INDEX idx_taxis_shift ON public.taxis(shift);
-- 5. Add updated sample data with new structure
DO $$
DECLARE
taxi1_id UUID := gen_random_uuid();
taxi2_id UUID := gen_random_uuid();
taxi3_id UUID := gen_random_uuid();
taxi4_id UUID := gen_random_uuid();
taxi5_id UUID := gen_random_uuid();
taxi6_id UUID := gen_random_uuid();
BEGIN
-- Remove existing sample data first
DELETE FROM public.taxis WHERE phone IN ('+507 720-1234', '+507 775-5678');
-- Insert comprehensive sample data with corregimiento and shift
INSERT INTO public.taxis (id, name, phone, corregimiento, shift, is_active, created_at, updated_at) VALUES
(taxi1_id, 'Taxi Central Boquete', '+507 720-1234', 'Boquete', 'day'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(taxi2_id, 'Taxi Flores David', '+507 775-5678', 'David', 'evening'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(taxi3_id, 'Taxi Noctuno David', '+507 776-9999', 'David', 'night'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(taxi4_id, 'Taxi Diurno Chiriquí', '+507 721-4567', 'Chiriquí', 'day'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(taxi5_id, 'Taxi Tarde Boquete', '+507 722-8890', 'Boquete', 'evening'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(taxi6_id, 'Taxi Madrugada Chiriquí', '+507 723-1122', 'Chiriquí', 'night'::public.taxi_shift, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'Some taxi data already exists, skipping duplicates';
WHEN OTHERS THEN
RAISE NOTICE 'Error inserting taxi data: %', SQLERRM;
END $$;
-- 6. Create helper function for getting distinct corregimientos
CREATE OR REPLACE FUNCTION public.get_distinct_corregimientos()
RETURNS TABLE(corregimiento TEXT)
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT DISTINCT t.corregimiento::TEXT
FROM public.taxis t
WHERE t.is_active = true
ORDER BY t.corregimiento::TEXT;
$$;
-- 7. Create helper function for filtering taxis by corregimiento and shift
CREATE OR REPLACE FUNCTION public.filter_taxis_by_criteria(
selected_corregimiento TEXT DEFAULT NULL,
selected_shift TEXT DEFAULT NULL
)
RETURNS TABLE(
id UUID,
name TEXT,
phone TEXT,
corregimiento TEXT,
shift TEXT,
is_active BOOLEAN,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
)
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT
t.id,
t.name,
t.phone,
t.corregimiento::TEXT,
t.shift::TEXT,
t.is_active,
t.created_at,
t.updated_at
FROM public.taxis t
WHERE t.is_active = true
AND (selected_corregimiento IS NULL OR t.corregimiento = selected_corregimiento)
AND (selected_shift IS NULL OR t.shift::TEXT = selected_shift)
ORDER BY t.name ASC;
$$;

View File

@ -0,0 +1,119 @@
-- Schema Analysis: Existing coupons table with basic structure
-- Integration Type: extension - adding missing columns for enhanced functionality
-- Dependencies: existing coupons table
-- Add missing columns to existing coupons table
ALTER TABLE public.coupons
ADD COLUMN image_url TEXT,
ADD COLUMN category TEXT,
ADD COLUMN is_active BOOLEAN DEFAULT true,
ADD COLUMN created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP;
-- Create enum for category validation
CREATE TYPE public.coupon_category AS ENUM (
'restaurantes',
'tiendas',
'servicios',
'entretenimiento',
'salud',
'belleza'
);
-- Update category column to use enum with proper constraint
ALTER TABLE public.coupons
ALTER COLUMN category TYPE public.coupon_category USING category::public.coupon_category;
-- Add indexes for performance
CREATE INDEX idx_coupons_category ON public.coupons(category);
CREATE INDEX idx_coupons_is_active ON public.coupons(is_active);
CREATE INDEX idx_coupons_created_at ON public.coupons(created_at);
CREATE INDEX idx_coupons_valid_until ON public.coupons(valid_until);
-- Enable RLS if not already enabled
ALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;
-- RLS Policy: Public read access for active and valid coupons
CREATE POLICY "public_can_read_active_coupons"
ON public.coupons
FOR SELECT
TO public
USING (
is_active = true
AND (valid_until IS NULL OR valid_until >= CURRENT_DATE)
);
-- Mock data with proper Spanish categories
DO $$
BEGIN
-- Insert sample coupons with all required fields
INSERT INTO public.coupons (
business_name, title, description, valid_until,
image_url, category, is_active, created_at
) VALUES
(
'Restaurante El Buen Sabor',
'Descuento del 20%',
'Descuento válido en toda la carta. No aplica con otras promociones.',
'2025-01-15'::date,
'https://images.unsplash.com/photo-1647695822638-a40e238ddc39',
'restaurantes'::public.coupon_category,
true,
'2024-10-15 10:00:00'::timestamptz
),
(
'Tienda La Moderna',
'Descuento de $10',
'Descuento en compras mayores a $50. Válido en toda la tienda.',
'2024-12-31'::date,
'https://images.unsplash.com/photo-1599190118801-8e4463c6a993',
'tiendas'::public.coupon_category,
true,
'2024-10-10 14:30:00'::timestamptz
),
(
'Spa Relajación Total',
'Descuento del 30% en masajes',
'Descuento en todos los tipos de masajes. Incluye aromaterapia.',
'2024-11-30'::date,
'https://images.unsplash.com/photo-1706795033849-7ca391f007c5',
'belleza'::public.coupon_category,
true,
'2024-10-05 09:15:00'::timestamptz
),
(
'Café Montaña',
'Café gratis con postre',
'Café americano gratis con la compra de cualquier postre.',
'2024-10-25'::date,
'https://images.unsplash.com/photo-1587299103717-ca5b2db07f47',
'restaurantes'::public.coupon_category,
true,
'2024-10-12 11:20:00'::timestamptz
),
(
'Farmacia San José',
'Descuento del 15% en medicamentos',
'Descuento en medicamentos de venta libre y productos de cuidado personal.',
'2024-12-15'::date,
'https://images.unsplash.com/photo-1701117553039-d39c1ee3f3cd',
'salud'::public.coupon_category,
true,
'2024-10-08 13:45:00'::timestamptz
),
(
'Cine Boquete',
'2x1 en entradas',
'Dos entradas por el precio de una. Válido de lunes a miércoles.',
'2024-11-20'::date,
'https://images.unsplash.com/photo-1669697243629-8c8a5d6dad9e',
'entretenimiento'::public.coupon_category,
true,
'2024-10-01 08:30:00'::timestamptz
);
EXCEPTION
WHEN duplicate_object THEN
RAISE NOTICE 'Objects already exist, skipping creation';
WHEN OTHERS THEN
RAISE NOTICE 'Migration error: %', SQLERRM;
END $$;