diff --git a/sigap-website/components/app-sidebar.tsx b/sigap-website/components/app-sidebar.tsx index 9462c6c..046cb55 100644 --- a/sigap-website/components/app-sidebar.tsx +++ b/sigap-website/components/app-sidebar.tsx @@ -151,12 +151,12 @@ export function AppSidebar({ ...props }: React.ComponentProps) { title: item.title, url: item.url, icon: item.icon, - isActive: item.is_active, - items: item.sub_items.map((subItem) => ({ + isActive: item.isActive, + items: item.subItems.map((subItem) => ({ title: subItem.title, url: subItem.url, icon: subItem.icon, - isActive: subItem.is_active, + isActive: subItem.isActive, })), })); }, [navItems]); diff --git a/sigap-website/prisma/migrations/20250220204901_add_map_model_and_set_field_to_camel_case_but_snake_case_for_db/migration.sql b/sigap-website/prisma/migrations/20250220204901_add_map_model_and_set_field_to_camel_case_but_snake_case_for_db/migration.sql new file mode 100644 index 0000000..183be9a --- /dev/null +++ b/sigap-website/prisma/migrations/20250220204901_add_map_model_and_set_field_to_camel_case_but_snake_case_for_db/migration.sql @@ -0,0 +1,179 @@ +/* + Warnings: + + - Added the required column `slug` to the `nav_items` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `nav_sub_items` table without a default value. This is not possible if the table is not empty. + - Added the required column `slug` to the `nav_sub_items` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "crime_rates" AS ENUM ('low', 'medium', 'high'); + +-- CreateEnum +CREATE TYPE "crime_status" AS ENUM ('new', 'in_progress', 'resolved'); + +-- AlterTable +ALTER TABLE "contact_messages" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "nav_items" ADD COLUMN "slug" VARCHAR(255) NOT NULL, +ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "nav_sub_items" ADD COLUMN "icon" VARCHAR(100) NOT NULL, +ADD COLUMN "is_active" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "slug" VARCHAR(255) NOT NULL, +ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" DROP DEFAULT; + +-- CreateTable +CREATE TABLE "cities" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "geographic_id" UUID, + "name" VARCHAR(100) NOT NULL, + "code" VARCHAR(10) NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "cities_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "districts" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "city_id" UUID NOT NULL, + "name" VARCHAR(100) NOT NULL, + "code" VARCHAR(10) NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "districts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "geographics" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "district_id" UUID, + "latitude" DOUBLE PRECISION, + "longitude" DOUBLE PRECISION, + "land_area" DOUBLE PRECISION, + "polygon" JSONB, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "geographics_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "demographics" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "district_id" UUID, + "city_id" UUID, + "province_id" UUID, + "year" INTEGER NOT NULL, + "population" INTEGER NOT NULL, + "population_density" DOUBLE PRECISION NOT NULL, + "poverty_rate" DOUBLE PRECISION NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "demographics_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "crimes" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "district_id" UUID, + "city_id" UUID, + "year" INTEGER NOT NULL, + "number_of_crime" INTEGER NOT NULL, + "rate" "crime_rates" NOT NULL DEFAULT 'low', + "heat_map" JSONB, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "crimes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "crime_cases" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "crime_id" UUID, + "crime_category_id" UUID, + "date" TIMESTAMPTZ(6) NOT NULL, + "time" TIMESTAMPTZ(6) NOT NULL, + "location" VARCHAR(255) NOT NULL, + "latitude" DOUBLE PRECISION NOT NULL, + "longitude" DOUBLE PRECISION NOT NULL, + "description" TEXT NOT NULL, + "victim_count" INTEGER NOT NULL, + "status" "crime_status" NOT NULL DEFAULT 'new', + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "crime_cases_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "crime_categories" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "name" VARCHAR(255) NOT NULL, + "description" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + + CONSTRAINT "crime_categories_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "cities_name_idx" ON "cities"("name"); + +-- CreateIndex +CREATE INDEX "districts_name_idx" ON "districts"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "geographics_district_id_key" ON "geographics"("district_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "demographics_district_id_year_key" ON "demographics"("district_id", "year"); + +-- CreateIndex +CREATE UNIQUE INDEX "demographics_city_id_year_key" ON "demographics"("city_id", "year"); + +-- CreateIndex +CREATE UNIQUE INDEX "crimes_district_id_year_key" ON "crimes"("district_id", "year"); + +-- CreateIndex +CREATE UNIQUE INDEX "crimes_city_id_year_key" ON "crimes"("city_id", "year"); + +-- CreateIndex +CREATE INDEX "profiles_user_id_idx" ON "profiles"("user_id"); + +-- AddForeignKey +ALTER TABLE "cities" ADD CONSTRAINT "cities_geographic_id_fkey" FOREIGN KEY ("geographic_id") REFERENCES "geographics"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "districts" ADD CONSTRAINT "districts_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "geographics" ADD CONSTRAINT "geographics_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "demographics" ADD CONSTRAINT "demographics_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "demographics" ADD CONSTRAINT "demographics_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "crimes" ADD CONSTRAINT "crimes_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "crimes" ADD CONSTRAINT "crimes_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "crime_cases" ADD CONSTRAINT "crime_cases_crime_id_fkey" FOREIGN KEY ("crime_id") REFERENCES "crimes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "crime_cases" ADD CONSTRAINT "crime_cases_crime_category_id_fkey" FOREIGN KEY ("crime_category_id") REFERENCES "crime_categories"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/sigap-website/prisma/migrations/20250220212621_add_nav_sub_sub_menu/migration.sql b/sigap-website/prisma/migrations/20250220212621_add_nav_sub_sub_menu/migration.sql new file mode 100644 index 0000000..513901f --- /dev/null +++ b/sigap-website/prisma/migrations/20250220212621_add_nav_sub_sub_menu/migration.sql @@ -0,0 +1,63 @@ +-- AlterTable +ALTER TABLE "cities" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "contact_messages" ALTER COLUMN "created_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "crime_cases" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "crime_categories" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "crimes" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "demographics" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "districts" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "geographics" ALTER COLUMN "created_at" SET DEFAULT now(), +ALTER COLUMN "updated_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "nav_items" ALTER COLUMN "created_at" SET DEFAULT now(); + +-- AlterTable +ALTER TABLE "nav_sub_items" ALTER COLUMN "created_at" SET DEFAULT now(); + +-- CreateTable +CREATE TABLE "nav_sub_sub_items" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "title" VARCHAR(255) NOT NULL, + "url" VARCHAR(255) NOT NULL, + "slug" VARCHAR(255) NOT NULL, + "icon" VARCHAR(100) NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT false, + "order_seq" INTEGER NOT NULL, + "nav_sub_item_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ(6) NOT NULL, + "created_by" UUID, + "updated_by" UUID, + + CONSTRAINT "nav_sub_sub_items_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "nav_sub_sub_items_nav_sub_item_id_idx" ON "nav_sub_sub_items"("nav_sub_item_id"); + +-- CreateIndex +CREATE INDEX "nav_sub_sub_items_title_idx" ON "nav_sub_sub_items"("title"); + +-- AddForeignKey +ALTER TABLE "nav_sub_sub_items" ADD CONSTRAINT "nav_sub_sub_items_nav_sub_item_id_fkey" FOREIGN KEY ("nav_sub_item_id") REFERENCES "nav_sub_items"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index 2506c55..168d7a8 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -17,18 +17,18 @@ datasource db { } model User { - id String @id @db.Uuid - email String @unique @db.VarChar(255) - email_verified Boolean @default(false) - password String? @db.VarChar(255) - first_name String? @db.VarChar(255) - last_name String? @db.VarChar(255) - avatar String? @db.VarChar(255) - role Role @default(user) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - last_signed_in DateTime? - metadata Json? + id String @id @db.Uuid + email String @unique @db.VarChar(255) + emailVerified Boolean @default(false) @map("email_verified") + password String? @db.VarChar(255) + firstName String? @map("first_name") @db.VarChar(255) + lastName String? @map("last_name") @db.VarChar(255) + avatar String? @db.VarChar(255) + role Role @default(user) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lastSignedIn DateTime? @map("last_signed_in") + metadata Json? profile Profile? @@ -37,75 +37,237 @@ model User { } model Profile { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - user_id String @unique @db.Uuid - bio String? @db.Text - phone String? @db.VarChar(20) - address String? @db.VarChar(255) - city String? @db.VarChar(100) - country String? @db.VarChar(100) - birth_date DateTime? - user User @relation(fields: [user_id], references: [id]) + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userId String @unique @map("user_id") @db.Uuid + bio String? @db.Text + phone String? @db.VarChar(20) + address String? @db.VarChar(255) + city String? @db.VarChar(100) + country String? @db.VarChar(100) + birthDate DateTime? @map("birth_date") + user User @relation(fields: [userId], references: [id]) - @@index([user_id]) + @@index([userId]) @@map("profiles") // Maps to Supabase's 'profiles' table } -model ContactMessages { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String? @db.VarChar(255) - email String? @db.VarChar(255) - phone String? @db.VarChar(20) - message_type String? @db.VarChar(50) - message_type_label String? @db.VarChar(50) - message String? @db.Text - status StatusContactMessages @default(new) - created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6) - updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6) +model ContactMessage { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String? @db.VarChar(255) + email String? @db.VarChar(255) + phone String? @db.VarChar(20) + messageType String? @map("message_type") @db.VarChar(50) + messageTypeLabel String? @map("message_type_label") @db.VarChar(50) + message String? @db.Text + status StatusContactMessages @default(new) + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) @@map("contact_messages") // Maps to Supabase's 'contact_messages' table } -model NavItems { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - title String @db.VarChar(255) - url String @db.VarChar(255) - slug String @db.VarChar(255) - icon String @db.VarChar(100) - is_active Boolean @default(false) - order_seq Int - created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6) - updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6) - sub_items NavSubItems[] - created_by String? @db.Uuid - updated_by String? @db.Uuid +model NavItem { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + title String @db.VarChar(255) + url String @db.VarChar(255) + slug String @db.VarChar(255) + icon String @db.VarChar(100) + isActive Boolean @default(false) @map("is_active") + orderSeq Int @map("order_seq") + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + subItems NavSubItem[] + createdBy String? @map("created_by") @db.Uuid + updatedBy String? @map("updated_by") @db.Uuid @@index([title]) - @@index([is_active]) + @@index([isActive]) @@map("nav_items") } -model NavSubItems { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - title String @db.VarChar(255) - url String @db.VarChar(255) - slug String @db.VarChar(255) - icon String @db.VarChar(100) - is_active Boolean @default(false) - order_seq Int - nav_item_id String @db.Uuid - created_at DateTime @default(dbgenerated("now()")) @db.Timestamptz(6) - updated_at DateTime @default(dbgenerated("now()")) @updatedAt @db.Timestamptz(6) - created_by String? @db.Uuid - updated_by String? @db.Uuid - nav_item NavItems @relation(fields: [nav_item_id], references: [id], onDelete: Cascade) +model NavSubItem { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + title String @db.VarChar(255) + url String @db.VarChar(255) + slug String @db.VarChar(255) + icon String @db.VarChar(100) + isActive Boolean @default(false) @map("is_active") + orderSeq Int @map("order_seq") + navItemId String @map("nav_item_id") @db.Uuid + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + createdBy String? @map("created_by") @db.Uuid + updatedBy String? @map("updated_by") @db.Uuid + navItem NavItem @relation(fields: [navItemId], references: [id], onDelete: Cascade) + NavSubSubItem NavSubSubItem[] - @@index([nav_item_id]) + @@index([navItemId]) @@index([title]) @@map("nav_sub_items") } +model NavSubSubItem { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + title String @db.VarChar(255) + url String @db.VarChar(255) + slug String @db.VarChar(255) + icon String @db.VarChar(100) + isActive Boolean @default(false) @map("is_active") + orderSeq Int @map("order_seq") + navSubItemId String @map("nav_sub_item_id") @db.Uuid + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + createdBy String? @map("created_by") @db.Uuid + updatedBy String? @map("updated_by") @db.Uuid + navSubItem NavSubItem @relation(fields: [navSubItemId], references: [id], onDelete: Cascade) + @@index([navSubItemId]) + @@index([title]) + @@map("nav_sub_sub_items") +} + +model NavigationItem { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + title String @db.VarChar(255) + url String? @db.VarChar(255) + slug String @db.VarChar(255) + icon String? @db.VarChar(100) + path String @unique @db.VarChar(255) // Materialized path (e.g., "1.2.3") + level Int @default(0) + isActive Boolean @default(false) @map("is_active") + orderSeq Int @map("order_seq") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + createdBy String? @map("created_by") @db.Uuid + updatedBy String? @map("updated_by") @db.Uuid + + @@index([path]) + @@index([level]) + @@index([isActive]) + @@map("navigation_items") +} + +model City { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + geographicId String? @map("geographic_id") @db.Uuid + name String @map("name") @db.VarChar(100) + code String @map("code") @db.VarChar(10) + demographics Demographic[] + crimes Crime[] + districts District[] + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + geographic Geographic? @relation(fields: [geographicId], references: [id]) + + @@index([name]) + @@map("cities") +} + +model District { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + cityId String @map("city_id") @db.Uuid + name String @map("name") @db.VarChar(100) + code String @map("code") @db.VarChar(10) + geographic Geographic? + demographics Demographic[] + crimes Crime[] + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + city City @relation(fields: [cityId], references: [id], onDelete: Cascade) + + @@index([name]) + @@map("districts") +} + +model Geographic { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + districtId String? @unique @map("district_id") @db.Uuid + latitude Float? @map("latitude") + longitude Float? @map("longitude") + landArea Float? @map("land_area") + polygon Json? @map("polygon") + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + district District? @relation(fields: [districtId], references: [id]) + cities City[] + + @@map("geographics") +} + +model Demographic { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + districtId String? @map("district_id") @db.Uuid + cityId String? @map("city_id") @db.Uuid + provinceId String? @map("province_id") @db.Uuid + year Int @map("year") + population Int @map("population") + populationDensity Float @map("population_density") + povertyRate Float @map("poverty_rate") + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + district District? @relation(fields: [districtId], references: [id]) + city City? @relation(fields: [cityId], references: [id]) + + @@unique([districtId, year]) + @@unique([cityId, year]) + @@map("demographics") +} + +model Crime { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + districtId String? @map("district_id") @db.Uuid + cityId String? @map("city_id") @db.Uuid + year Int @map("year") + numberOfCrime Int @map("number_of_crime") + rate CrimeRate @default(low) @map("rate") + heatMap Json? @map("heat_map") + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + district District? @relation(fields: [districtId], references: [id]) + city City? @relation(fields: [cityId], references: [id]) + crimeCases CrimeCase[] + + @@unique([districtId, year]) + @@unique([cityId, year]) + @@map("crimes") +} + +model CrimeCase { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + crimeId String? @map("crime_id") @db.Uuid + crimeCategoryId String? @map("crime_category_id") @db.Uuid + date DateTime @map("date") @db.Timestamptz(6) + time DateTime @map("time") @db.Timestamptz(6) + location String @map("location") @db.VarChar(255) + latitude Float @map("latitude") + longitude Float @map("longitude") + description String @map("description") @db.Text + victimCount Int @map("victim_count") + status CrimeStatus @default(new) @map("status") + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + crime Crime? @relation(fields: [crimeId], references: [id]) + crimeCategory CrimeCategory? @relation(fields: [crimeCategoryId], references: [id]) + + @@map("crime_cases") +} + +model CrimeCategory { + id String @id @default(dbgenerated("gen_random_uuid()")) @map("id") @db.Uuid + name String @map("name") @db.VarChar(255) + description String @map("description") @db.Text + crimeCases CrimeCase[] + createdAt DateTime @default(dbgenerated("now()")) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(dbgenerated("now()")) @updatedAt @map("updated_at") @db.Timestamptz(6) + + @@map("crime_categories") +} enum Role { admin @@ -123,3 +285,19 @@ enum StatusContactMessages { @@map("status_contact_messages") } + +enum CrimeRate { + low + medium + high + + @@map("crime_rates") +} + +enum CrimeStatus { + new + inProgress @map("in_progress") + resolved + + @@map("crime_status") +} diff --git a/sigap-website/prisma/seed.ts b/sigap-website/prisma/seed.ts index ab1c27a..88f036d 100644 --- a/sigap-website/prisma/seed.ts +++ b/sigap-website/prisma/seed.ts @@ -1,61 +1,294 @@ const { PrismaClient } = require("@prisma/client"); const prisma = new PrismaClient(); +// Navigation data structure +const navItemDatas = [ + { + title: "Dashboard", + url: "/dashboard", + slug: "dashboard", + orderSeq: 1, + icon: "IconHome", + isActive: true, + subItems: [], + }, + { + title: "Crime Management", + url: "/crime-management", + slug: "crime-management", + orderSeq: 2, + icon: "IconAlertTriangle", + isActive: true, + subItems: [ + { + title: "Crime Overview", + url: "/crime-management/crime-overview", + slug: "crime-overview", + icon: "IconAlertTriangle", + orderSeq: 1, + isActive: true, + }, + { + title: "Crime Categories", + url: "/crime-management/crime-categories", + slug: "crime-categories", + icon: "IconSettings", + orderSeq: 2, + isActive: true, + }, + { + title: "Cases", + url: "/crime-management/crime-cases", + slug: "crime-cases", + icon: "IconAlertTriangle", + orderSeq: 3, + isActive: true, + subItems: [ + { + title: "New Case", + url: "/crime-management/crime-cases/case-new", + slug: "new-case", + icon: "IconAlertTriangle", + orderSeq: 1, + isActive: true, + }, + { + title: "Active Cases", + url: "/crime-management/crime-cases/case-active", + slug: "active-cases", + icon: "IconAlertTriangle", + orderSeq: 2, + isActive: true, + }, + { + title: "Resolved Cases", + url: "/crime-management/crime-cases/case-closed", + slug: "resolved-cases", + icon: "IconAlertTriangle", + orderSeq: 3, + isActive: true, + }, + ], + }, + ], + }, + { + title: "Geographic Data", + url: "/geographic-data", + slug: "geographic-data", + orderSeq: 3, + icon: "IconMap", + isActive: true, + subItems: [ + { + title: "Locations", + url: "/geographic-data/locations", + slug: "locations", + icon: "IconMap", + orderSeq: 1, + isActive: true, + subItems: [ + { + title: "Cities", + url: "/geographic-data/cities", + slug: "cities", + icon: "IconMap", + orderSeq: 1, + isActive: true, + }, + { + title: "Districts", + url: "/geographic-data/districts", + slug: "districts", + icon: "IconMap", + orderSeq: 2, + isActive: true, + }, + ], + }, + { + title: "Geographic Info", + url: "/geographic-data/geographic-info", + slug: "geographic-info", + icon: "IconMap", + orderSeq: 3, + isActive: true, + }, + ], + }, + { + title: "Demographics", + url: "/demographics", + slug: "demographics", + orderSeq: 4, + icon: "IconDatabase", + isActive: true, + subItems: [ + { + title: "Demographics Data", + url: "/demographics/demographics-data", + slug: "demographics-data", + icon: "IconDatabase", + orderSeq: 1, + isActive: true, + }, + ], + }, + { + title: "User Management", + url: "/user-management", + slug: "user-management", + orderSeq: 5, + icon: "IconUsers", + isActive: true, + subItems: [ + { + title: "Users", + url: "/user-management/users", + slug: "users", + icon: "IconUsers", + orderSeq: 1, + isActive: true, + }, + ], + }, + { + title: "Communication", + url: "/communication", + slug: "communication", + orderSeq: 6, + icon: "IconMessageCircle", + isActive: true, + subItems: [ + { + title: "Contact Messages", + url: "/communication/contact-messages", + slug: "contact-messages", + icon: "IconMessageCircle", + orderSeq: 1, + isActive: true, + }, + ], + }, + { + title: "Settings", + url: "/settings", + slug: "settings", + orderSeq: 7, + icon: "IconSettings", + isActive: true, + subItems: [ + { + title: "Navigation", + url: "/settings/navigation", + slug: "navigation", + icon: "IconMenu2", + orderSeq: 1, + isActive: true, + subItems: [ + { + title: "Nav Items", + url: "/settings/navigation/nav-items", + slug: "nav-items", + icon: "IconMenu2", + orderSeq: 1, + isActive: true, + subSubItems: [ + { + title: "Nav Sub Items", + url: "/settings/navigation/nav-sub-items", + slug: "nav-sub-items", + icon: "IconMenu2", + orderSeq: 1, + isActive: true, + }, + ], + }, + ], + }, + ], + }, +]; + +// Helper function to convert icon names +const convertIconName = (iconName: string): string => { + return iconName.replace("Icon", "").toLowerCase(); +}; + +// Helper function to create path +const createPath = (currentPath: string, orderSeq: number): string => { + return currentPath ? `${currentPath}.${orderSeq}` : `${orderSeq}`; +}; + +// Helper function to calculate level from path +const calculateLevel = (path: string): number => { + return path.split(".").length - 1; +}; + +// Helper function to process navigation items recursively +const processNavigationItems = ( + items: any[], + parentPath: string = "" +): any[] => { + const processed: any[] = []; + + items.forEach((item) => { + const currentPath = createPath(parentPath, item.orderSeq); + + const navigationItem = { + title: item.title, + url: item.url, + slug: item.slug, + icon: convertIconName(item.icon), + path: currentPath, + level: calculateLevel(currentPath), + isActive: item.isActive, + orderSeq: item.orderSeq, + }; + + processed.push(navigationItem); + + // Process subItems if they exist + if (item.subItems && item.subItems.length > 0) { + const subItems = processNavigationItems(item.subItems, currentPath); + processed.push(...subItems); + } + + // Process subSubItems if they exist (for backward compatibility) + if (item.subSubItems && item.subSubItems.length > 0) { + const subSubItems = processNavigationItems(item.subSubItems, currentPath); + processed.push(...subSubItems); + } + }); + + return processed; +}; async function main() { - // Clear existing data - await prisma.navSubItems.deleteMany({}); - await prisma.navItems.deleteMany({}); + // First, delete all existing navigation items + await prisma.navSubItem.deleteMany({}); + await prisma.navItem.deleteMany({}); + await prisma.navigationItem.deleteMany({}); - const navItemDatas = [ - { - title: "Dashboard", - url: "/dashboard", - slug: "dashboard", - order_seq: 1, - icon: "LayoutDashboard", - sub_items: [], - }, - { - title: "Master", - url: "/master", - slug: "master", - order_seq: 2, - icon: "IconDashboard", - sub_items: [ - { - title: "Users", - url: "/master/users", - slug: "users", - icon: "IconUsers ", - order_seq: 1, - }, - ], - }, - { - title: "Map", - url: "/map", - slug: "map", - order_seq: 3, - icon: "Map", - sub_items: [], - }, - ]; + // Process the navigation data + const navigationItems = processNavigationItems(navItemDatas); - // Create nav items and their sub-items - for (const navItemData of navItemDatas) { - const { sub_items, ...navItemFields } = navItemData; - - await prisma.navItems.create({ + // Create all navigation items + for (const item of navigationItems) { + await prisma.navigationItem.create({ data: { - ...navItemFields, - sub_items: { - create: sub_items, - }, + title: item.title, + url: item.url, + slug: item.slug, + icon: item.icon, + path: item.path, + level: item.level, + isActive: item.isActive, + orderSeq: item.orderSeq, }, }); } - console.log("Seed data created successfully", navItemDatas); + console.log(`Created ${navigationItems.length} navigation items`); } main() diff --git a/sigap-website/src/applications/entities/models/nav-items.model.ts b/sigap-website/src/applications/entities/models/nav-items.model.ts index 55cfbda..3564503 100644 --- a/sigap-website/src/applications/entities/models/nav-items.model.ts +++ b/sigap-website/src/applications/entities/models/nav-items.model.ts @@ -6,13 +6,13 @@ export const navSubItemsSchema = z.object({ url: z.string(), slug: z.string(), icon: z.string(), - is_active: z.boolean().default(false), - order_seq: z.number().int(), - nav_item_id: z.string().uuid(), - created_at: z.date(), - updated_at: z.date(), - created_by: z.string().uuid().nullable(), - updated_by: z.string().uuid().nullable(), + isActive: z.boolean().default(false), + orderSeq: z.number().int(), + navItemId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + createdBy: z.string().uuid().nullable(), + updatedBy: z.string().uuid().nullable(), }); export type NavSubItems = z.infer; @@ -23,13 +23,13 @@ export const navItemsSchema = z.object({ url: z.string(), slug: z.string(), icon: z.string(), - is_active: z.boolean().default(false), - order_seq: z.number().int(), - created_at: z.date(), - updated_at: z.date(), - created_by: z.string().uuid().nullable(), - updated_by: z.string().uuid().nullable(), - sub_items: z.array(navSubItemsSchema), + isActive: z.boolean().default(false), + orderSeq: z.number().int(), + createdAt: z.date(), + updatedAt: z.date(), + createdBy: z.string().uuid().nullable(), + updatedBy: z.string().uuid().nullable(), + subItems: z.array(navSubItemsSchema), }); export type NavItems = z.infer; @@ -43,9 +43,9 @@ export const navItemsInsertSchema = navItemsSchema.pick({ url: true, slug: true, icon: true, - is_active: true, - order_seq: true, - sub_items: true, + isActive: true, + orderSeq: true, + subItems: true, }); export type NavItemsInsert = z.infer; @@ -55,9 +55,9 @@ export const navItemsUpdateSchema = navItemsSchema.pick({ url: true, slug: true, icon: true, - is_active: true, - order_seq: true, - sub_items: true, + isActive: true, + orderSeq: true, + subItems: true, }); export type NavItemsUpdate = z.infer; diff --git a/sigap-website/src/applications/entities/models/navigation-item.model.ts b/sigap-website/src/applications/entities/models/navigation-item.model.ts new file mode 100644 index 0000000..4d0da05 --- /dev/null +++ b/sigap-website/src/applications/entities/models/navigation-item.model.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; + +// Define the Zod schemas +export const navigationItemSchema = z.object({ + id: z.string().uuid(), + title: z.string(), + url: z.string().nullable(), + slug: z.string(), + icon: z.string().nullable(), + path: z.string(), + level: z.number().int(), + isActive: z.boolean().default(false), + orderSeq: z.number().int(), + createdAt: z.date(), + updatedAt: z.date(), + createdBy: z.string().uuid().nullable(), + updatedBy: z.string().uuid().nullable(), +}); + +export const createNavigationItemDTOSchema = z.object({ + title: z.string(), + url: z.string(), + slug: z.string(), + icon: z.string(), + path: z.string(), + level: z.number().int(), + isActive: z.boolean().default(false), + orderSeq: z.number().int(), + createdBy: z.string().uuid().nullable(), +}); + +export const updateNavigationItemDTOSchema = z.object({ + title: z.string().optional(), + url: z.string().optional(), + slug: z.string().optional(), + icon: z.string().optional(), + path: z.string().optional(), + level: z.number().int().optional(), + isActive: z.boolean().optional(), + orderSeq: z.number().int().optional(), + updatedBy: z.string().uuid().nullable().optional(), +}); + +export const NavigationItemResponseSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), + error: z.string().optional(), + errors: z.record(z.string()).optional(), + data: navigationItemSchema, +}); + +// Infer the types from the schemas +export type NavigationItem = z.infer; +export type CreateNavigationItemDTO = z.infer< + typeof createNavigationItemDTOSchema +>; +export type UpdateNavigationItemDTO = z.infer< + typeof updateNavigationItemDTOSchema +>; +export type NavigationItemResponse = z.infer< + typeof NavigationItemResponseSchema +>; diff --git a/sigap-website/src/applications/repositories/navigation-item.repository.ts b/sigap-website/src/applications/repositories/navigation-item.repository.ts new file mode 100644 index 0000000..3292975 --- /dev/null +++ b/sigap-website/src/applications/repositories/navigation-item.repository.ts @@ -0,0 +1,15 @@ +import { + CreateNavigationItemDTO, + NavigationItem, + UpdateNavigationItemDTO, +} from "../entities/models/navigation-item.model"; + +export interface NavigationRepository { + findAll(): Promise; + findById(id: string): Promise; + findByPath(path: string): Promise; + create(data: CreateNavigationItemDTO): Promise; + update(id: string, data: UpdateNavigationItemDTO): Promise; + delete(id: string): Promise; + reorder(items: { id: string; orderSeq: number }[]): Promise; +} diff --git a/sigap-website/src/applications/usecases/navigation-item.usecase.ts b/sigap-website/src/applications/usecases/navigation-item.usecase.ts new file mode 100644 index 0000000..40bf96c --- /dev/null +++ b/sigap-website/src/applications/usecases/navigation-item.usecase.ts @@ -0,0 +1,75 @@ +// useCases/navigationUseCases.ts + +import { + CreateNavigationItemDTO, + NavigationItem, + UpdateNavigationItemDTO, +} from "../entities/models/navigation-item.model"; +import { NavigationRepository } from "../repositories/navigation-item.repository"; + +export class NavigationUseCases { + constructor(private repository: NavigationRepository) {} + + async getNavigationTree(): Promise { + return this.repository.findAll(); + } + + async getChildren(parentPath: string): Promise { + return this.repository.findByPath(parentPath); + } + + async createNavigationItem( + data: CreateNavigationItemDTO + ): Promise { + // Calculate level based on path + const level = data.path.split(".").length - 1; + return this.repository.create({ ...data, level }); + } + + async updateNavigationItem( + id: string, + data: UpdateNavigationItemDTO + ): Promise { + if (data.path) { + data.level = data.path.split(".").length - 1; + } + return this.repository.update(id, data); + } + + async deleteNavigationItem(id: string): Promise { + return this.repository.delete(id); + } + + async reorderItems(items: { id: string; orderSeq: number }[]): Promise { + return this.repository.reorder(items); + } + + async moveItem( + id: string, + newParentPath: string, + newOrderSeq: number + ): Promise { + const item = await this.repository.findById(id); + if (!item) throw new Error("Item not found"); + + const oldPath = item.path; + const newPath = newParentPath + ? `${newParentPath}.${newOrderSeq}` + : `${newOrderSeq}`; + + // Update all children paths + const children = await this.repository.findByPath(oldPath); + for (const child of children) { + const relativePath = child.path.slice(oldPath.length); + await this.repository.update(child.id, { + path: newPath + relativePath, + }); + } + + return this.repository.update(id, { + path: newPath, + orderSeq: newOrderSeq, + level: newPath.split(".").length - 1, + }); + } +} diff --git a/sigap-website/src/infrastructure/hooks/use-navigation-item.ts b/sigap-website/src/infrastructure/hooks/use-navigation-item.ts new file mode 100644 index 0000000..e87f3c7 --- /dev/null +++ b/sigap-website/src/infrastructure/hooks/use-navigation-item.ts @@ -0,0 +1,184 @@ +import { useState, useCallback, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { NavigationUseCases } from "@/src/applications/usecases/navigation-item.usecase"; +import { NavigationItem } from "@prisma/client"; +import { + NavigationItemFormData, + navigationItemSchema, +} from "../validators/navigation-item.validator"; +import { + CreateNavigationItemDTO, + UpdateNavigationItemDTO, +} from "@/src/applications/entities/models/navigation-item.model"; +import { toast } from "@/hooks/use-toast"; + +export const useNavigationItem = (navigationUseCases: NavigationUseCases) => { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [formData, setFormData] = useState({ + title: "", + url: "", + slug: "", + icon: "", + isActive: false, + orderSeq: 0, + parentId: null, + }); + const [errors, setErrors] = useState< + Partial> + >({}); + + const form = useForm({ + resolver: zodResolver(navigationItemSchema), + }); + + const loadNavigationTree = useCallback(async () => { + try { + setLoading(true); + const data = await navigationUseCases.getNavigationTree(); + setItems(data); + setError(null); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to load navigation" + ); + toast({ + title: "Error", + description: + err instanceof Error ? err.message : "Failed to load navigation", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }, [navigationUseCases]); + + const createItem = useCallback( + async (data: CreateNavigationItemDTO) => { + try { + setLoading(true); + await navigationUseCases.createNavigationItem(data); + await loadNavigationTree(); + setError(null); + toast({ + title: "Success", + description: "Navigation item created successfully", + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create item"); + toast({ + title: "Error", + description: + err instanceof Error ? err.message : "Failed to create item", + variant: "destructive", + }); + throw err; + } finally { + setLoading(false); + } + }, + [navigationUseCases, loadNavigationTree] + ); + + const updateItem = useCallback( + async (id: string, data: UpdateNavigationItemDTO) => { + try { + setLoading(true); + await navigationUseCases.updateNavigationItem(id, data); + await loadNavigationTree(); + setError(null); + toast({ + title: "Success", + description: "Navigation item updated successfully", + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to update item"); + toast({ + title: "Error", + description: + err instanceof Error ? err.message : "Failed to update item", + variant: "destructive", + }); + throw err; + } finally { + setLoading(false); + } + }, + [navigationUseCases, loadNavigationTree] + ); + + const deleteItem = useCallback( + async (id: string) => { + try { + setLoading(true); + await navigationUseCases.deleteNavigationItem(id); + await loadNavigationTree(); + setError(null); + toast({ + title: "Success", + description: "Navigation item deleted successfully", + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to delete item"); + toast({ + title: "Error", + description: + err instanceof Error ? err.message : "Failed to delete item", + variant: "destructive", + }); + throw err; + } finally { + setLoading(false); + } + }, + [navigationUseCases, loadNavigationTree] + ); + + const moveItem = useCallback( + async (id: string, newParentPath: string, newOrderSeq: number) => { + try { + setLoading(true); + await navigationUseCases.moveItem(id, newParentPath, newOrderSeq); + await loadNavigationTree(); + setError(null); + toast({ + title: "Success", + description: "Navigation item moved successfully", + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to move item"); + toast({ + title: "Error", + description: + err instanceof Error ? err.message : "Failed to move item", + variant: "destructive", + }); + throw err; + } finally { + setLoading(false); + } + }, + [navigationUseCases, loadNavigationTree] + ); + + useEffect(() => { + loadNavigationTree(); + }, [loadNavigationTree]); + + return { + items, + loading, + error, + form, + formData, + errors, + setFormData, + createItem, + updateItem, + deleteItem, + moveItem, + refresh: loadNavigationTree, + }; +}; diff --git a/sigap-website/src/infrastructure/repositories/nav-items.repository.impl.ts b/sigap-website/src/infrastructure/repositories/nav-items.repository.impl.ts index c5e18e6..0d9fce6 100644 --- a/sigap-website/src/infrastructure/repositories/nav-items.repository.impl.ts +++ b/sigap-website/src/infrastructure/repositories/nav-items.repository.impl.ts @@ -11,12 +11,12 @@ import { NavItemsRepository } from "@/src/applications/repositories/nav-items.re export class NavItemsRepositoryImpl implements NavItemsRepository { async getNavItems(): Promise { - const data = await db.navItems.findMany({ + const data = await db.navItem.findMany({ where: { - is_active: true, + isActive: true, }, include: { - sub_items: true, + subItems: true, }, }); return { @@ -41,7 +41,7 @@ export class NavItemsRepositoryImpl implements NavItemsRepository { } async deleteNavItems(id: string): Promise { - await db.navItems.delete({ + await db.navItem.delete({ where: { id, }, diff --git a/sigap-website/src/infrastructure/repositories/navigation-item.repository.impl.ts b/sigap-website/src/infrastructure/repositories/navigation-item.repository.impl.ts new file mode 100644 index 0000000..17cf52b --- /dev/null +++ b/sigap-website/src/infrastructure/repositories/navigation-item.repository.impl.ts @@ -0,0 +1,75 @@ +// repositories/navigationRepository.ts + +import db from "@/lib/db"; +import { + CreateNavigationItemDTO, + NavigationItem, + UpdateNavigationItemDTO, +} from "@/src/applications/entities/models/navigation-item.model"; +import { NavigationRepository } from "@/src/applications/repositories/navigation-item.repository"; + +export class NavigationItemRepository implements NavigationRepository { + async findAll(): Promise { + return db.navigationItem.findMany({ + orderBy: [{ path: "asc" }, { orderSeq: "asc" }], + }); + } + + async findById(id: string): Promise { + return db.navigationItem.findUnique({ + where: { id }, + }); + } + + async findByPath(path: string): Promise { + return db.navigationItem.findMany({ + where: { + path: { startsWith: path }, + }, + orderBy: [{ path: "asc" }, { orderSeq: "asc" }], + }); + } + + async create(data: CreateNavigationItemDTO): Promise { + return db.navigationItem.create({ + data, + }); + } + + async update( + id: string, + data: UpdateNavigationItemDTO + ): Promise { + return db.navigationItem.update({ + where: { id }, + data, + }); + } + + async delete(id: string): Promise { + // Get the item to be deleted + const item = await db.navigationItem.findUnique({ + where: { id }, + }); + + if (!item) return; + + // Delete all items with paths that start with itdbem's path + await db.navigationItem.deleteMany({ + where: { + path: { startsWith: item.path }, + }, + }); + } + + async reorder(items: { id: string; orderSeq: number }[]): Promise { + await db.$transaction( + items.map((item) => + db.navigationItem.update({ + where: { id: item.id }, + data: { orderSeq: item.orderSeq }, + }) + ) + ); + } +} diff --git a/sigap-website/src/infrastructure/validators/navigation-item.validator.ts b/sigap-website/src/infrastructure/validators/navigation-item.validator.ts new file mode 100644 index 0000000..c717a13 --- /dev/null +++ b/sigap-website/src/infrastructure/validators/navigation-item.validator.ts @@ -0,0 +1,39 @@ +// validators/navigationValidator.ts + +import { z } from "zod"; + +export const navigationItemSchema = z.object({ + title: z + .string() + .min(1, "Title is required") + .max(255, "Title must be less than 255 characters"), + url: z + .string() + .min(1, "URL is required") + .max(255, "URL must be less than 255 characters") + .regex(/^\//, "URL must start with /") + .regex( + /^[a-zA-Z0-9\-/_]+$/, + "URL can only contain letters, numbers, hyphens, and forward slashes" + ), + slug: z + .string() + .min(1, "Slug is required") + .max(255, "Slug must be less than 255 characters") + .regex( + /^[a-z0-9\-]+$/, + "Slug can only contain lowercase letters, numbers, and hyphens" + ), + icon: z + .string() + .min(1, "Icon is required") + .max(100, "Icon name must be less than 100 characters"), + isActive: z.boolean(), + orderSeq: z + .number() + .int("Order must be an integer") + .min(1, "Order must be greater than 0"), + parentId: z.string().uuid().optional().nullable(), +}); + +export type NavigationItemFormData = z.infer;