diff --git a/sigap-website/prisma/backups/20250421115005_remote_schema.sql b/sigap-website/prisma/backups/20250421115005_remote_schema.sql index b70dd19..740011a 100644 --- a/sigap-website/prisma/backups/20250421115005_remote_schema.sql +++ b/sigap-website/prisma/backups/20250421115005_remote_schema.sql @@ -68,60 +68,51 @@ CREATE TRIGGER on_auth_user_deleted BEFORE DELETE ON auth.users FOR EACH ROW EXE CREATE TRIGGER on_auth_user_updated AFTER UPDATE ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_user_update(); - drop trigger if exists "objects_delete_delete_prefix" on "storage"."objects"; drop trigger if exists "objects_insert_create_prefix" on "storage"."objects"; drop trigger if exists "objects_update_create_prefix" on "storage"."objects"; -drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"; - -drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"; - -revoke delete on table "storage"."prefixes" from "anon"; - -revoke insert on table "storage"."prefixes" from "anon"; - -revoke references on table "storage"."prefixes" from "anon"; - -revoke select on table "storage"."prefixes" from "anon"; - -revoke trigger on table "storage"."prefixes" from "anon"; - -revoke truncate on table "storage"."prefixes" from "anon"; - -revoke update on table "storage"."prefixes" from "anon"; - -revoke delete on table "storage"."prefixes" from "authenticated"; - -revoke insert on table "storage"."prefixes" from "authenticated"; - -revoke references on table "storage"."prefixes" from "authenticated"; - -revoke select on table "storage"."prefixes" from "authenticated"; - -revoke trigger on table "storage"."prefixes" from "authenticated"; - -revoke truncate on table "storage"."prefixes" from "authenticated"; - -revoke update on table "storage"."prefixes" from "authenticated"; - -revoke delete on table "storage"."prefixes" from "service_role"; - -revoke insert on table "storage"."prefixes" from "service_role"; - -revoke references on table "storage"."prefixes" from "service_role"; - -revoke select on table "storage"."prefixes" from "service_role"; - -revoke trigger on table "storage"."prefixes" from "service_role"; - -revoke truncate on table "storage"."prefixes" from "service_role"; - -revoke update on table "storage"."prefixes" from "service_role"; - -alter table "storage"."prefixes" drop constraint "prefixes_bucketId_fkey"; +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_catalog.pg_tables + WHERE schemaname = 'storage' + AND tablename = 'prefixes') THEN + + EXECUTE 'drop trigger if exists "prefixes_create_hierarchy" on "storage"."prefixes"'; + EXECUTE 'drop trigger if exists "prefixes_delete_hierarchy" on "storage"."prefixes"'; + + EXECUTE 'revoke delete on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke insert on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke references on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke select on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke trigger on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke truncate on table "storage"."prefixes" from "anon"'; + EXECUTE 'revoke update on table "storage"."prefixes" from "anon"'; + + EXECUTE 'revoke delete on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke insert on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke references on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke select on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke trigger on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke truncate on table "storage"."prefixes" from "authenticated"'; + EXECUTE 'revoke update on table "storage"."prefixes" from "authenticated"'; + + EXECUTE 'revoke delete on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke insert on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke references on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke select on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke trigger on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke truncate on table "storage"."prefixes" from "service_role"'; + EXECUTE 'revoke update on table "storage"."prefixes" from "service_role"'; + + EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_bucketId_fkey"'; + EXECUTE 'alter table "storage"."prefixes" drop constraint if exists "prefixes_pkey"'; + + EXECUTE 'drop table "storage"."prefixes"'; + END IF; +END $$; drop function if exists "storage"."add_prefixes"(_bucket_id text, _name text); @@ -147,21 +138,41 @@ drop function if exists "storage"."search_v1_optimised"(prefix text, bucketname drop function if exists "storage"."search_v2"(prefix text, bucket_name text, limits integer, levels integer, start_after text); -alter table "storage"."prefixes" drop constraint "prefixes_pkey"; +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_name_bucket_level_unique') THEN + EXECUTE 'drop index "storage"."idx_name_bucket_level_unique"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_objects_lower_name') THEN + EXECUTE 'drop index "storage"."idx_objects_lower_name"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'idx_prefixes_lower_name') THEN + EXECUTE 'drop index "storage"."idx_prefixes_lower_name"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'objects_bucket_id_level_idx') THEN + EXECUTE 'drop index "storage"."objects_bucket_id_level_idx"'; + END IF; + + IF EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = 'storage' AND indexname = 'prefixes_pkey') THEN + EXECUTE 'drop index "storage"."prefixes_pkey"'; + END IF; +END $$; -drop index if exists "storage"."idx_name_bucket_level_unique"; - -drop index if exists "storage"."idx_objects_lower_name"; - -drop index if exists "storage"."idx_prefixes_lower_name"; - -drop index if exists "storage"."objects_bucket_id_level_idx"; - -drop index if exists "storage"."prefixes_pkey"; - -drop table "storage"."prefixes"; - -alter table "storage"."objects" drop column "level"; +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'storage' + AND table_name = 'objects' + AND column_name = 'level' + ) THEN + EXECUTE 'alter table "storage"."objects" drop column "level"'; + END IF; +END $$; set check_function_bodies = off; diff --git a/sigap-website/prisma/backups/20250505150200_remote_schema.sql b/sigap-website/prisma/backups/20250505150200_remote_schema.sql new file mode 100644 index 0000000..df57f72 --- /dev/null +++ b/sigap-website/prisma/backups/20250505150200_remote_schema.sql @@ -0,0 +1,204 @@ +grant delete on table "storage"."s3_multipart_uploads" to "postgres"; + +grant insert on table "storage"."s3_multipart_uploads" to "postgres"; + +grant references on table "storage"."s3_multipart_uploads" to "postgres"; + +grant select on table "storage"."s3_multipart_uploads" to "postgres"; + +grant trigger on table "storage"."s3_multipart_uploads" to "postgres"; + +grant truncate on table "storage"."s3_multipart_uploads" to "postgres"; + +grant update on table "storage"."s3_multipart_uploads" to "postgres"; + +grant delete on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant insert on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant references on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant select on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant trigger on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant truncate on table "storage"."s3_multipart_uploads_parts" to "postgres"; + +grant update on table "storage"."s3_multipart_uploads_parts" to "postgres"; + + +-- drop type "gis"."geometry_dump"; + +-- drop type "gis"."valid_detail"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION gis.calculate_unit_incident_distances(p_unit_id character varying, p_district_id character varying DEFAULT NULL::character varying) + RETURNS TABLE(unit_code character varying, unit_name character varying, unit_lat double precision, unit_lng double precision, incident_id character varying, incident_description text, incident_lat double precision, incident_lng double precision, category_name character varying, district_name character varying, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH unit_locations AS ( + SELECT + u.code_unit, + u.name, + u.latitude, + u.longitude, + u.district_id, + ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location + FROM + units u + WHERE + (p_unit_id IS NULL OR u.code_unit = p_unit_id) + AND (p_district_id IS NULL OR u.district_id = p_district_id) + AND u.latitude IS NOT NULL + AND u.longitude IS NOT NULL + ), + incident_locations AS ( + SELECT + ci.id, + ci.description, + ci.crime_id, + ci.crime_category_id, + l.latitude, + l.longitude, + ST_SetSRID(ST_MakePoint(l.longitude, l.latitude), 4326)::geography AS location + FROM + crime_incidents ci + JOIN + locations l ON ci.location_id = l.id + WHERE + l.latitude IS NOT NULL + AND l.longitude IS NOT NULL + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ul.latitude as unit_lat, + ul.longitude as unit_lng, + il.id as incident_id, + il.description as incident_description, + il.latitude as incident_lat, + il.longitude as incident_lng, + cc.name as category_name, + d.name as district_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + JOIN + districts d ON ul.district_id = d.id + JOIN + crimes c ON c.district_id = d.id + JOIN + incident_locations il ON il.crime_id = c.id + JOIN + crime_categories cc ON il.crime_category_id = cc.id + ORDER BY + ul.code_unit, + ul.location <-> il.location; -- Use KNN operator for efficient ordering +END; +$function$ +; + +CREATE OR REPLACE FUNCTION gis.find_nearest_unit_to_incident(p_incident_id integer) + RETURNS TABLE(unit_code text, unit_name text, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + ST_SetSRID(ST_MakePoint( + (ci.locations->>'longitude')::float, + (ci.locations->>'latitude')::float + ), 4326)::geography AS location + FROM + crime_incidents ci + WHERE + ci.id = p_incident_id + AND (ci.locations->>'latitude') IS NOT NULL + AND (ci.locations->>'longitude') IS NOT NULL + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location + FROM + units u + WHERE + u.latitude IS NOT NULL + AND u.longitude IS NOT NULL + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + ORDER BY + ul.location <-> il.location + LIMIT 1; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id integer, p_max_distance_meters double precision DEFAULT 5000) + RETURNS TABLE(unit_code text, unit_name text, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + ST_SetSRID(ST_MakePoint( + (ci.locations->>'longitude')::float, + (ci.locations->>'latitude')::float + ), 4326)::geography AS location + FROM + crime_incidents ci + WHERE + ci.id = p_incident_id + AND (ci.locations->>'latitude') IS NOT NULL + AND (ci.locations->>'longitude') IS NOT NULL + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::geography AS location + FROM + units u + WHERE + u.latitude IS NOT NULL + AND u.longitude IS NOT NULL + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + WHERE + ST_DWithin(ul.location, il.location, p_max_distance_meters) + ORDER BY + ST_Distance(ul.location, il.location); +END; +$function$ +; + +-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + + diff --git a/sigap-website/prisma/backups/20250505150857_remote_schema.sql b/sigap-website/prisma/backups/20250505150857_remote_schema.sql new file mode 100644 index 0000000..bc459fe --- /dev/null +++ b/sigap-website/prisma/backups/20250505150857_remote_schema.sql @@ -0,0 +1,94 @@ +-- drop type "gis"."geometry_dump"; + +-- drop type "gis"."valid_detail"; + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION gis.find_nearest_unit(p_incident_id character varying) + RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + l.location AS location + FROM + crime_incidents ci + JOIN + locations l ON ci.location_id = l.id + WHERE + ci.id = p_incident_id + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + u.location + FROM + units u + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + ORDER BY + ul.location <-> il.location + LIMIT 1; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id character varying, p_max_distance_meters double precision DEFAULT 5000) + RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision) + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +BEGIN + RETURN QUERY + WITH incident_location AS ( + SELECT + ci.id, + l.location AS location + FROM + crime_incidents ci + JOIN + locations l ON ci.location_id = l.id + WHERE + ci.id = p_incident_id + ), + unit_locations AS ( + SELECT + u.code_unit, + u.name, + u.location + FROM + units u + ) + SELECT + ul.code_unit as unit_code, + ul.name as unit_name, + ST_Distance(ul.location, il.location) as distance_meters + FROM + unit_locations ul + CROSS JOIN + incident_location il + WHERE + ST_DWithin(ul.location, il.location, p_max_distance_meters) + ORDER BY + ST_Distance(ul.location, il.location); +END; +$function$ +; + +-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + + diff --git a/sigap-website/prisma/backups/20250505161323_remote_schema.sql b/sigap-website/prisma/backups/20250505161323_remote_schema.sql new file mode 100644 index 0000000..af7930e --- /dev/null +++ b/sigap-website/prisma/backups/20250505161323_remote_schema.sql @@ -0,0 +1,9 @@ +-- drop type "gis"."geometry_dump"; + +-- drop type "gis"."valid_detail"; + +-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry); + +-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry); + + diff --git a/sigap-website/prisma/migrations/20250505171603_add_field_phone_on_units/migration.sql b/sigap-website/prisma/migrations/20250505171603_add_field_phone_on_units/migration.sql index 59237c5..d83f8ae 100644 --- a/sigap-website/prisma/migrations/20250505171603_add_field_phone_on_units/migration.sql +++ b/sigap-website/prisma/migrations/20250505171603_add_field_phone_on_units/migration.sql @@ -1,2 +1,2 @@ -- AlterTable -ALTER TABLE "units" ADD COLUMN "phone" TEXT; +ALTER TABLE "units" ADD COLUMN "phone" TEXT; diff --git a/sigap-website/prisma/migrations/20250506075152_add_field_phone/migration.sql b/sigap-website/prisma/migrations/20250506075152_add_field_phone/migration.sql new file mode 100644 index 0000000..c86810b --- /dev/null +++ b/sigap-website/prisma/migrations/20250506075152_add_field_phone/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - You are about to drop the column `code_unit` on the `unit_statistics` table. All the data in the column will be lost. + - A unique constraint covering the columns `[unit_id,month,year]` on the table `unit_statistics` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[district_id]` on the table `units` will be added. If there are existing duplicate values, this will fail. + - Made the column `year` on table `crimes` required. This step will fail if there are existing NULL values in that column. + - Added the required column `unit_id` to the `unit_statistics` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "unit_statistics" DROP CONSTRAINT "unit_statistics_code_unit_fkey"; + +-- DropForeignKey +ALTER TABLE "units" DROP CONSTRAINT "units_district_id_fkey"; + +-- DropIndex +DROP INDEX "unit_statistics_code_unit_month_year_key"; + +-- AlterTable +ALTER TABLE "crimes" ALTER COLUMN "year" SET NOT NULL; + +-- AlterTable +ALTER TABLE "unit_statistics" DROP COLUMN "code_unit", +ADD COLUMN "unit_id" UUID NOT NULL; + +-- AlterTable +ALTER TABLE "units" ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(), +ADD CONSTRAINT "units_pkey" PRIMARY KEY ("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "unit_statistics_unit_id_month_year_key" ON "unit_statistics"("unit_id", "month", "year"); + +-- CreateIndex +CREATE UNIQUE INDEX "units_district_id_key" ON "units"("district_id"); + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "unit_statistics" ADD CONSTRAINT "unit_statistics_unit_id_fkey" FOREIGN KEY ("unit_id") REFERENCES "units"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/sigap-website/prisma/migrations/20250506080119_change_id_to_code_unit/migration.sql b/sigap-website/prisma/migrations/20250506080119_change_id_to_code_unit/migration.sql new file mode 100644 index 0000000..d506b9a --- /dev/null +++ b/sigap-website/prisma/migrations/20250506080119_change_id_to_code_unit/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the column `unit_id` on the `unit_statistics` table. All the data in the column will be lost. + - The primary key for the `units` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `units` table. All the data in the column will be lost. + - A unique constraint covering the columns `[code_unit,month,year]` on the table `unit_statistics` will be added. If there are existing duplicate values, this will fail. + - Added the required column `code_unit` to the `unit_statistics` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "unit_statistics" DROP CONSTRAINT "unit_statistics_unit_id_fkey"; + +-- DropIndex +DROP INDEX "unit_statistics_unit_id_month_year_key"; + +-- AlterTable +ALTER TABLE "unit_statistics" DROP COLUMN "unit_id", +ADD COLUMN "code_unit" VARCHAR(20) NOT NULL; + +-- AlterTable +ALTER TABLE "units" DROP CONSTRAINT "units_pkey", +DROP COLUMN "id"; + +-- CreateIndex +CREATE UNIQUE INDEX "unit_statistics_code_unit_month_year_key" ON "unit_statistics"("code_unit", "month", "year"); + +-- AddForeignKey +ALTER TABLE "unit_statistics" ADD CONSTRAINT "unit_statistics_code_unit_fkey" FOREIGN KEY ("code_unit") REFERENCES "units"("code_unit") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/sigap-website/prisma/migrations/20250506080400_make_year_optional_in_crimes/migration.sql b/sigap-website/prisma/migrations/20250506080400_make_year_optional_in_crimes/migration.sql new file mode 100644 index 0000000..f3124d0 --- /dev/null +++ b/sigap-website/prisma/migrations/20250506080400_make_year_optional_in_crimes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "crimes" ALTER COLUMN "year" DROP NOT NULL; diff --git a/sigap-website/prisma/migrations/20250506082733_make_code_unit_to_primary_key/migration.sql b/sigap-website/prisma/migrations/20250506082733_make_code_unit_to_primary_key/migration.sql new file mode 100644 index 0000000..0bf2413 --- /dev/null +++ b/sigap-website/prisma/migrations/20250506082733_make_code_unit_to_primary_key/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - Added the required column `city_id` to the `units` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "units_district_id_key"; + +-- AlterTable +ALTER TABLE "units" ADD COLUMN "city_id" VARCHAR(20) NOT NULL, +ADD CONSTRAINT "units_pkey" PRIMARY KEY ("code_unit"); + +-- AddForeignKey +ALTER TABLE "units" ADD CONSTRAINT "units_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index a446e83..f9eaa1a 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -124,6 +124,7 @@ model cities { created_at DateTime? @default(now()) @db.Timestamptz(6) updated_at DateTime? @default(now()) @db.Timestamptz(6) districts districts[] + units units[] @@index([name], map: "idx_cities_name") } @@ -260,8 +261,9 @@ model incident_logs { } model units { - code_unit String @unique @db.VarChar(20) + code_unit String @id @unique @db.VarChar(20) district_id String @db.VarChar(20) + city_id String @db.VarChar(20) name String @db.VarChar(100) description String? type unit_type @@ -274,7 +276,8 @@ model units { location Unsupported("geography") phone String? unit_statistics unit_statistics[] - districts districts @relation(fields: [district_id], references: [id]) + districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction) @@index([name], map: "idx_units_name") @@index([type], map: "idx_units_type") @@ -285,7 +288,6 @@ model units { model unit_statistics { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - code_unit String @db.VarChar(20) crime_total Int crime_cleared Int percentage Float? @@ -294,7 +296,8 @@ model unit_statistics { year Int created_at DateTime? @default(now()) @db.Timestamptz(6) updated_at DateTime? @default(now()) @db.Timestamptz(6) - unit units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) + code_unit String @db.VarChar(20) + units units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction) @@unique([code_unit, month, year]) @@index([year, month], map: "idx_unit_statistics_year_month") diff --git a/sigap-website/prisma/seeds/crime-incidents.ts b/sigap-website/prisma/seeds/crime-incidents.ts index a2f5fdb..13329d8 100644 --- a/sigap-website/prisma/seeds/crime-incidents.ts +++ b/sigap-website/prisma/seeds/crime-incidents.ts @@ -65,39 +65,83 @@ export class CrimeIncidentsSeeder { } /** - * Generates well-distributed points within a district's area + * Generates well-distributed points within a district's area with geographical constraints * @param centerLat - The center latitude of the district * @param centerLng - The center longitude of the district * @param landArea - Land area in square km * @param numPoints - Number of points to generate + * @param districtId - ID of the district for special handling + * @param districtName - Name of the district for constraints * @returns Array of {latitude, longitude, radius} points */ private generateDistributedPoints( centerLat: number, centerLng: number, landArea: number, - numPoints: number + numPoints: number, + districtId: string, + districtName: string ): Array<{ latitude: number; longitude: number; radius: number }> { const points = []; // Calculate a reasonable radius based on land area - // Using square root of area as an approximation of district "radius" const areaFactor = Math.sqrt(landArea) / 100; const baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03)); // Create a grid-based distribution for better coverage - const gridSize = Math.ceil(Math.sqrt(numPoints * 1.5)); // Slightly larger grid for variety + const gridSize = Math.ceil(Math.sqrt(numPoints * 1.5)); - // Define district bounds approximately - // 0.1 degrees is roughly 11km at equator - const estimatedDistrictRadius = Math.sqrt(landArea / Math.PI) / 111; - const bounds = { + // Define district bounds with geographical constraints + // Standard calculation for general districts + let estimatedDistrictRadius = Math.sqrt(landArea / Math.PI) / 111; + + // District-specific adjustments to avoid generating points in the ocean + const southernCoastalDistricts = [ + 'puger', + 'tempurejo', + 'ambulu', + 'gumukmas', + 'kencong', + 'wuluhan', + 'kencong', + ]; + const isCoastalDistrict = southernCoastalDistricts.some((district) => + districtName.toLowerCase().includes(district) + ); + + // Default bounds + let bounds = { minLat: centerLat - estimatedDistrictRadius, maxLat: centerLat + estimatedDistrictRadius, minLng: centerLng - estimatedDistrictRadius, maxLng: centerLng + estimatedDistrictRadius, }; + // Apply special constraints for coastal districts + if (isCoastalDistrict) { + // Shift points northward for southern coastal districts to avoid ocean + if ( + districtName.toLowerCase().includes('puger') || + districtName.toLowerCase().includes('tempurejo') + ) { + // For Puger and Tempurejo, shift more aggressively northward + bounds = { + minLat: centerLat, // Don't go south of the center + maxLat: centerLat + estimatedDistrictRadius * 1.5, // Extend more to the north + minLng: centerLng - estimatedDistrictRadius * 0.8, + maxLng: centerLng + estimatedDistrictRadius * 0.8, + }; + } else { + // For other coastal districts, shift moderately northward + bounds = { + minLat: centerLat - estimatedDistrictRadius * 0.5, // Less southward + maxLat: centerLat + estimatedDistrictRadius * 1.2, // More northward + minLng: centerLng - estimatedDistrictRadius, + maxLng: centerLng + estimatedDistrictRadius, + }; + } + } + const latStep = (bounds.maxLat - bounds.minLat) / gridSize; const lngStep = (bounds.maxLng - bounds.minLng) / gridSize; @@ -105,7 +149,7 @@ export class CrimeIncidentsSeeder { let totalPoints = 0; for (let i = 0; i < gridSize && totalPoints < numPoints; i++) { for (let j = 0; j < gridSize && totalPoints < numPoints; j++) { - // Base position within the grid cell + // Base position within the grid cell with randomness const cellLat = bounds.minLat + (i + 0.2 + Math.random() * 0.6) * latStep; const cellLng = @@ -121,22 +165,38 @@ export class CrimeIncidentsSeeder { const latitude = cellLat + (Math.random() * 2 - 1) * jitter; const longitude = cellLng + (Math.random() * 2 - 1) * jitter; - points.push({ - latitude, - longitude, - radius: distance * 111000, // Convert to meters (approx) - }); + // Ensure the point is within district boundaries + // Simple check to ensure points don't stray too far from center + if (distance <= estimatedDistrictRadius * 1.2) { + points.push({ + latitude, + longitude, + radius: distance * 111000, // Convert to meters (approx) + }); - totalPoints++; + totalPoints++; + } } } - // Add some completely random points for diversity + // If we still need more points, add some with tighter constraints while (points.length < numPoints) { - const latitude = - centerLat + (Math.random() * 2 - 1) * estimatedDistrictRadius; - const longitude = - centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius; + // For coastal districts, use more controlled distribution + let latitude, longitude; + + if (isCoastalDistrict) { + // Generate points with northward bias for coastal districts + const northBias = Math.random() * 0.7 + 0.3; // 0.3 to 1.0, favoring north + latitude = centerLat + northBias * estimatedDistrictRadius * 0.8; + longitude = + centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius * 0.8; + } else { + // Standard distribution for non-coastal districts + latitude = + centerLat + (Math.random() * 2 - 1) * estimatedDistrictRadius * 0.8; + longitude = + centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius * 0.8; + } const latDiff = latitude - centerLat; const lngDiff = longitude - centerLng; @@ -249,15 +309,17 @@ export class CrimeIncidentsSeeder { return []; } - // Generate a variable number of incidents between 10 and 25 for more variability - const numLocations = Math.floor(Math.random() * 16) + 10; // 10-25 locations + // Use the actual number of crimes instead of a random count + const numLocations = crime.number_of_crime; - // Generate distributed locations using our new utility function + // Update the call to the function in createIncidentsForCrime method: const locationPool = this.generateDistributedPoints( geo.latitude, geo.longitude, geo.land_area || 100, // Default to 100 km² if not available - numLocations + numLocations, + district.id, + district.name ); // List of common street names in Jember with more variety diff --git a/sigap-website/prisma/seeds/units.ts b/sigap-website/prisma/seeds/units.ts index 9570f36..6d7e564 100644 --- a/sigap-website/prisma/seeds/units.ts +++ b/sigap-website/prisma/seeds/units.ts @@ -32,6 +32,7 @@ interface UnitStatistics { interface CreateLocationDto { district_id: string; code_unit: string; + city_id: string; name?: string; description?: string; address?: string; @@ -101,6 +102,7 @@ export class UnitSeeder { let locationData: CreateLocationDto = { district_id: city.districts[0].id, // This will be replaced with Patrang's ID + city_id: city.id, code_unit: newId, name: `Polres ${city.name}`, description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`, @@ -163,6 +165,7 @@ export class UnitSeeder { const locationData: CreateLocationDto = { district_id: district.id, + city_id: district.city_id, code_unit: newId, name: `Polsek ${district.name}`, description: `Unit ${district.name} is categorized as POLSEK and operates in the ${district.name} area.`,