pearlgw il y a 2 mois
commit
3012a8a770
100 fichiers modifiés avec 6398 ajouts et 0 suppressions
  1. 4 0
      .dockerignore
  2. 22 0
      .env.example
  3. 5 0
      .gitignore
  4. 17 0
      Dockerfile
  5. 6 0
      config/config.js
  6. 10 0
      config/keycloak.js
  7. 32 0
      docker-compose.yml
  8. 14 0
      duplicate_keycloak.json
  9. 13 0
      entrypoint.sh
  10. 38 0
      index.js
  11. 14 0
      keycloak.json
  12. 1606 0
      package-lock.json
  13. 33 0
      package.json
  14. 104 0
      prisma/migrations/20250623021621_migrate_init/migration.sql
  15. 18 0
      prisma/migrations/20250623071313_update_field_user/migration.sql
  16. 18 0
      prisma/migrations/20250623091018_add_field_activity_logs/migration.sql
  17. 8 0
      prisma/migrations/20250624013629_update_table_hospital_and_user_area/migration.sql
  18. 8 0
      prisma/migrations/20250624013831_update_table/migration.sql
  19. 12 0
      prisma/migrations/20250624041254_update_field_enum_in_hospital/migration.sql
  20. 3 0
      prisma/migrations/migration_lock.toml
  21. 126 0
      prisma/schema.prisma
  22. 93 0
      prisma/seeders/DatabaseSeeder.js
  23. 49 0
      prisma/seeders/HospitalSeeder.js
  24. 30 0
      prisma/seeders/ProvinceSeeder.js
  25. 51 0
      prisma/seeders/UserSeeder.js
  26. 62 0
      prisma/seeders/city/AcehCitySeeder.js
  27. 48 0
      prisma/seeders/city/BaliCitySeeder.js
  28. 47 0
      prisma/seeders/city/BantenCitySeeder.js
  29. 47 0
      prisma/seeders/city/BengkuluCitySeeder.js
  30. 42 0
      prisma/seeders/city/DIYogyakartaCitySeeder.js
  31. 45 0
      prisma/seeders/city/DKIJakartaCitySeeder.js
  32. 45 0
      prisma/seeders/city/GorontaloCitySeeder.js
  33. 50 0
      prisma/seeders/city/JambiCitySeeder.js
  34. 66 0
      prisma/seeders/city/JawaBaratCitySeeder.js
  35. 74 0
      prisma/seeders/city/JawaTengahCitySeeder.js
  36. 77 0
      prisma/seeders/city/JawaTimurCitySeeder.js
  37. 53 0
      prisma/seeders/city/KalimantanBaratCitySeeder.js
  38. 52 0
      prisma/seeders/city/KalimantanSelatanCitySeeder.js
  39. 53 0
      prisma/seeders/city/KalimantanTengahCitySeeder.js
  40. 49 0
      prisma/seeders/city/KalimantanTimurCitySeeder.js
  41. 44 0
      prisma/seeders/city/KalimantanUtaraCitySeeder.js
  42. 46 0
      prisma/seeders/city/KepulauanBangkaBelitungCitySeeder.js
  43. 46 0
      prisma/seeders/city/KepulauanRiauCitySeeder.js
  44. 54 0
      prisma/seeders/city/LampungCitySeeder.js
  45. 50 0
      prisma/seeders/city/MalukuCitySeeder.js
  46. 49 0
      prisma/seeders/city/MalukuUtaraCitySeeder.js
  47. 49 0
      prisma/seeders/city/NusaTenggaraBaratCitySeeder.js
  48. 61 0
      prisma/seeders/city/NusaTenggaraTimurCitySeeder.js
  49. 46 0
      prisma/seeders/city/PapuaBaratCitySeeder.js
  50. 45 0
      prisma/seeders/city/PapuaBaratDayaCitySeeder.js
  51. 46 0
      prisma/seeders/city/PapuaCitySeeder.js
  52. 45 0
      prisma/seeders/city/PapuaPegununganCitySeeder.js
  53. 41 0
      prisma/seeders/city/PapuaSelatanCitySeeder.js
  54. 45 0
      prisma/seeders/city/PapuaTengahCitySeeder.js
  55. 49 0
      prisma/seeders/city/RiauCitySeeder.js
  56. 43 0
      prisma/seeders/city/SulawesiBaratCitySeeder.js
  57. 61 0
      prisma/seeders/city/SulawesiSelatanCitySeeder.js
  58. 50 0
      prisma/seeders/city/SulawesiTengahCitySeeder.js
  59. 54 0
      prisma/seeders/city/SulawesiTenggaraCitySeeder.js
  60. 52 0
      prisma/seeders/city/SulawesiUtaraCitySeeder.js
  61. 56 0
      prisma/seeders/city/SumateraBaratCitySeeder.js
  62. 54 0
      prisma/seeders/city/SumateraSelatanCitySeeder.js
  63. 70 0
      prisma/seeders/city/SumateraUtaraCitySeeder.js
  64. 60 0
      src/controllers/admin/CityController.js
  65. 60 0
      src/controllers/admin/HospitalController.js
  66. 60 0
      src/controllers/admin/ProvinceController.js
  67. 60 0
      src/controllers/admin/SalesController.js
  68. 31 0
      src/controllers/auth/AuthControllers.js
  69. 48 0
      src/controllers/sales/HospitalController.js
  70. 60 0
      src/controllers/superadmin/AdminController.js
  71. 18 0
      src/middleware/CheckRole.js
  72. 19 0
      src/middleware/ErrorHandler.js
  73. 24 0
      src/middleware/UploadImage.js
  74. 44 0
      src/middleware/VerifyJWT.js
  75. 5 0
      src/prisma/PrismaClient.js
  76. 63 0
      src/repository/admin/CityRepository.js
  77. 87 0
      src/repository/admin/HospitalRepository.js
  78. 50 0
      src/repository/admin/ProvinceRepository.js
  79. 261 0
      src/repository/admin/SalesRepository.js
  80. 87 0
      src/repository/sales/HospitalRepository.js
  81. 219 0
      src/repository/superadmin/AdminRepository.js
  82. 13 0
      src/routes/admin/CityRoute.js
  83. 14 0
      src/routes/admin/HospitalRoute.js
  84. 19 0
      src/routes/admin/ProvinceRoute.js
  85. 14 0
      src/routes/admin/SalesRoute.js
  86. 11 0
      src/routes/auth/AuthRoute.js
  87. 13 0
      src/routes/sales/HospitalRoute.js
  88. 14 0
      src/routes/superadmin/AdminRoute.js
  89. 70 0
      src/services/admin/CityService.js
  90. 148 0
      src/services/admin/HospitalService.js
  91. 56 0
      src/services/admin/ProvinceService.js
  92. 68 0
      src/services/admin/SalesService.js
  93. 152 0
      src/services/auth/AuthService.js
  94. 173 0
      src/services/sales/HospitalService.js
  95. 59 0
      src/services/superadmin/AdminService.js
  96. BIN
      src/storage/img/1750755220747-442027743.jpeg
  97. BIN
      src/storage/img/1750755241850-643108364.jpeg
  98. BIN
      src/storage/img/1750755362021-855858980.jpeg
  99. 18 0
      src/utils/HttpException.js
  100. 0 0
      src/utils/ListResponse.js

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
1
+node_modules
2
+npm-debug.log
3
+
4
+/generated/prisma

+ 22 - 0
.env.example

@@ -0,0 +1,22 @@
1
+# Environment variables declared in this file are automatically made available to Prisma.
2
+# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
3
+
4
+# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
5
+# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6
+
7
+# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres 
8
+# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the 
9
+# one found in a remote Prisma Postgres URL, does not contain any sensitive information.
10
+
11
+# Database
12
+DATABASE_URL="postgresql://postgres:postgres@localhost:5432/<namadb>"
13
+
14
+PORT=3200
15
+
16
+# Keycloak
17
+KEYCLOAK_TOKEN_URL=<url>/realms/<name_realm>/protocol/openid-connect/token
18
+CLIENT_ID=my-app
19
+CLIENT_SECRET=vUT3dbn2EfYslBYMm0cjvvA6D5bLT2mH
20
+JWT_SECRET=FF9q223buywGKYfLbu4ojSOYJ9s63G0iJFiCwSiVXB4FOm4WJQMoOF7DubqYT46v
21
+KEYCLOAK_ADMIN_URL=<url>
22
+KEYCLOAK_REALM=<name_realm>

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
1
+node_modules
2
+# Keep environment variables out of version control
3
+.env
4
+
5
+/generated/prisma

+ 17 - 0
Dockerfile

@@ -0,0 +1,17 @@
1
+FROM node:18
2
+
3
+WORKDIR /app
4
+
5
+# Install dependensi
6
+COPY package*.json ./
7
+RUN npm install
8
+
9
+# Salin semua kode
10
+COPY . .
11
+
12
+# Tambah entrypoint
13
+COPY entrypoint.sh /entrypoint.sh
14
+RUN chmod +x /entrypoint.sh
15
+
16
+# Jalankan entrypoint saat container start
17
+CMD ["/entrypoint.sh"]

+ 6 - 0
config/config.js

@@ -0,0 +1,6 @@
1
+require('dotenv').config();
2
+
3
+module.exports = {
4
+  port: process.env.PORT || 3000,
5
+  BASE_URL: process.env.BASE_URL || 'http://localhost:3200'
6
+};

+ 10 - 0
config/keycloak.js

@@ -0,0 +1,10 @@
1
+require('dotenv').config();
2
+
3
+module.exports = {
4
+    KEYCLOAK_TOKEN_URL: process.env.KEYCLOAK_TOKEN_URL,
5
+    CLIENT_ID: process.env.CLIENT_ID,
6
+    CLIENT_SECRET: process.env.CLIENT_SECRET,
7
+    JWT_SECRET: process.env.JWT_SECRET,
8
+    KEYCLOAK_ADMIN_URL: process.env.KEYCLOAK_ADMIN_URL,
9
+    KEYCLOAK_REALM: process.env.KEYCLOAK_REALM,
10
+};

+ 32 - 0
docker-compose.yml

@@ -0,0 +1,32 @@
1
+version: "3.8"
2
+
3
+services:
4
+  db:
5
+    image: postgres:15
6
+    restart: always
7
+    ports:
8
+      - "6000:5432"
9
+    environment:
10
+      POSTGRES_USER: postgres123
11
+      POSTGRES_PASSWORD: postgres123
12
+      POSTGRES_DB: db_radar
13
+    volumes:
14
+      - postgres-data:/var/lib/postgresql/data
15
+    healthcheck:
16
+      test: ["CMD-SHELL", "pg_isready -U postgres123"]
17
+      interval: 5s
18
+      timeout: 3s
19
+      retries: 5
20
+
21
+  api:
22
+    build: .
23
+    ports:
24
+      - "3200:3200"
25
+    env_file:
26
+      - .env
27
+    depends_on:
28
+      db:
29
+        condition: service_healthy
30
+
31
+volumes:
32
+  postgres-data:

+ 14 - 0
duplicate_keycloak.json

@@ -0,0 +1,14 @@
1
+{
2
+    "realm": "pearlgw",
3
+    "auth-server-url": "http://localhost:8020/",
4
+    "ssl-required": "external",
5
+    "resource": "my-app",
6
+    "verify-token-audience": true,
7
+    "credentials": {
8
+        "secret": "vUT3dbn2EfYslBYMm0cjvvA6D5bLT2mH"
9
+    },
10
+    "confidential-port": 0,
11
+    "policy-enforcer": {
12
+        "credentials": {}
13
+    }
14
+}

+ 13 - 0
entrypoint.sh

@@ -0,0 +1,13 @@
1
+#!/bin/sh
2
+
3
+echo "📦 Menjalankan Prisma generate..."
4
+npx prisma generate
5
+
6
+echo "🔃 Menjalankan migrasi dev..."
7
+npx prisma migrate dev --name init
8
+
9
+echo "♻️ Reset database dan menjalankan seeder..."
10
+npx prisma migrate reset --force
11
+
12
+echo "🚀 Menjalankan aplikasi..."
13
+exec node index.js

+ 38 - 0
index.js

@@ -0,0 +1,38 @@
1
+const express = require('express')
2
+const cors = require('cors')
3
+const errorHandler = require('./src/middleware/ErrorHandler.js')
4
+const app = express()
5
+const path = require('path');
6
+// const hospitalRoutes = require('./src/routes/HospitalRoute.js')
7
+const provinceRoutes = require('./src/routes/admin/ProvinceRoute.js')
8
+const authRoutes = require('./src/routes/auth/AuthRoute.js')
9
+// const userRoutes = require('./src/routes/UserRoute.js')
10
+const cityRoutes = require('./src/routes/admin/CityRoute.js')
11
+const salesRoutes = require('./src/routes/admin/SalesRoute.js')
12
+const adminRoutes = require('./src/routes/superadmin/AdminRoute.js')
13
+const hospitalRoutes = require('./src/routes/admin/HospitalRoute.js')
14
+const salesHospitalRoutes = require('./src/routes/sales/HospitalRoute.js')
15
+const { port } = require('./config/config.js')
16
+
17
+app.use(cors())
18
+app.use(express.json())
19
+app.use('/storage/', express.static(path.join(__dirname, 'storage/')));
20
+
21
+const apiV1 = express.Router();
22
+
23
+// apiV1.use('/hospitals', hospitalRoutes);
24
+apiV1.use('/province', provinceRoutes);
25
+apiV1.use('/auth', authRoutes);
26
+// apiV1.use('/user', userRoutes);
27
+apiV1.use('/city', cityRoutes);
28
+apiV1.use('/sales', salesRoutes);
29
+apiV1.use('/admin', adminRoutes);
30
+apiV1.use('/hospital', hospitalRoutes);
31
+apiV1.use('/hospital-area', salesHospitalRoutes);
32
+
33
+app.use('/v1/api', apiV1);
34
+app.use(errorHandler);
35
+
36
+app.listen(port, () => {
37
+    console.log(`Server started on port ${port}`);
38
+})

+ 14 - 0
keycloak.json

@@ -0,0 +1,14 @@
1
+{
2
+    "realm": "radar",
3
+    "auth-server-url": "https://keycloak.natagw.my.id/",
4
+    "ssl-required": "external",
5
+    "resource": "fg-radar",
6
+    "verify-token-audience": true,
7
+    "credentials": {
8
+        "secret": "7kVIBw6FYdHhjLRfnruLj4l8X0qlP2Yz"
9
+    },
10
+    "confidential-port": 0,
11
+    "policy-enforcer": {
12
+        "credentials": {}
13
+    }
14
+}

Fichier diff supprimé car celui-ci est trop grand
+ 1606 - 0
package-lock.json


+ 33 - 0
package.json

@@ -0,0 +1,33 @@
1
+{
2
+  "name": "proyek_radar_farmagitechs",
3
+  "version": "1.0.0",
4
+  "main": "index.js",
5
+  "scripts": {
6
+    "test": "echo \"Error: no test specified\" && exit 1"
7
+  },
8
+  "prisma": {
9
+    "seed": "node prisma/seeders/DatabaseSeeder.js"
10
+  },
11
+  "keywords": [],
12
+  "author": "",
13
+  "license": "ISC",
14
+  "description": "",
15
+  "dependencies": {
16
+    "@faker-js/faker": "^9.8.0",
17
+    "@prisma/client": "^6.10.1",
18
+    "axios": "^1.10.0",
19
+    "bcrypt": "^6.0.0",
20
+    "cors": "^2.8.5",
21
+    "dayjs": "^1.11.13",
22
+    "dayjs-plugin-utc": "^0.1.2",
23
+    "dotenv": "^16.5.0",
24
+    "express": "^5.1.0",
25
+    "jsonwebtoken": "^9.0.2",
26
+    "multer": "^2.0.1",
27
+    "pg": "^8.16.2",
28
+    "qs": "^6.14.0"
29
+  },
30
+  "devDependencies": {
31
+    "prisma": "^6.10.1"
32
+  }
33
+}

+ 104 - 0
prisma/migrations/20250623021621_migrate_init/migration.sql

@@ -0,0 +1,104 @@
1
+-- CreateTable
2
+CREATE TABLE "revoked_tokens" (
3
+    "id" TEXT NOT NULL,
4
+    "token" TEXT NOT NULL,
5
+    "revoked_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6
+
7
+    CONSTRAINT "revoked_tokens_pkey" PRIMARY KEY ("id")
8
+);
9
+
10
+-- CreateTable
11
+CREATE TABLE "users" (
12
+    "id" TEXT NOT NULL,
13
+    "username" TEXT NOT NULL,
14
+    "name" TEXT NOT NULL,
15
+    "password_hash" TEXT NOT NULL,
16
+    "role" TEXT NOT NULL,
17
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
18
+    "updatedAt" TIMESTAMP(3) NOT NULL,
19
+    "deletedAt" TIMESTAMP(3),
20
+
21
+    CONSTRAINT "users_pkey" PRIMARY KEY ("id")
22
+);
23
+
24
+-- CreateTable
25
+CREATE TABLE "provinces" (
26
+    "id" TEXT NOT NULL,
27
+    "name" TEXT NOT NULL,
28
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
29
+    "updatedAt" TIMESTAMP(3) NOT NULL,
30
+    "deletedAt" TIMESTAMP(3),
31
+
32
+    CONSTRAINT "provinces_pkey" PRIMARY KEY ("id")
33
+);
34
+
35
+-- CreateTable
36
+CREATE TABLE "cities" (
37
+    "id" TEXT NOT NULL,
38
+    "name" TEXT NOT NULL,
39
+    "province_id" TEXT NOT NULL,
40
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+    "updatedAt" TIMESTAMP(3) NOT NULL,
42
+    "deletedAt" TIMESTAMP(3),
43
+
44
+    CONSTRAINT "cities_pkey" PRIMARY KEY ("id")
45
+);
46
+
47
+-- CreateTable
48
+CREATE TABLE "hospitals" (
49
+    "id" TEXT NOT NULL,
50
+    "name" TEXT NOT NULL,
51
+    "hospital_code" TEXT,
52
+    "type" TEXT,
53
+    "ownership" TEXT,
54
+    "province_id" TEXT NOT NULL,
55
+    "city_id" TEXT NOT NULL,
56
+    "address" TEXT,
57
+    "simrs_type" TEXT,
58
+    "contact" TEXT,
59
+    "image" TEXT,
60
+    "progress_status" TEXT,
61
+    "note" TEXT,
62
+    "created_by" TEXT NOT NULL,
63
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
64
+    "updatedAt" TIMESTAMP(3) NOT NULL,
65
+    "deletedAt" TIMESTAMP(3),
66
+
67
+    CONSTRAINT "hospitals_pkey" PRIMARY KEY ("id")
68
+);
69
+
70
+-- CreateTable
71
+CREATE TABLE "user_areas" (
72
+    "id" TEXT NOT NULL,
73
+    "user_id" TEXT NOT NULL,
74
+    "province_id" TEXT NOT NULL,
75
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
76
+    "updatedAt" TIMESTAMP(3) NOT NULL,
77
+    "deletedAt" TIMESTAMP(3),
78
+
79
+    CONSTRAINT "user_areas_pkey" PRIMARY KEY ("id")
80
+);
81
+
82
+-- CreateIndex
83
+CREATE UNIQUE INDEX "provinces_name_key" ON "provinces"("name");
84
+
85
+-- CreateIndex
86
+CREATE UNIQUE INDEX "cities_name_province_id_key" ON "cities"("name", "province_id");
87
+
88
+-- AddForeignKey
89
+ALTER TABLE "cities" ADD CONSTRAINT "cities_province_id_fkey" FOREIGN KEY ("province_id") REFERENCES "provinces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
90
+
91
+-- AddForeignKey
92
+ALTER TABLE "hospitals" ADD CONSTRAINT "hospitals_province_id_fkey" FOREIGN KEY ("province_id") REFERENCES "provinces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
93
+
94
+-- AddForeignKey
95
+ALTER TABLE "hospitals" ADD CONSTRAINT "hospitals_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "cities"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
96
+
97
+-- AddForeignKey
98
+ALTER TABLE "hospitals" ADD CONSTRAINT "hospitals_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
99
+
100
+-- AddForeignKey
101
+ALTER TABLE "user_areas" ADD CONSTRAINT "user_areas_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
102
+
103
+-- AddForeignKey
104
+ALTER TABLE "user_areas" ADD CONSTRAINT "user_areas_province_id_fkey" FOREIGN KEY ("province_id") REFERENCES "provinces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 18 - 0
prisma/migrations/20250623071313_update_field_user/migration.sql

@@ -0,0 +1,18 @@
1
+/*
2
+  Warnings:
3
+
4
+  - You are about to drop the column `name` on the `users` table. All the data in the column will be lost.
5
+  - You are about to drop the column `password_hash` on the `users` table. All the data in the column will be lost.
6
+  - Added the required column `email` to the `users` table without a default value. This is not possible if the table is not empty.
7
+  - Added the required column `firstname` to the `users` table without a default value. This is not possible if the table is not empty.
8
+  - Added the required column `lastname` to the `users` table without a default value. This is not possible if the table is not empty.
9
+  - Added the required column `password` to the `users` table without a default value. This is not possible if the table is not empty.
10
+
11
+*/
12
+-- AlterTable
13
+ALTER TABLE "users" DROP COLUMN "name",
14
+DROP COLUMN "password_hash",
15
+ADD COLUMN     "email" TEXT NOT NULL,
16
+ADD COLUMN     "firstname" TEXT NOT NULL,
17
+ADD COLUMN     "lastname" TEXT NOT NULL,
18
+ADD COLUMN     "password" TEXT NOT NULL;

+ 18 - 0
prisma/migrations/20250623091018_add_field_activity_logs/migration.sql

@@ -0,0 +1,18 @@
1
+-- AlterTable
2
+ALTER TABLE "hospitals" ADD COLUMN     "logActivityId" TEXT;
3
+
4
+-- AlterTable
5
+ALTER TABLE "user_areas" ADD COLUMN     "logActivityId" TEXT;
6
+
7
+-- CreateTable
8
+CREATE TABLE "activity_logs" (
9
+    "id" TEXT NOT NULL,
10
+    "user_id" TEXT NOT NULL,
11
+    "username" TEXT NOT NULL,
12
+    "action" TEXT NOT NULL,
13
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+    "updatedAt" TIMESTAMP(3) NOT NULL,
15
+    "deletedAt" TIMESTAMP(3),
16
+
17
+    CONSTRAINT "activity_logs_pkey" PRIMARY KEY ("id")
18
+);

+ 8 - 0
prisma/migrations/20250624013629_update_table_hospital_and_user_area/migration.sql

@@ -0,0 +1,8 @@
1
+/*
2
+  Warnings:
3
+
4
+  - You are about to drop the column `logActivityId` on the `hospitals` table. All the data in the column will be lost.
5
+
6
+*/
7
+-- AlterTable
8
+ALTER TABLE "hospitals" DROP COLUMN "logActivityId";

+ 8 - 0
prisma/migrations/20250624013831_update_table/migration.sql

@@ -0,0 +1,8 @@
1
+/*
2
+  Warnings:
3
+
4
+  - You are about to drop the column `logActivityId` on the `user_areas` table. All the data in the column will be lost.
5
+
6
+*/
7
+-- AlterTable
8
+ALTER TABLE "user_areas" DROP COLUMN "logActivityId";

+ 12 - 0
prisma/migrations/20250624041254_update_field_enum_in_hospital/migration.sql

@@ -0,0 +1,12 @@
1
+/*
2
+  Warnings:
3
+
4
+  - Added the required column `progress_status` to the `hospitals` table without a default value. This is not possible if the table is not empty.
5
+
6
+*/
7
+-- CreateEnum
8
+CREATE TYPE "ProgressStatus" AS ENUM ('cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat');
9
+
10
+-- AlterTable
11
+ALTER TABLE "hospitals" DROP COLUMN "progress_status",
12
+ADD COLUMN     "progress_status" "ProgressStatus" NOT NULL;

+ 3 - 0
prisma/migrations/migration_lock.toml

@@ -0,0 +1,3 @@
1
+# Please do not edit this file manually
2
+# It should be added in your version-control system (e.g., Git)
3
+provider = "postgresql"

+ 126 - 0
prisma/schema.prisma

@@ -0,0 +1,126 @@
1
+// This is your Prisma schema file,
2
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5
+// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
+
7
+generator client {
8
+  provider = "prisma-client-js"
9
+}
10
+
11
+datasource db {
12
+  provider = "postgresql"
13
+  url      = env("DATABASE_URL")
14
+}
15
+
16
+enum ProgressStatus {
17
+  cari_data
18
+  dihubungi
19
+  negosiasi
20
+  follow_up
21
+  mou
22
+  onboarded
23
+  tidak_berminat
24
+}
25
+
26
+model RevokedToken {
27
+  id         String   @id @default(uuid())
28
+  token      String
29
+  revoked_at DateTime @default(now())
30
+
31
+  @@map("revoked_tokens")
32
+}
33
+
34
+model ActivityLog {
35
+  id        String    @id @default(uuid())
36
+  user_id   String
37
+  username  String
38
+  action    String
39
+  createdAt DateTime  @default(now())
40
+  updatedAt DateTime  @updatedAt
41
+  deletedAt DateTime?
42
+
43
+  @@map("activity_logs")
44
+}
45
+
46
+model User {
47
+  id         String     @id @default(uuid())
48
+  username   String
49
+  email      String
50
+  password   String
51
+  firstname  String
52
+  lastname   String
53
+  role       String
54
+  createdAt  DateTime   @default(now())
55
+  updatedAt  DateTime   @updatedAt
56
+  deletedAt  DateTime?
57
+  hospitals  Hospital[]
58
+  user_areas UserArea[]
59
+
60
+  @@map("users")
61
+}
62
+
63
+model Province {
64
+  id         String     @id @default(uuid())
65
+  name       String     @unique
66
+  cities     City[]
67
+  hospitals  Hospital[]
68
+  user_areas UserArea[]
69
+  createdAt  DateTime   @default(now())
70
+  updatedAt  DateTime   @updatedAt
71
+  deletedAt  DateTime?
72
+
73
+  @@map("provinces")
74
+}
75
+
76
+model City {
77
+  id          String     @id @default(uuid())
78
+  name        String
79
+  province_id String
80
+  province    Province   @relation(fields: [province_id], references: [id])
81
+  hospitals   Hospital[]
82
+  createdAt   DateTime   @default(now())
83
+  updatedAt   DateTime   @updatedAt
84
+  deletedAt   DateTime?
85
+
86
+  @@unique([name, province_id], name: "name_province_id")
87
+  @@map("cities")
88
+}
89
+
90
+model Hospital {
91
+  id              String       @id @default(uuid())
92
+  name            String
93
+  hospital_code   String?
94
+  type            String?
95
+  ownership       String?
96
+  province_id     String
97
+  city_id         String
98
+  address         String?
99
+  simrs_type      String?
100
+  contact         String?
101
+  image           String?
102
+  progress_status ProgressStatus
103
+  note            String?
104
+  created_by      String
105
+  createdAt       DateTime     @default(now())
106
+  updatedAt       DateTime     @updatedAt
107
+  deletedAt       DateTime?
108
+  province        Province     @relation(fields: [province_id], references: [id])
109
+  city            City         @relation(fields: [city_id], references: [id])
110
+  user            User         @relation(fields: [created_by], references: [id])
111
+
112
+  @@map("hospitals")
113
+}
114
+
115
+model UserArea {
116
+  id            String       @id @default(uuid())
117
+  user_id       String
118
+  province_id   String
119
+  user          User         @relation(fields: [user_id], references: [id])
120
+  province      Province     @relation(fields: [province_id], references: [id])
121
+  createdAt     DateTime     @default(now())
122
+  updatedAt     DateTime     @updatedAt
123
+  deletedAt     DateTime?
124
+
125
+  @@map("user_areas")
126
+}

+ 93 - 0
prisma/seeders/DatabaseSeeder.js

@@ -0,0 +1,93 @@
1
+const prisma = require('../../src/prisma/PrismaClient.js');
2
+
3
+const { seedProvinces } = require('./ProvinceSeeder');
4
+const { seedAcehCities } = require('./city/AcehCitySeeder');
5
+const { seedBaliCities } = require('./city/BaliCitySeeder');
6
+const { seedBantenCities } = require('./city/BantenCitySeeder');
7
+const { seedBengkuluCities } = require('./city/BengkuluCitySeeder');
8
+const { seedDIYogyakartaCities } = require('./city/DIYogyakartaCitySeeder');
9
+const { seedDKIJakartaCities } = require('./city/DKIJakartaCitySeeder');
10
+const { seedGorontaloCities } = require('./city/GorontaloCitySeeder');
11
+const { seedJambiCities } = require('./city/JambiCitySeeder');
12
+const { seedJawaBaratCities } = require('./city/JawaBaratCitySeeder');
13
+const { seedJawaTengahCities } = require('./city/JawaTengahCitySeeder');
14
+const { seedJawaTimurCities } = require('./city/JawaTimurCitySeeder');
15
+const { seedKalimantanBaratCities } = require('./city/KalimantanBaratCitySeeder');
16
+const { seedKalimantanSelatanCities } = require('./city/KalimantanSelatanCitySeeder');
17
+const { seedKalimantanTengahCities } = require('./city/KalimantanTengahCitySeeder');
18
+const { seedKalimantanTimurCities } = require('./city/KalimantanTimurCitySeeder');
19
+const { seedKalimantanUtaraCities } = require('./city/KalimantanUtaraCitySeeder');
20
+const { seedKepulauanBangkaBelitungCities } = require('./city/KepulauanBangkaBelitungCitySeeder');
21
+const { seedKepulauanRiauCities } = require('./city/KepulauanRiauCitySeeder');
22
+const { seedLampungCities } = require('./city/LampungCitySeeder');
23
+const { seedMalukuCities } = require('./city/MalukuCitySeeder');
24
+const { seedMalukuUtaraCities } = require('./city/MalukuUtaraCitySeeder');
25
+const { seedNusaTenggaraBaratCities } = require('./city/NusaTenggaraBaratCitySeeder');
26
+const { seedNusaTenggaraTimurCities } = require('./city/NusaTenggaraTimurCitySeeder');
27
+const { seedPapuaCities } = require('./city/PapuaCitySeeder');
28
+const { seedPapuaBaratCities } = require('./city/PapuaBaratCitySeeder');
29
+const { seedPapuaBaratDayaCities } = require('./city/PapuaBaratDayaCitySeeder');
30
+const { seedPapuaPegununganCities } = require('./city/PapuaPegununganCitySeeder');
31
+const { seedPapuaTengahCities } = require('./city/PapuaTengahCitySeeder');
32
+const { seedRiauCities } = require('./city/RiauCitySeeder');
33
+const { seedSulawesiBaratCities } = require('./city/SulawesiBaratCitySeeder');
34
+const { seedSulawesiSelatanCities } = require('./city/SulawesiSelatanCitySeeder');
35
+const { seedSulawesiTengahCities } = require('./city/SulawesiTengahCitySeeder');
36
+const { seedSulawesiTenggaraCities } = require('./city/SulawesiTenggaraCitySeeder');
37
+const { seedSulawesiUtaraCities } = require('./city/SulawesiUtaraCitySeeder');
38
+const { seedSumateraBaratCities } = require('./city/SumateraBaratCitySeeder');
39
+const { seedSumateraSelatanCities } = require('./city/SumateraSelatanCitySeeder');
40
+const { seedSumateraUtaraCities } = require('./city/SumateraUtaraCitySeeder');
41
+const { seedHospital } = require('./HospitalSeeder.js');
42
+const { seedSales } = require('./UserSeeder.js');
43
+
44
+async function main() {
45
+    await seedProvinces();
46
+    await seedAcehCities();
47
+    await seedBaliCities();
48
+    await seedBantenCities();
49
+    await seedBengkuluCities();
50
+    await seedDIYogyakartaCities();
51
+    await seedDKIJakartaCities();
52
+    await seedGorontaloCities();
53
+    await seedJambiCities();
54
+    await seedJawaBaratCities();
55
+    await seedJawaTengahCities();
56
+    await seedJawaTimurCities();
57
+    await seedKalimantanBaratCities();
58
+    await seedKalimantanSelatanCities();
59
+    await seedKalimantanTengahCities();
60
+    await seedKalimantanTimurCities();
61
+    await seedKalimantanUtaraCities();
62
+    await seedKepulauanBangkaBelitungCities();
63
+    await seedKepulauanRiauCities();
64
+    await seedLampungCities();
65
+    await seedMalukuCities();
66
+    await seedMalukuUtaraCities();
67
+    await seedNusaTenggaraBaratCities();
68
+    await seedNusaTenggaraTimurCities();
69
+    await seedPapuaCities();
70
+    await seedPapuaBaratCities();
71
+    await seedPapuaBaratDayaCities();
72
+    await seedPapuaPegununganCities();
73
+    await seedPapuaTengahCities();
74
+    await seedRiauCities();
75
+    await seedSulawesiBaratCities();
76
+    await seedSulawesiSelatanCities();
77
+    await seedSulawesiTengahCities();
78
+    await seedSulawesiTenggaraCities();
79
+    await seedSulawesiUtaraCities();
80
+    await seedSumateraBaratCities();
81
+    await seedSumateraSelatanCities();
82
+    await seedSumateraUtaraCities();
83
+    // await seedSales();
84
+    // await seedHospital();
85
+}
86
+
87
+main().then(() => {
88
+    console.log('✅ Seeding completed');
89
+    return prisma.$disconnect();
90
+}).catch((e) => {
91
+    console.error('❌ Seeding error:', e);
92
+    return prisma.$disconnect().then(() => process.exit(1));
93
+});

+ 49 - 0
prisma/seeders/HospitalSeeder.js

@@ -0,0 +1,49 @@
1
+const prisma = require('../../src/prisma/PrismaClient.js');
2
+const { faker } = require('@faker-js/faker');
3
+const timeLocal = require('../../src/utils/TimeLocal.js');
4
+
5
+exports.seedHospital = async () => {
6
+    // 1. Ambil semua provinsi
7
+    const allProvinces = await prisma.province.findMany();
8
+    const shuffled = allProvinces.sort(() => 0.5 - Math.random());
9
+    const selectedProvinces = shuffled.slice(0, 5); // ambil 5 random
10
+
11
+    for (const province of selectedProvinces) {
12
+        const cities = await prisma.city.findMany({
13
+            where: { province_id: province.id }
14
+        });
15
+
16
+        if (cities.length === 0) continue; // skip kalau tidak ada kota
17
+
18
+        const randomCity = cities[Math.floor(Math.random() * cities.length)];
19
+
20
+        await prisma.hospital.create({
21
+            data: {
22
+                name: `RS ${faker.company.name()}`,
23
+                hospital_code: faker.string.alphanumeric(6).toUpperCase(),
24
+                type: faker.helpers.arrayElement(['Tipe A', 'Tipe B', 'Tipe C']),
25
+                ownership: faker.helpers.arrayElement(['Pemerintah', 'Swasta']),
26
+                address: faker.location.streetAddress(),
27
+                simrs_type: faker.helpers.arrayElement(['SIMRS A', 'SIMRS B', 'SIMRS C']),
28
+                contact: faker.phone.number('08##########'),
29
+                image: '/storage/img/dummy.jpg',
30
+                note: faker.lorem.sentence(),
31
+                progress_status: 'cari_data',
32
+                createdAt: timeLocal.now().toDate(),
33
+                province: { connect: { id: province.id } },
34
+                city: { connect: { id: randomCity.id } },
35
+                // user: { connect: { id: await getRandomUserId() } }, // opsional: assign user
36
+                user: null
37
+            }
38
+        });
39
+    }
40
+
41
+    console.log('✅ Hospital seeder done!');
42
+};
43
+
44
+// Fungsi ambil 1 user id random dari tabel user
45
+async function getRandomUserId() {
46
+    const users = await prisma.user.findMany({ where: { deletedAt: null } });
47
+    if (users.length === 0) throw new Error('No users found for seeding hospital.');
48
+    return users[Math.floor(Math.random() * users.length)].id;
49
+}

+ 30 - 0
prisma/seeders/ProvinceSeeder.js

@@ -0,0 +1,30 @@
1
+const prisma = require('../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../src/utils/TimeLocal.js')
3
+
4
+const provinces = [
5
+    "Aceh", "Bali", "Banten", "Bengkulu", "DI Yogyakarta", "DKI Jakarta", "Gorontalo",
6
+    "Jambi", "Jawa Barat", "Jawa Tengah", "Jawa Timur", "Kalimantan Barat",
7
+    "Kalimantan Selatan", "Kalimantan Tengah", "Kalimantan Timur", "Kalimantan Utara",
8
+    "Kepulauan Bangka Belitung", "Kepulauan Riau", "Lampung", "Maluku",
9
+    "Maluku Utara", "Nusa Tenggara Barat", "Nusa Tenggara Timur", "Papua",
10
+    "Papua Barat", "Papua Barat Daya", "Papua Pegunungan", "Papua Selatan",
11
+    "Papua Tengah", "Riau", "Sulawesi Barat", "Sulawesi Selatan", "Sulawesi Tengah",
12
+    "Sulawesi Tenggara", "Sulawesi Utara", "Sumatera Barat", "Sumatera Selatan", "Sumatera Utara"
13
+];
14
+
15
+exports.seedProvinces = async () => {
16
+    for (const name of provinces.sort()) {
17
+        await prisma.province.upsert({
18
+            where: { name },
19
+            create: {
20
+                name,
21
+                createdAt: timeLocal.now().toDate(),
22
+            },
23
+            update: {
24
+                updatedAt: timeLocal.now().toDate()
25
+            }
26
+        });
27
+    }
28
+
29
+    console.log('✅ Provinces seeded!');
30
+};

+ 51 - 0
prisma/seeders/UserSeeder.js

@@ -0,0 +1,51 @@
1
+const { KeycloakRepository, UserRepository, UserAreaRepository } = require('../../src/repository/admin/SalesRepository.js');
2
+const prisma = require('../../src/prisma/PrismaClient');
3
+const { faker } = require('@faker-js/faker');
4
+
5
+exports.seedSales = async () => {
6
+    try {
7
+        const provinces = await prisma.province.findMany();
8
+        if (provinces.length === 0) throw new Error('No provinces found');
9
+
10
+        const users = [
11
+            {
12
+                username: 'sales1',
13
+                email: 'sales1@example.com',
14
+                firstname: 'Sales',
15
+                lastname: 'One',
16
+                password: 'Sales@123',
17
+            },
18
+            {
19
+                username: 'sales2',
20
+                email: 'sales2@example.com',
21
+                firstname: 'Sales',
22
+                lastname: 'Two',
23
+                password: 'Sales@123',
24
+            },
25
+            {
26
+                username: 'sales3',
27
+                email: 'sales3@example.com',
28
+                firstname: 'Sales',
29
+                lastname: 'Three',
30
+                password: 'Sales@123',
31
+            },
32
+        ];
33
+
34
+        for (const user of users) {
35
+            const shuffled = provinces.sort(() => 0.5 - Math.random());
36
+            const selectedProvinces = shuffled.slice(0, Math.floor(Math.random() * 3) + 1);
37
+            const province_ids = selectedProvinces.map(p => p.id);
38
+
39
+            user.province_ids = province_ids;
40
+
41
+            const userId = await KeycloakRepository.createUser(user);
42
+            await KeycloakRepository.assignSalesRole(userId);
43
+            await UserRepository.createUser(userId, user);
44
+            await UserAreaRepository.createMany(userId, province_ids);
45
+
46
+            console.log(`✅ Seeded user ${user.username}`);
47
+        }
48
+    } catch (err) {
49
+        console.error('❌ Failed to seed sales:', err.message);
50
+    }
51
+};

+ 62 - 0
prisma/seeders/city/AcehCitySeeder.js

@@ -0,0 +1,62 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Aceh Barat',
6
+    'Kabupaten Aceh Barat Daya',
7
+    'Kabupaten Aceh Besar',
8
+    'Kabupaten Aceh Jaya',
9
+    'Kabupaten Aceh Selatan',
10
+    'Kabupaten Aceh Singkil',
11
+    'Kabupaten Aceh Tamiang',
12
+    'Kabupaten Aceh Tengah',
13
+    'Kabupaten Aceh Tenggara',
14
+    'Kabupaten Aceh Timur',
15
+    'Kabupaten Aceh Utara',
16
+    'Kabupaten Bener Meriah',
17
+    'Kabupaten Bireuen',
18
+    'Kabupaten Gayo Lues',
19
+    'Kabupaten Nagan Raya',
20
+    'Kabupaten Pidie',
21
+    'Kabupaten Pidie Jaya',
22
+    'Kabupaten Simeulue',
23
+    'Kota Banda Aceh',
24
+    'Kota Langsa',
25
+    'Kota Lhokseumawe',
26
+    'Kota Sabang',
27
+    'Kota Subulussalam'
28
+
29
+    // 23
30
+];
31
+
32
+exports.seedAcehCities = async () => {
33
+    const province = await prisma.province.findFirst({
34
+        where: { name: 'Aceh' },
35
+    });
36
+
37
+    if (!province) {
38
+        console.error('❌ Province Aceh not found. Seed it first.');
39
+        return;
40
+    }
41
+
42
+    for (const name of cityNames) {
43
+        await prisma.city.upsert({
44
+            where: {
45
+                name_province_id: {
46
+                    name,
47
+                    province_id: province.id,
48
+                },
49
+            },
50
+            create: {
51
+                name,
52
+                province_id: province.id,
53
+                updatedAt: timeLocal.now().toDate()
54
+            },
55
+            update: {
56
+                updatedAt: timeLocal.now().toDate()
57
+            }
58
+        });
59
+    }
60
+
61
+    console.log('✅ Aceh City seeded!.');
62
+};

+ 48 - 0
prisma/seeders/city/BaliCitySeeder.js

@@ -0,0 +1,48 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Badung',
6
+    'Kabupaten Bangli',
7
+    'Kabupaten Buleleng',
8
+    'Kabupaten Gianyar',
9
+    'Kabupaten Jembrana',
10
+    'Kabupaten Karangasem',
11
+    'Kabupaten Klungkung',
12
+    'Kabupaten Tabanan',
13
+    'Kota Denpasar'
14
+
15
+    // 9
16
+];
17
+
18
+exports.seedBaliCities = async () => {
19
+    const province = await prisma.province.findFirst({
20
+        where: { name: 'Bali' },
21
+    });
22
+
23
+    if (!province) {
24
+        console.error('❌ Province Bali not found. Seed it first.');
25
+        return;
26
+    }
27
+
28
+    for (const name of cityNames) {
29
+        await prisma.city.upsert({
30
+            where: {
31
+                name_province_id: {
32
+                    name,
33
+                    province_id: province.id,
34
+                },
35
+            },
36
+            update: {
37
+                updatedAt: timeLocal.now().toDate()
38
+            },
39
+            create: {
40
+                name,
41
+                province_id: province.id,
42
+                createdAt: timeLocal.now().toDate()
43
+            },
44
+        });
45
+    }
46
+
47
+    console.log('✅ Bali City seeded!.');
48
+};

+ 47 - 0
prisma/seeders/city/BantenCitySeeder.js

@@ -0,0 +1,47 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Lebak',
6
+    'Kabupaten Pandeglang',
7
+    'Kabupaten Serang',
8
+    'Kabupaten Tangerang',
9
+    'Kota Cilegon',
10
+    'Kota Serang',
11
+    'Kota Tangerang',
12
+    'Kota Tangerang Selatan'
13
+
14
+    // 8
15
+];
16
+
17
+exports.seedBantenCities = async () => {
18
+    const province = await prisma.province.findFirst({
19
+        where: { name: 'Banten' },
20
+    });
21
+
22
+    if (!province) {
23
+        console.error('❌ Province Banten not found. Seed it first.');
24
+        return;
25
+    }
26
+
27
+    for (const name of cityNames) {
28
+        await prisma.city.upsert({
29
+            where: {
30
+                name_province_id: {
31
+                    name,
32
+                    province_id: province.id,
33
+                },
34
+            },
35
+            update: {
36
+                updatedAt: timeLocal.now().toDate()
37
+            },
38
+            create: {
39
+                name,
40
+                province_id: province.id,
41
+                createdAt: timeLocal.now().toDate()
42
+            },
43
+        });
44
+    }
45
+
46
+    console.log('✅ Banten City seeded!.');
47
+};

+ 47 - 0
prisma/seeders/city/BengkuluCitySeeder.js

@@ -0,0 +1,47 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bengkulu Selatan',
6
+    'Kabupaten Bengkulu Tengah',
7
+    'Kabupaten Bengkulu Utara',
8
+    'Kabupaten Kaur',
9
+    'Kabupaten Kepahiang',
10
+    'Kabupaten Lebong',
11
+    'Kabupaten Muko Muko',
12
+    'Kabupaten Rejang Lebong',
13
+    'Kabupaten Seluma',
14
+    'Kota Bengkulu'
15
+
16
+    // 10
17
+];
18
+
19
+exports.seedBengkuluCities = async () => {
20
+    const province = await prisma.province.findFirst({
21
+        where: { name: 'Bengkulu' },
22
+    });
23
+
24
+    if (!province) {
25
+        console.error('❌ Province Bengkulu not found. Seed it first.');
26
+        return;
27
+    }
28
+
29
+    for (const name of cityNames) {
30
+        await prisma.city.upsert({
31
+            where: {
32
+                name_province_id: {
33
+                    name,
34
+                    province_id: province.id,
35
+                },
36
+            },
37
+            update: { updatedAt: timeLocal.now().toDate() },
38
+            create: {
39
+                name,
40
+                province_id: province.id,
41
+                createdAt: timeLocal.now().toDate()
42
+            },
43
+        });
44
+    }
45
+
46
+    console.log('✅ Bengkulu City seeded!.');
47
+};

+ 42 - 0
prisma/seeders/city/DIYogyakartaCitySeeder.js

@@ -0,0 +1,42 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bantul',
6
+    'Kabupaten Gunungkidul',
7
+    'Kabupaten Kulon Progo',
8
+    'Kabupaten Sleman',
9
+    'Kota Yogyakarta'
10
+
11
+    // 5
12
+];
13
+
14
+exports.seedDIYogyakartaCities = async () => {
15
+    const province = await prisma.province.findFirst({
16
+        where: { name: 'DI Yogyakarta' },
17
+    });
18
+
19
+    if (!province) {
20
+        console.error('❌ Province DI Yogyakarta not found. Seed it first.');
21
+        return;
22
+    }
23
+
24
+    for (const name of cityNames) {
25
+        await prisma.city.upsert({
26
+            where: {
27
+                name_province_id: {
28
+                    name,
29
+                    province_id: province.id,
30
+                },
31
+            },
32
+            update: { updatedAt: timeLocal.now().toDate() },
33
+            create: {
34
+                name,
35
+                province_id: province.id,
36
+                createdAt: timeLocal.now().toDate()
37
+            },
38
+        });
39
+    }
40
+
41
+    console.log('✅ DI Yogyakarta City seeded!.');
42
+};

+ 45 - 0
prisma/seeders/city/DKIJakartaCitySeeder.js

@@ -0,0 +1,45 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Kepulauan Seribu',
6
+    'Kota Jakarta Barat',
7
+    'Kota Jakarta Pusat',
8
+    'Kota Jakarta Selatan',
9
+    'Kota Jakarta Timur',
10
+    'Kota Jakarta Utara'
11
+
12
+    // 6
13
+];
14
+
15
+exports.seedDKIJakartaCities = async () => {
16
+    const province = await prisma.province.findFirst({
17
+        where: { name: 'DKI Jakarta' },
18
+    });
19
+
20
+    if (!province) {
21
+        console.error('❌ Province DKI Jakarta not found. Seed it first.');
22
+        return;
23
+    }
24
+
25
+    for (const name of cityNames) {
26
+        await prisma.city.upsert({
27
+            where: {
28
+                name_province_id: {
29
+                    name,
30
+                    province_id: province.id,
31
+                },
32
+            },
33
+            update: {
34
+                updatedAt: timeLocal.now().toDate()
35
+            },
36
+            create: {
37
+                name,
38
+                province_id: province.id,
39
+                createdAt: timeLocal.now().toDate()
40
+            },
41
+        });
42
+    }
43
+
44
+    console.log('✅ DKI Jakarta City seeded!.');
45
+};

+ 45 - 0
prisma/seeders/city/GorontaloCitySeeder.js

@@ -0,0 +1,45 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Boalemo',
6
+    'Kabupaten Bone Bolango',
7
+    'Kabupaten Gorontalo',
8
+    'Kabupaten Gorontalo Utara',
9
+    'Kabupaten Pohuwato',
10
+    'Kota Gorontalo'
11
+
12
+    // 6
13
+];
14
+
15
+exports.seedGorontaloCities = async () => {
16
+    const province = await prisma.province.findFirst({
17
+        where: { name: 'Gorontalo' },
18
+    });
19
+
20
+    if (!province) {
21
+        console.error('❌ Province Gorontalo not found. Seed it first.');
22
+        return;
23
+    }
24
+
25
+    for (const name of cityNames) {
26
+        await prisma.city.upsert({
27
+            where: {
28
+                name_province_id: {
29
+                    name,
30
+                    province_id: province.id,
31
+                },
32
+            },
33
+            update: {
34
+                updatedAt: timeLocal.now().toDate()
35
+            },
36
+            create: {
37
+                name,
38
+                province_id: province.id,
39
+                createdAt: timeLocal.now().toDate()
40
+            },
41
+        });
42
+    }
43
+
44
+    console.log('✅ Gorontalo City seeded!.');
45
+};

+ 50 - 0
prisma/seeders/city/JambiCitySeeder.js

@@ -0,0 +1,50 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Batang Hari',
6
+    'Kabupaten Bungo',
7
+    'Kabupaten Kerinci',
8
+    'Kabupaten Merangin',
9
+    'Kabupaten Muaro Jambi',
10
+    'Kabupaten Sarolangun',
11
+    'Kabupaten Tanjung Jabung Barat',
12
+    'Kabupaten Tanjung Jabung Timur',
13
+    'Kabupaten Tebo',
14
+    'Kota Jambi',
15
+    'Kota Sungai Penuh'
16
+
17
+    // 11
18
+];
19
+
20
+exports.seedJambiCities = async () => {
21
+    const province = await prisma.province.findFirst({
22
+        where: { name: 'Jambi' },
23
+    });
24
+
25
+    if (!province) {
26
+        console.error('❌ Province Jambi not found. Seed it first.');
27
+        return;
28
+    }
29
+
30
+    for (const name of cityNames) {
31
+        await prisma.city.upsert({
32
+            where: {
33
+                name_province_id: {
34
+                    name,
35
+                    province_id: province.id,
36
+                },
37
+            },
38
+            update: {
39
+                updatedAt: timeLocal.now().toDate()
40
+            },
41
+            create: {
42
+                name,
43
+                province_id: province.id,
44
+                createdAt: timeLocal.now().toDate()
45
+            },
46
+        });
47
+    }
48
+
49
+    console.log('✅ Jambi City seeded!.');
50
+};

+ 66 - 0
prisma/seeders/city/JawaBaratCitySeeder.js

@@ -0,0 +1,66 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bandung',
6
+    'Kabupaten Bandung Barat',
7
+    'Kabupaten Bekasi',
8
+    'Kabupaten Bogor',
9
+    'Kabupaten Ciamis',
10
+    'Kabupaten Cianjur',
11
+    'Kabupaten Cirebon',
12
+    'Kabupaten Garut',
13
+    'Kabupaten Indramayu',
14
+    'Kabupaten Karawang',
15
+    'Kabupaten Kuningan',
16
+    'Kabupaten Majalengka',
17
+    'Kabupaten Pangandaran',
18
+    'Kabupaten Purwakarta',
19
+    'Kabupaten Subang',
20
+    'Kabupaten Sukabumi',
21
+    'Kabupaten Sumedang',
22
+    'Kabupaten Tasikmalaya',
23
+    'Kota Bandung',
24
+    'Kota Banjar',
25
+    'Kota Bekasi',
26
+    'Kota Bogor',
27
+    'Kota Cimahi',
28
+    'Kota Cirebon',
29
+    'Kota Depok',
30
+    'Kota Sukabumi',
31
+    'Kota Tasikmalaya'
32
+
33
+    // 27
34
+];
35
+
36
+exports.seedJawaBaratCities = async () => {
37
+    const province = await prisma.province.findFirst({
38
+        where: { name: 'Jawa Barat' },
39
+    });
40
+
41
+    if (!province) {
42
+        console.error('❌ Province Jawa Barat not found. Seed it first.');
43
+        return;
44
+    }
45
+
46
+    for (const name of cityNames) {
47
+        await prisma.city.upsert({
48
+            where: {
49
+                name_province_id: {
50
+                    name,
51
+                    province_id: province.id,
52
+                },
53
+            },
54
+            update: {
55
+                updatedAt: timeLocal.now().toDate()
56
+            },
57
+            create: {
58
+                name,
59
+                province_id: province.id,
60
+                createdAt: timeLocal.now().toDate()
61
+            },
62
+        });
63
+    }
64
+
65
+    console.log('✅ Jawa Barat City seeded!.');
66
+};

+ 74 - 0
prisma/seeders/city/JawaTengahCitySeeder.js

@@ -0,0 +1,74 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Banjarnegara',
6
+    'Kabupaten Banyumas',
7
+    'Kabupaten Batang',
8
+    'Kabupaten Blora',
9
+    'Kabupaten Boyolali',
10
+    'Kabupaten Brebes',
11
+    'Kabupaten Cilacap',
12
+    'Kabupaten Demak',
13
+    'Kabupaten Grobogan',
14
+    'Kabupaten Jepara',
15
+    'Kabupaten Karanganyar',
16
+    'Kabupaten Kebumen',
17
+    'Kabupaten Kendal',
18
+    'Kabupaten Klaten',
19
+    'Kabupaten Kudus',
20
+    'Kabupaten Magelang',
21
+    'Kabupaten Pati',
22
+    'Kabupaten Pekalongan',
23
+    'Kabupaten Pemalang',
24
+    'Kabupaten Purbalingga',
25
+    'Kabupaten Purworejo',
26
+    'Kabupaten Rembang',
27
+    'Kabupaten Semarang',
28
+    'Kabupaten Sragen',
29
+    'Kabupaten Sukoharjo',
30
+    'Kabupaten Tegal',
31
+    'Kabupaten Temanggung',
32
+    'Kabupaten Wonogiri',
33
+    'Kabupaten Wonosobo',
34
+    'Kota Magelang',
35
+    'Kota Pekalongan',
36
+    'Kota Salatiga',
37
+    'Kota Semarang',
38
+    'Kota Surakarta',
39
+    'Kota Tegal'
40
+
41
+    // 35
42
+];
43
+
44
+exports.seedJawaTengahCities = async () => {
45
+    const province = await prisma.province.findFirst({
46
+        where: { name: 'Jawa Tengah' },
47
+    });
48
+
49
+    if (!province) {
50
+        console.error('❌ Province Jawa Tengah not found. Seed it first.');
51
+        return;
52
+    }
53
+
54
+    for (const name of cityNames) {
55
+        await prisma.city.upsert({
56
+            where: {
57
+                name_province_id: {
58
+                    name,
59
+                    province_id: province.id,
60
+                },
61
+            },
62
+            update: {
63
+                updatedAt: timeLocal.now().toDate()
64
+            },
65
+            create: {
66
+                name,
67
+                province_id: province.id,
68
+                createdAt: timeLocal.now().toDate()
69
+            },
70
+        });
71
+    }
72
+
73
+    console.log('✅ Jawa Tengah City seeded!.');
74
+};

+ 77 - 0
prisma/seeders/city/JawaTimurCitySeeder.js

@@ -0,0 +1,77 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bangkalan',
6
+    'Kabupaten Banyuwangi',
7
+    'Kabupaten Blitar',
8
+    'Kabupaten Bojonegoro',
9
+    'Kabupaten Bondowoso',
10
+    'Kabupaten Gresik',
11
+    'Kabupaten Jember',
12
+    'Kabupaten Jombang',
13
+    'Kabupaten Kediri',
14
+    'Kabupaten Lamongan',
15
+    'Kabupaten Lumajang',
16
+    'Kabupaten Madiun',
17
+    'Kabupaten Magetan',
18
+    'Kabupaten Malang',
19
+    'Kabupaten Mojokerto',
20
+    'Kabupaten Nganjuk',
21
+    'Kabupaten Ngawi',
22
+    'Kabupaten Pacitan',
23
+    'Kabupaten Pamekasan',
24
+    'Kabupaten Pasuruan',
25
+    'Kabupaten Ponorogo',
26
+    'Kabupaten Probolinggo',
27
+    'Kabupaten Sampang',
28
+    'Kabupaten Sidoarjo',
29
+    'Kabupaten Situbondo',
30
+    'Kabupaten Sumenep',
31
+    'Kabupaten Trenggalek',
32
+    'Kabupaten Tuban',
33
+    'Kabupaten Tulungagung',
34
+    'Kota Batu',
35
+    'Kota Blitar',
36
+    'Kota Kediri',
37
+    'Kota Madiun',
38
+    'Kota Malang',
39
+    'Kota Mojokerto',
40
+    'Kota Pasuruan',
41
+    'Kota Probolinggo',
42
+    'Kota Surabaya'
43
+
44
+    // 38
45
+];
46
+
47
+exports.seedJawaTimurCities = async () => {
48
+    const province = await prisma.province.findFirst({
49
+        where: { name: 'Jawa Timur' },
50
+    });
51
+
52
+    if (!province) {
53
+        console.error('❌ Province Jawa Timur not found. Seed it first.');
54
+        return;
55
+    }
56
+
57
+    for (const name of cityNames) {
58
+        await prisma.city.upsert({
59
+            where: {
60
+                name_province_id: {
61
+                    name,
62
+                    province_id: province.id,
63
+                },
64
+            },
65
+            update: {
66
+                updatedAt: timeLocal.now().toDate()
67
+            },
68
+            create: {
69
+                name,
70
+                province_id: province.id,
71
+                createdAt: timeLocal.now().toDate()
72
+            },
73
+        });
74
+    }
75
+
76
+    console.log('✅ Jawa Timur City seeded!.');
77
+};

+ 53 - 0
prisma/seeders/city/KalimantanBaratCitySeeder.js

@@ -0,0 +1,53 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bengkayang',
6
+    'Kabupaten Kapuas Hulu',
7
+    'Kabupaten Kayong Utara',
8
+    'Kabupaten Ketapang',
9
+    'Kabupaten Kubu Raya',
10
+    'Kabupaten Landak',
11
+    'Kabupaten Melawi',
12
+    'Kabupaten Mempawah',
13
+    'Kabupaten Sambas',
14
+    'Kabupaten Sanggau',
15
+    'Kabupaten Sekadau',
16
+    'Kabupaten Sintang',
17
+    'Kota Pontianak',
18
+    'Kota Singkawang'
19
+
20
+    // 14
21
+];
22
+
23
+exports.seedKalimantanBaratCities = async () => {
24
+    const province = await prisma.province.findFirst({
25
+        where: { name: 'Kalimantan Barat' },
26
+    });
27
+
28
+    if (!province) {
29
+        console.error('❌ Province Kalimantan Barat not found. Seed it first.');
30
+        return;
31
+    }
32
+
33
+    for (const name of cityNames) {
34
+        await prisma.city.upsert({
35
+            where: {
36
+                name_province_id: {
37
+                    name,
38
+                    province_id: province.id,
39
+                },
40
+            },
41
+            update: {
42
+                updatedAt: timeLocal.now().toDate()
43
+            },
44
+            create: {
45
+                name,
46
+                province_id: province.id,
47
+                createdAt: timeLocal.now().toDate()
48
+            },
49
+        });
50
+    }
51
+
52
+    console.log('✅ Kalimantan Barat City seeded!.');
53
+};

+ 52 - 0
prisma/seeders/city/KalimantanSelatanCitySeeder.js

@@ -0,0 +1,52 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Balangan',
6
+    'Kabupaten Banjar',
7
+    'Kabupaten Barito Kuala',
8
+    'Kabupaten Hulu Sungai Selatan',
9
+    'Kabupaten Hulu Sungai Tengah',
10
+    'Kabupaten Hulu Sungai Utara',
11
+    'Kabupaten Kotabaru',
12
+    'Kabupaten Tabalong',
13
+    'Kabupaten Tanah Bumbu',
14
+    'Kabupaten Tanah Laut',
15
+    'Kabupaten Tapin',
16
+    'Kota Banjarbaru',
17
+    'Kota Banjarmasin'
18
+
19
+    // 13
20
+];
21
+
22
+exports.seedKalimantanSelatanCities = async () => {
23
+    const province = await prisma.province.findFirst({
24
+        where: { name: 'Kalimantan Selatan' },
25
+    });
26
+
27
+    if (!province) {
28
+        console.error('❌ Province Kalimantan Selatan not found. Seed it first.');
29
+        return;
30
+    }
31
+
32
+    for (const name of cityNames) {
33
+        await prisma.city.upsert({
34
+            where: {
35
+                name_province_id: {
36
+                    name,
37
+                    province_id: province.id,
38
+                },
39
+            },
40
+            update: {
41
+                updatedAt: timeLocal.now().toDate()
42
+            },
43
+            create: {
44
+                name,
45
+                province_id: province.id,
46
+                createdAt: timeLocal.now().toDate()
47
+            },
48
+        });
49
+    }
50
+
51
+    console.log('✅ Kalimantan Selatan City seeded!.');
52
+};

+ 53 - 0
prisma/seeders/city/KalimantanTengahCitySeeder.js

@@ -0,0 +1,53 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Barito Selatan',
6
+    'Kabupaten Barito Timur',
7
+    'Kabupaten Barito Utara',
8
+    'Kabupaten Gunung Mas',
9
+    'Kabupaten Kapuas',
10
+    'Kabupaten Katingan',
11
+    'Kabupaten Kotawaringin Barat',
12
+    'Kabupaten Kotawaringin Timur',
13
+    'Kabupaten Lamandau',
14
+    'Kabupaten Murung Raya',
15
+    'Kabupaten Pulang Pisau',
16
+    'Kabupaten Sukamara',
17
+    'Kabupaten Seruyan',
18
+    'Kota Palangka Raya'
19
+
20
+    // 14
21
+];
22
+
23
+exports.seedKalimantanTengahCities = async () => {
24
+    const province = await prisma.province.findFirst({
25
+        where: { name: 'Kalimantan Tengah' },
26
+    });
27
+
28
+    if (!province) {
29
+        console.error('❌ Province Kalimantan Tengah not found. Seed it first.');
30
+        return;
31
+    }
32
+
33
+    for (const name of cityNames) {
34
+        await prisma.city.upsert({
35
+            where: {
36
+                name_province_id: {
37
+                    name,
38
+                    province_id: province.id,
39
+                },
40
+            },
41
+            update: {
42
+                updatedAt: timeLocal.now().toDate()
43
+            },
44
+            create: {
45
+                name,
46
+                province_id: province.id,
47
+                createdAt: timeLocal.now().toDate()
48
+            },
49
+        });
50
+    }
51
+
52
+    console.log('✅ Kalimantan Tengah City seeded!.');
53
+};

+ 49 - 0
prisma/seeders/city/KalimantanTimurCitySeeder.js

@@ -0,0 +1,49 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Berau',
6
+    'Kabupaten Kutai Barat',
7
+    'Kabupaten Kutai Kartanegara',
8
+    'Kabupaten Kutai Timur',
9
+    'Kabupaten Mahakam Ulu',
10
+    'Kabupaten Paser',
11
+    'Kabupaten Penajam Paser Utara',
12
+    'Kota Balikpapan',
13
+    'Kota Bontang',
14
+    'Kota Samarinda'
15
+
16
+    // 10
17
+];
18
+
19
+exports.seedKalimantanTimurCities = async () => {
20
+    const province = await prisma.province.findFirst({
21
+        where: { name: 'Kalimantan Timur' },
22
+    });
23
+
24
+    if (!province) {
25
+        console.error('❌ Province Kalimantan Timur not found. Seed it first.');
26
+        return;
27
+    }
28
+
29
+    for (const name of cityNames) {
30
+        await prisma.city.upsert({
31
+            where: {
32
+                name_province_id: {
33
+                    name,
34
+                    province_id: province.id,
35
+                },
36
+            },
37
+            update: {
38
+                updatedAt: timeLocal.now().toDate()
39
+            },
40
+            create: {
41
+                name,
42
+                province_id: province.id,
43
+                createdAt: timeLocal.now().toDate()
44
+            },
45
+        });
46
+    }
47
+
48
+    console.log('✅ Kalimantan Timur City seeded!.');
49
+};

+ 44 - 0
prisma/seeders/city/KalimantanUtaraCitySeeder.js

@@ -0,0 +1,44 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bulungan',
6
+    'Kabupaten Malinau',
7
+    'Kabupaten Nunukan',
8
+    'Kabupaten Tana Tidung',
9
+    'Kota Tarakan'
10
+
11
+    // 5
12
+];
13
+
14
+exports.seedKalimantanUtaraCities = async () => {
15
+    const province = await prisma.province.findFirst({
16
+        where: { name: 'Kalimantan Utara' },
17
+    });
18
+
19
+    if (!province) {
20
+        console.error('❌ Province Kalimantan Utara not found. Seed it first.');
21
+        return;
22
+    }
23
+
24
+    for (const name of cityNames) {
25
+        await prisma.city.upsert({
26
+            where: {
27
+                name_province_id: {
28
+                    name,
29
+                    province_id: province.id,
30
+                },
31
+            },
32
+            update: {
33
+                updatedAt: timeLocal.now().toDate()
34
+            },
35
+            create: {
36
+                name,
37
+                province_id: province.id,
38
+                createdAt: timeLocal.now().toDate()
39
+            },
40
+        });
41
+    }
42
+
43
+    console.log('✅ Kalimantan Utara City seeded!.');
44
+};

+ 46 - 0
prisma/seeders/city/KepulauanBangkaBelitungCitySeeder.js

@@ -0,0 +1,46 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bangka',
6
+    'Kabupaten Bangka Barat',
7
+    'Kabupaten Bangka Selatan',
8
+    'Kabupaten Bangka Tengah',
9
+    'Kabupaten Belitung',
10
+    'Kabupaten Belitung Timur',
11
+    'Kota Pangkal Pinang'
12
+
13
+    // 7
14
+];
15
+
16
+exports.seedKepulauanBangkaBelitungCities = async () => {
17
+    const province = await prisma.province.findFirst({
18
+        where: { name: 'Kepulauan Bangka Belitung' },
19
+    });
20
+
21
+    if (!province) {
22
+        console.error('❌ Province Kepulauan Bangka Belitung not found. Seed it first.');
23
+        return;
24
+    }
25
+
26
+    for (const name of cityNames) {
27
+        await prisma.city.upsert({
28
+            where: {
29
+                name_province_id: {
30
+                    name,
31
+                    province_id: province.id,
32
+                },
33
+            },
34
+            update: {
35
+                updatedAt: timeLocal.now().toDate()
36
+            },
37
+            create: {
38
+                name,
39
+                province_id: province.id,
40
+                createdAt: timeLocal.now().toDate()
41
+            },
42
+        });
43
+    }
44
+
45
+    console.log('✅ Kepulauan Bangka Belitung City seeded!.');
46
+};

+ 46 - 0
prisma/seeders/city/KepulauanRiauCitySeeder.js

@@ -0,0 +1,46 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bintan',
6
+    'Kabupaten Karimun',
7
+    'Kabupaten Kepulauan Anambas',
8
+    'Kabupaten Lingga',
9
+    'Kabupaten Natuna',
10
+    'Kota Batam',
11
+    'Kota Tanjung Pinang'
12
+
13
+    // 7
14
+];
15
+
16
+exports.seedKepulauanRiauCities = async () => {
17
+    const province = await prisma.province.findFirst({
18
+        where: { name: 'Kepulauan Riau' },
19
+    });
20
+
21
+    if (!province) {
22
+        console.error('❌ Province Kepulauan Riau not found. Seed it first.');
23
+        return;
24
+    }
25
+
26
+    for (const name of cityNames) {
27
+        await prisma.city.upsert({
28
+            where: {
29
+                name_province_id: {
30
+                    name,
31
+                    province_id: province.id,
32
+                },
33
+            },
34
+            update: {
35
+                updatedAt: timeLocal.now().toDate()
36
+            },
37
+            create: {
38
+                name,
39
+                province_id: province.id,
40
+                createdAt: timeLocal.now().toDate()
41
+            },
42
+        });
43
+    }
44
+
45
+    console.log('✅ Kepulauan Riau City seeded!.');
46
+};

+ 54 - 0
prisma/seeders/city/LampungCitySeeder.js

@@ -0,0 +1,54 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Lampung Barat',
6
+    'Kabupaten Lampung Selatan',
7
+    'Kabupaten Lampung Tengah',
8
+    'Kabupaten Lampung Timur',
9
+    'Kabupaten Lampung Utara',
10
+    'Kabupaten Mesuji',
11
+    'Kabupaten Pesawaran',
12
+    'Kabupaten Pesisir Barat',
13
+    'Kabupaten Pringsewu',
14
+    'Kabupaten Tanggamus',
15
+    'Kabupaten Tulang Bawang',
16
+    'Kabupaten Tulang Bawang Barat',
17
+    'Kabupaten Way Kanan',
18
+    'Kota Bandar Lampung',
19
+    'Kota Metro'
20
+
21
+    // 15
22
+];
23
+
24
+exports.seedLampungCities = async () => {
25
+    const province = await prisma.province.findFirst({
26
+        where: { name: 'Lampung' },
27
+    });
28
+
29
+    if (!province) {
30
+        console.error('❌ Province Lampung not found. Seed it first.');
31
+        return;
32
+    }
33
+
34
+    for (const name of cityNames) {
35
+        await prisma.city.upsert({
36
+            where: {
37
+                name_province_id: {
38
+                    name,
39
+                    province_id: province.id,
40
+                },
41
+            },
42
+            update: {
43
+                updatedAt: timeLocal.now().toDate()
44
+            },
45
+            create: {
46
+                name,
47
+                province_id: province.id,
48
+                createdAt: timeLocal.now().toDate()
49
+            },
50
+        });
51
+    }
52
+
53
+    console.log('✅ Lampung City seeded!.');
54
+};

+ 50 - 0
prisma/seeders/city/MalukuCitySeeder.js

@@ -0,0 +1,50 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Buru',
6
+    'Kabupaten Buru Selatan',
7
+    'Kabupaten Kepulauan Aru',
8
+    'Kabupaten Maluku Barat Daya',
9
+    'Kabupaten Maluku Tengah',
10
+    'Kabupaten Maluku Tenggara',
11
+    'Kabupaten Seram Bagian Barat',
12
+    'Kabupaten Seram Bagian Timur',
13
+    'Kota Ambon',
14
+    'Kota Tual',
15
+    'Kabupaten Maluku Tenggara Barat'
16
+
17
+    // 11
18
+];
19
+
20
+exports.seedMalukuCities = async () => {
21
+    const province = await prisma.province.findFirst({
22
+        where: { name: 'Maluku' },
23
+    });
24
+
25
+    if (!province) {
26
+        console.error('❌ Province Maluku not found. Seed it first.');
27
+        return;
28
+    }
29
+
30
+    for (const name of cityNames) {
31
+        await prisma.city.upsert({
32
+            where: {
33
+                name_province_id: {
34
+                    name,
35
+                    province_id: province.id,
36
+                },
37
+            },
38
+            update: {
39
+                updatedAt: timeLocal.now().toDate()
40
+            },
41
+            create: {
42
+                name,
43
+                province_id: province.id,
44
+                createdAt: timeLocal.now().toDate()
45
+            },
46
+        });
47
+    }
48
+
49
+    console.log('✅ Maluku City seeded!.');
50
+};

+ 49 - 0
prisma/seeders/city/MalukuUtaraCitySeeder.js

@@ -0,0 +1,49 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Halmahera Barat',
6
+    'Kabupaten Halmahera Tengah',
7
+    'Kabupaten Halmahera Timur',
8
+    'Kabupaten Halmahera Selatan',
9
+    'Kabupaten Halmahera Utara',
10
+    'Kabupaten Kepulauan Sula',
11
+    'Kabupaten Pulau Morotai',
12
+    'Kabupaten Pulau Taliabu',
13
+    'Kota Ternate',
14
+    'Kota Tidore Kepulauan'
15
+
16
+    // 10
17
+];
18
+
19
+exports.seedMalukuUtaraCities = async () => {
20
+    const province = await prisma.province.findFirst({
21
+        where: { name: 'Maluku Utara' },
22
+    });
23
+
24
+    if (!province) {
25
+        console.error('❌ Province Maluku Utara not found. Seed it first.');
26
+        return;
27
+    }
28
+
29
+    for (const name of cityNames) {
30
+        await prisma.city.upsert({
31
+            where: {
32
+                name_province_id: {
33
+                    name,
34
+                    province_id: province.id,
35
+                },
36
+            },
37
+            update: {
38
+                updatedAt: timeLocal.now().toDate()
39
+            },
40
+            create: {
41
+                name,
42
+                province_id: province.id,
43
+                createdAt: timeLocal.now().toDate()
44
+            },
45
+        });
46
+    }
47
+
48
+    console.log('✅ Maluku Utara City seeded!.');
49
+};

+ 49 - 0
prisma/seeders/city/NusaTenggaraBaratCitySeeder.js

@@ -0,0 +1,49 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bima',
6
+    'Kabupaten Dompu',
7
+    'Kabupaten Lombok Barat',
8
+    'Kabupaten Lombok Tengah',
9
+    'Kabupaten Lombok Timur',
10
+    'Kabupaten Lombok Utara',
11
+    'Kabupaten Sumbawa',
12
+    'Kabupaten Sumbawa Barat',
13
+    'Kota Bima',
14
+    'Kota Mataram'
15
+
16
+    // 10
17
+];
18
+
19
+exports.seedNusaTenggaraBaratCities = async () => {
20
+    const province = await prisma.province.findFirst({
21
+        where: { name: 'Nusa Tenggara Barat' },
22
+    });
23
+
24
+    if (!province) {
25
+        console.error('❌ Province Nusa Tenggara Barat not found. Seed it first.');
26
+        return;
27
+    }
28
+
29
+    for (const name of cityNames) {
30
+        await prisma.city.upsert({
31
+            where: {
32
+                name_province_id: {
33
+                    name,
34
+                    province_id: province.id,
35
+                },
36
+            },
37
+            update: {
38
+                updatedAt: timeLocal.now().toDate()
39
+            },
40
+            create: {
41
+                name,
42
+                province_id: province.id,
43
+                createdAt: timeLocal.now().toDate()
44
+            },
45
+        });
46
+    }
47
+
48
+    console.log('✅ Nusa Tenggara Barat City seeded!.');
49
+};

+ 61 - 0
prisma/seeders/city/NusaTenggaraTimurCitySeeder.js

@@ -0,0 +1,61 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Alor',
6
+    'Kabupaten Belu',
7
+    'Kabupaten Ende',
8
+    'Kabupaten Flores Timur',
9
+    'Kabupaten Kupang',
10
+    'Kabupaten Lembata',
11
+    'Kabupaten Malaka',
12
+    'Kabupaten Manggarai',
13
+    'Kabupaten Manggarai Barat',
14
+    'Kabupaten Manggarai Timur',
15
+    'Kabupaten Nagekeo',
16
+    'Kabupaten Ngada',
17
+    'Kabupaten Rote Ndao',
18
+    'Kabupaten Sabu Raijua',
19
+    'Kabupaten Sikka',
20
+    'Kabupaten Sumba Barat',
21
+    'Kabupaten Sumba Barat Daya',
22
+    'Kabupaten Sumba Tengah',
23
+    'Kabupaten Sumba Timur',
24
+    'Kabupaten Timor Tengah Selatan',
25
+    'Kabupaten Timor Tengah Utara',
26
+    'Kota Kupang'
27
+
28
+    // 22
29
+];
30
+
31
+exports.seedNusaTenggaraTimurCities = async () => {
32
+    const province = await prisma.province.findFirst({
33
+        where: { name: 'Nusa Tenggara Timur' },
34
+    });
35
+
36
+    if (!province) {
37
+        console.error('❌ Province Nusa Tenggara Timur not found. Seed it first.');
38
+        return;
39
+    }
40
+
41
+    for (const name of cityNames) {
42
+        await prisma.city.upsert({
43
+            where: {
44
+                name_province_id: {
45
+                    name,
46
+                    province_id: province.id,
47
+                },
48
+            },
49
+            update: {
50
+                updatedAt: timeLocal.now().toDate()
51
+            },
52
+            create: {
53
+                name,
54
+                province_id: province.id,
55
+                createdAt: timeLocal.now().toDate()
56
+            },
57
+        });
58
+    }
59
+
60
+    console.log('✅ Nusa Tenggara Timur City seeded!.');
61
+};

+ 46 - 0
prisma/seeders/city/PapuaBaratCitySeeder.js

@@ -0,0 +1,46 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Fakfak',
6
+    'Kabupaten Kaimana',
7
+    'Kabupaten Manokwari',
8
+    'Kabupaten Manokwari Selatan',
9
+    'Kabupaten Pegunungan Arfak',
10
+    'Kabupaten Teluk Bintuni',
11
+    'Kabupaten Teluk Wondama',
12
+
13
+    // 7
14
+];
15
+
16
+exports.seedPapuaBaratCities = async () => {
17
+    const province = await prisma.province.findFirst({
18
+        where: { name: 'Papua Barat' },
19
+    });
20
+
21
+    if (!province) {
22
+        console.error('❌ Province Papua Barat not found. Seed it first.');
23
+        return;
24
+    }
25
+
26
+    for (const name of cityNames) {
27
+        await prisma.city.upsert({
28
+            where: {
29
+                name_province_id: {
30
+                    name,
31
+                    province_id: province.id,
32
+                },
33
+            },
34
+            update: {
35
+                updatedAt: timeLocal.now().toDate()
36
+            },
37
+            create: {
38
+                name,
39
+                province_id: province.id,
40
+                createdAt: timeLocal.now().toDate()
41
+            },
42
+        });
43
+    }
44
+
45
+    console.log('✅ Papua Barat City seeded!.');
46
+};

+ 45 - 0
prisma/seeders/city/PapuaBaratDayaCitySeeder.js

@@ -0,0 +1,45 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Maybrat',
6
+    'Kabupaten Sorong',
7
+    'Kabupaten Sorong Selatan',
8
+    'Kabupaten Tambrauw',
9
+    'Kabupaten Raja Ampat',
10
+    'Kota Sorong'
11
+
12
+    // 6
13
+];
14
+
15
+exports.seedPapuaBaratDayaCities = async () => {
16
+    const province = await prisma.province.findFirst({
17
+        where: { name: 'Papua Barat Daya' },
18
+    });
19
+
20
+    if (!province) {
21
+        console.error('❌ Province Papua Barat Daya not found. Seed it first.');
22
+        return;
23
+    }
24
+
25
+    for (const name of cityNames) {
26
+        await prisma.city.upsert({
27
+            where: {
28
+                name_province_id: {
29
+                    name,
30
+                    province_id: province.id,
31
+                },
32
+            },
33
+            update: {
34
+                updatedAt: timeLocal.now().toDate()
35
+            },
36
+            create: {
37
+                name,
38
+                province_id: province.id,
39
+                createdAt: timeLocal.now().toDate()
40
+            },
41
+        });
42
+    }
43
+
44
+    console.log('✅ Papua Barat Daya City seeded!.');
45
+};

+ 46 - 0
prisma/seeders/city/PapuaCitySeeder.js

@@ -0,0 +1,46 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Biak Numfor',
6
+    'Kabupaten Jayapura',
7
+    'Kabupaten Keerom',
8
+    'Kabupaten Kepulauan Yapen',
9
+    'Kabupaten Mamberamo Raya',
10
+    'Kabupaten Sarmi',
11
+    'Kabupaten Supiori',
12
+    'Kabupaten Waropen',
13
+    'Kota Jayapura'
14
+
15
+    // 9
16
+];
17
+
18
+exports.seedPapuaCities = async () => {
19
+    const province = await prisma.province.findFirst({
20
+        where: { name: 'Papua' },
21
+    });
22
+
23
+    if (!province) {
24
+        console.error('❌ Province Papua not found. Seed it first.');
25
+        return;
26
+    }
27
+
28
+    for (const name of cityNames) {
29
+        await prisma.city.upsert({
30
+            where: {
31
+                name_province_id: {
32
+                    name,
33
+                    province_id: province.id,
34
+                },
35
+            },
36
+            update: { updatedAt: timeLocal.now().toDate() },
37
+            create: {
38
+                name,
39
+                province_id: province.id,
40
+                createdAt: timeLocal.now().toDate()
41
+            },
42
+        });
43
+    }
44
+
45
+    console.log('✅ Papua City seeded!.');
46
+};

+ 45 - 0
prisma/seeders/city/PapuaPegununganCitySeeder.js

@@ -0,0 +1,45 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Jayawijaya',
6
+    'Kabupaten Lanny Jaya',
7
+    'Kabupaten Mamberamo Tengah',
8
+    'Kabupaten Nduga',
9
+    'Kabupaten Pegunungan Bintang',
10
+    'Kabupaten Tolikara',
11
+    'Kabupaten Yahukimo',
12
+    'Kabupaten Yalimo'
13
+
14
+    // 8
15
+];
16
+
17
+exports.seedPapuaPegununganCities = async () => {
18
+    const province = await prisma.province.findFirst({
19
+        where: { name: 'Papua Pegunungan' },
20
+    });
21
+
22
+    if (!province) {
23
+        console.error('❌ Province Papua Pegunungan not found. Seed it first.');
24
+        return;
25
+    }
26
+
27
+    for (const name of cityNames) {
28
+        await prisma.city.upsert({
29
+            where: {
30
+                name_province_id: {
31
+                    name,
32
+                    province_id: province.id,
33
+                },
34
+            },
35
+            update: { updatedAt: timeLocal.now().toDate() },
36
+            create: {
37
+                name,
38
+                province_id: province.id,
39
+                createdAt: timeLocal.now().toDate()
40
+            },
41
+        });
42
+    }
43
+
44
+    console.log('✅ Papua Pegunungan City seeded!.');
45
+};

+ 41 - 0
prisma/seeders/city/PapuaSelatanCitySeeder.js

@@ -0,0 +1,41 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Asmat',
6
+    'Kabupaten Boven Digoel',
7
+    'Kabupaten Mappi',
8
+    'Kabupaten Merauke'
9
+
10
+    // 4
11
+];
12
+
13
+exports.seedPapuaSelatanCities = async () => {
14
+    const province = await prisma.province.findFirst({
15
+        where: { name: 'Papua Selatan' },
16
+    });
17
+
18
+    if (!province) {
19
+        console.error('❌ Province Papua Selatan not found. Seed it first.');
20
+        return;
21
+    }
22
+
23
+    for (const name of cityNames) {
24
+        await prisma.city.upsert({
25
+            where: {
26
+                name_province_id: {
27
+                    name,
28
+                    province_id: province.id,
29
+                },
30
+            },
31
+            update: { updatedAt: timeLocal.now().toDate() },
32
+            create: {
33
+                name,
34
+                province_id: province.id,
35
+                createdAt: timeLocal.now().toDate()
36
+            },
37
+        });
38
+    }
39
+
40
+    console.log('✅ Papua Selatan City seeded!.');
41
+};

+ 45 - 0
prisma/seeders/city/PapuaTengahCitySeeder.js

@@ -0,0 +1,45 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Deiyai',
6
+    'Kabupaten Dogiyai',
7
+    'Kabupaten Intan Jaya',
8
+    'Kabupaten Mimika',
9
+    'Kabupaten Nabire',
10
+    'Kabupaten Paniai',
11
+    'Kabupaten Puncak',
12
+    'Kabupaten Puncak Jaya'
13
+
14
+    // 8
15
+];
16
+
17
+exports.seedPapuaTengahCities = async () => {
18
+    const province = await prisma.province.findFirst({
19
+        where: { name: 'Papua Tengah' },
20
+    });
21
+
22
+    if (!province) {
23
+        console.error('❌ Province Papua Tengah not found. Seed it first.');
24
+        return;
25
+    }
26
+
27
+    for (const name of cityNames) {
28
+        await prisma.city.upsert({
29
+            where: {
30
+                name_province_id: {
31
+                    name,
32
+                    province_id: province.id,
33
+                },
34
+            },
35
+            update: { updatedAt: timeLocal.now().toDate() },
36
+            create: {
37
+                name,
38
+                province_id: province.id,
39
+                createdAt: timeLocal.now().toDate()
40
+            },
41
+        });
42
+    }
43
+
44
+    console.log('✅ Papua Tengah City seeded!.');
45
+};

+ 49 - 0
prisma/seeders/city/RiauCitySeeder.js

@@ -0,0 +1,49 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bengkalis',
6
+    'Kabupaten Indragiri Hilir',
7
+    'Kabupaten Indragiri Hulu',
8
+    'Kabupaten Kampar',
9
+    'Kabupaten Kepulauan Meranti',
10
+    'Kabupaten Kuantan Singingi',
11
+    'Kabupaten Pelalawan',
12
+    'Kabupaten Rokan Hilir',
13
+    'Kabupaten Rokan Hulu',
14
+    'Kabupaten Siak',
15
+    'Kota Dumai',
16
+    'Kota Pekanbaru'
17
+
18
+    // 12
19
+];
20
+
21
+exports.seedRiauCities = async () => {
22
+    const province = await prisma.province.findFirst({
23
+        where: { name: 'Riau' },
24
+    });
25
+
26
+    if (!province) {
27
+        console.error('❌ Province Riau not found. Seed it first.');
28
+        return;
29
+    }
30
+
31
+    for (const name of cityNames) {
32
+        await prisma.city.upsert({
33
+            where: {
34
+                name_province_id: {
35
+                    name,
36
+                    province_id: province.id,
37
+                },
38
+            },
39
+            update: { updatedAt: timeLocal.now().toDate() },
40
+            create: {
41
+                name,
42
+                province_id: province.id,
43
+                createdAt: timeLocal.now().toDate()
44
+            },
45
+        });
46
+    }
47
+
48
+    console.log('✅ Riau City seeded!.');
49
+};

+ 43 - 0
prisma/seeders/city/SulawesiBaratCitySeeder.js

@@ -0,0 +1,43 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Majene',
6
+    'Kabupaten Mamasa',
7
+    'Kabupaten Mamuju',
8
+    'Kabupaten Mamuju Tengah',
9
+    'Kabupaten Pasangkayu',
10
+    'Kabupaten Polewali Mandar'
11
+
12
+    // 6
13
+];
14
+
15
+exports.seedSulawesiBaratCities = async () => {
16
+    const province = await prisma.province.findFirst({
17
+        where: { name: 'Sulawesi Barat' },
18
+    });
19
+
20
+    if (!province) {
21
+        console.error('❌ Province Sulawesi Barat not found. Seed it first.');
22
+        return;
23
+    }
24
+
25
+    for (const name of cityNames) {
26
+        await prisma.city.upsert({
27
+            where: {
28
+                name_province_id: {
29
+                    name,
30
+                    province_id: province.id,
31
+                },
32
+            },
33
+            update: { updatedAt: timeLocal.now().toDate() },
34
+            create: {
35
+                name,
36
+                province_id: province.id,
37
+                createdAt: timeLocal.now().toDate()
38
+            },
39
+        });
40
+    }
41
+
42
+    console.log('✅ Sulawesi Barat City seeded!.');
43
+};

+ 61 - 0
prisma/seeders/city/SulawesiSelatanCitySeeder.js

@@ -0,0 +1,61 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bantaeng',
6
+    'Kabupaten Barru',
7
+    'Kabupaten Bone',
8
+    'Kabupaten Bulukumba',
9
+    'Kabupaten Enrekang',
10
+    'Kabupaten Gowa',
11
+    'Kabupaten Jeneponto',
12
+    'Kabupaten Kepulauan Selayar',
13
+    'Kabupaten Luwu',
14
+    'Kabupaten Luwu Timur',
15
+    'Kabupaten Luwu Utara',
16
+    'Kabupaten Maros',
17
+    'Kabupaten Pangkajene dan Kepulauan',
18
+    'Kabupaten Pinrang',
19
+    'Kabupaten Sidenreng Rappang',
20
+    'Kabupaten Sinjai',
21
+    'Kabupaten Soppeng',
22
+    'Kabupaten Takalar',
23
+    'Kabupaten Tana Toraja',
24
+    'Kabupaten Toraja Utara',
25
+    'Kabupaten Wajo',
26
+    'Kota Makassar',
27
+    'Kota Palopo',
28
+    'Kota Parepare'
29
+
30
+    // 24
31
+];
32
+
33
+exports.seedSulawesiSelatanCities = async () => {
34
+    const province = await prisma.province.findFirst({
35
+        where: { name: 'Sulawesi Selatan' },
36
+    });
37
+
38
+    if (!province) {
39
+        console.error('❌ Province Sulawesi Selatan not found. Seed it first.');
40
+        return;
41
+    }
42
+
43
+    for (const name of cityNames) {
44
+        await prisma.city.upsert({
45
+            where: {
46
+                name_province_id: {
47
+                    name,
48
+                    province_id: province.id,
49
+                },
50
+            },
51
+            update: { updatedAt: timeLocal.now().toDate() },
52
+            create: {
53
+                name,
54
+                province_id: province.id,
55
+                createdAt: timeLocal.now().toDate()
56
+            },
57
+        });
58
+    }
59
+
60
+    console.log('✅ Sulawesi Selatan City seeded!.');
61
+};

+ 50 - 0
prisma/seeders/city/SulawesiTengahCitySeeder.js

@@ -0,0 +1,50 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Banggai',
6
+    'Kabupaten Banggai Kepulauan',
7
+    'Kabupaten Banggai Laut',
8
+    'Kabupaten Buol',
9
+    'Kabupaten Donggala',
10
+    'Kabupaten Morowali',
11
+    'Kabupaten Morowali Utara',
12
+    'Kabupaten Parigi Moutong',
13
+    'Kabupaten Poso',
14
+    'Kabupaten Sigi',
15
+    'Kabupaten Tojo Una-Una',
16
+    'Kabupaten Tolitoli',
17
+    'Kota Palu'
18
+
19
+    // 13
20
+];
21
+
22
+exports.seedSulawesiTengahCities = async () => {
23
+    const province = await prisma.province.findFirst({
24
+        where: { name: 'Sulawesi Tengah' },
25
+    });
26
+
27
+    if (!province) {
28
+        console.error('❌ Province Sulawesi Tengah not found. Seed it first.');
29
+        return;
30
+    }
31
+
32
+    for (const name of cityNames) {
33
+        await prisma.city.upsert({
34
+            where: {
35
+                name_province_id: {
36
+                    name,
37
+                    province_id: province.id,
38
+                },
39
+            },
40
+            update: { updatedAt: timeLocal.now().toDate() },
41
+            create: {
42
+                name,
43
+                province_id: province.id,
44
+                createdAt: timeLocal.now().toDate()
45
+            },
46
+        });
47
+    }
48
+
49
+    console.log('✅ Sulawesi Tengah City seeded!.');
50
+};

+ 54 - 0
prisma/seeders/city/SulawesiTenggaraCitySeeder.js

@@ -0,0 +1,54 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bombana',
6
+    'Kabupaten Buton',
7
+    'Kabupaten Buton Selatan',
8
+    'Kabupaten Buton Tengah',
9
+    'Kabupaten Buton Utara',
10
+    'Kabupaten Kolaka',
11
+    'Kabupaten Kolaka Timur',
12
+    'Kabupaten Kolaka Utara',
13
+    'Kabupaten Konawe',
14
+    'Kabupaten Konawe Kepulauan',
15
+    'Kabupaten Konawe Selatan',
16
+    'Kabupaten Konawe Utara',
17
+    'Kabupaten Muna',
18
+    'Kabupaten Muna Barat',
19
+    'Kabupaten Wakatobi',
20
+    'Kota Baubau',
21
+    'Kota Kendari'
22
+
23
+    // 17
24
+];
25
+
26
+exports.seedSulawesiTenggaraCities = async () => {
27
+    const province = await prisma.province.findFirst({
28
+        where: { name: 'Sulawesi Tenggara' },
29
+    });
30
+
31
+    if (!province) {
32
+        console.error('❌ Province Sulawesi Tenggara not found. Seed it first.');
33
+        return;
34
+    }
35
+
36
+    for (const name of cityNames) {
37
+        await prisma.city.upsert({
38
+            where: {
39
+                name_province_id: {
40
+                    name,
41
+                    province_id: province.id,
42
+                },
43
+            },
44
+            update: { updatedAt: timeLocal.now().toDate() },
45
+            create: {
46
+                name,
47
+                province_id: province.id,
48
+                createdAt: timeLocal.now().toDate()
49
+            },
50
+        });
51
+    }
52
+
53
+    console.log('✅ Sulawesi Tenggara City seeded!.');
54
+};

+ 52 - 0
prisma/seeders/city/SulawesiUtaraCitySeeder.js

@@ -0,0 +1,52 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Bolaang Mongondow',
6
+    'Kabupaten Bolaang Mongondow Selatan',
7
+    'Kabupaten Bolaang Mongondow Timur',
8
+    'Kabupaten Bolaang Mongondow Utara',
9
+    'Kabupaten Kepulauan Sangihe',
10
+    'Kabupaten Kepulauan Siau Tagulandang Biaro (Sitaro)',
11
+    'Kabupaten Kepulauan Talaud',
12
+    'Kabupaten Minahasa',
13
+    'Kabupaten Minahasa Selatan',
14
+    'Kabupaten Minahasa Tenggara',
15
+    'Kabupaten Minahasa Utara',
16
+    'Kota Bitung',
17
+    'Kota Kotamobagu',
18
+    'Kota Manado',
19
+    'Kota Tomohon'
20
+
21
+    // 15
22
+];
23
+
24
+exports.seedSulawesiUtaraCities = async () => {
25
+    const province = await prisma.province.findFirst({
26
+        where: { name: 'Sulawesi Utara' },
27
+    });
28
+
29
+    if (!province) {
30
+        console.error('❌ Province Sulawesi Utara not found. Seed it first.');
31
+        return;
32
+    }
33
+
34
+    for (const name of cityNames) {
35
+        await prisma.city.upsert({
36
+            where: {
37
+                name_province_id: {
38
+                    name,
39
+                    province_id: province.id,
40
+                },
41
+            },
42
+            update: { updatedAt: timeLocal.now().toDate() },
43
+            create: {
44
+                name,
45
+                province_id: province.id,
46
+                createdAt: timeLocal.now().toDate()
47
+            },
48
+        });
49
+    }
50
+
51
+    console.log('✅ Sulawesi Utara City seeded!.');
52
+};

+ 56 - 0
prisma/seeders/city/SumateraBaratCitySeeder.js

@@ -0,0 +1,56 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Agam',
6
+    'Kabupaten Dharmasraya',
7
+    'Kabupaten Kepulauan Mentawai',
8
+    'Kabupaten Lima Puluh Kota',
9
+    'Kabupaten Padang Pariaman',
10
+    'Kabupaten Pasaman',
11
+    'Kabupaten Pasaman Barat',
12
+    'Kabupaten Pesisir Selatan',
13
+    'Kabupaten Sijunjung',
14
+    'Kabupaten Solok',
15
+    'Kabupaten Solok Selatan',
16
+    'Kabupaten Tanah Datar',
17
+    'Kota Bukittinggi',
18
+    'Kota Padang',
19
+    'Kota Padang Panjang',
20
+    'Kota Pariaman',
21
+    'Kota Payakumbuh',
22
+    'Kota Sawahlunto',
23
+    'Kota Solok'
24
+
25
+    // 19
26
+];
27
+
28
+exports.seedSumateraBaratCities = async () => {
29
+    const province = await prisma.province.findFirst({
30
+        where: { name: 'Sumatera Barat' },
31
+    });
32
+
33
+    if (!province) {
34
+        console.error('❌ Province Sumatera Barat not found. Seed it first.');
35
+        return;
36
+    }
37
+
38
+    for (const name of cityNames) {
39
+        await prisma.city.upsert({
40
+            where: {
41
+                name_province_id: {
42
+                    name,
43
+                    province_id: province.id,
44
+                },
45
+            },
46
+            update: { updatedAt: timeLocal.now().toDate() },
47
+            create: {
48
+                name,
49
+                province_id: province.id,
50
+                createdAt: timeLocal.now().toDate()
51
+            },
52
+        });
53
+    }
54
+
55
+    console.log('✅ Sumatera Barat City seeded!.');
56
+};

+ 54 - 0
prisma/seeders/city/SumateraSelatanCitySeeder.js

@@ -0,0 +1,54 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Banyuasin',
6
+    'Kabupaten Empat Lawang',
7
+    'Kabupaten Lahat',
8
+    'Kabupaten Muara Enim',
9
+    'Kabupaten Musi Banyuasin',
10
+    'Kabupaten Musi Rawas',
11
+    'Kabupaten Musi Rawas Utara',
12
+    'Kabupaten Ogan Ilir',
13
+    'Kabupaten Ogan Komering Ilir',
14
+    'Kabupaten Ogan Komering Ulu',
15
+    'Kabupaten Ogan Komering Ulu Selatan',
16
+    'Kabupaten Ogan Komering Ulu Timur',
17
+    'Kabupaten Penukal Abab Lematang Ilir',
18
+    'Kota Lubuklinggau',
19
+    'Kota Pagar Alam',
20
+    'Kota Palembang',
21
+    'Kota Prabumulih'
22
+
23
+    // 17
24
+];
25
+
26
+exports.seedSumateraSelatanCities = async () => {
27
+    const province = await prisma.province.findFirst({
28
+        where: { name: 'Sumatera Selatan' },
29
+    });
30
+
31
+    if (!province) {
32
+        console.error('❌ Province Sumatera Selatan not found. Seed it first.');
33
+        return;
34
+    }
35
+
36
+    for (const name of cityNames) {
37
+        await prisma.city.upsert({
38
+            where: {
39
+                name_province_id: {
40
+                    name,
41
+                    province_id: province.id,
42
+                },
43
+            },
44
+            update: { updatedAt: timeLocal.now().toDate() },
45
+            create: {
46
+                name,
47
+                province_id: province.id,
48
+                createdAt: timeLocal.now().toDate()
49
+            },
50
+        });
51
+    }
52
+
53
+    console.log('✅ Sumatera Selatan City seeded!.');
54
+};

+ 70 - 0
prisma/seeders/city/SumateraUtaraCitySeeder.js

@@ -0,0 +1,70 @@
1
+const prisma = require('../../../src/prisma/PrismaClient.js');
2
+const timeLocal = require('../../../src/utils/TimeLocal.js')
3
+
4
+const cityNames = [
5
+    'Kabupaten Asahan',
6
+    'Kabupaten Batu Bara',
7
+    'Kabupaten Dairi',
8
+    'Kabupaten Deli Serdang',
9
+    'Kabupaten Humbang Hasundutan',
10
+    'Kabupaten Karo',
11
+    'Kabupaten Labuhanbatu',
12
+    'Kabupaten Labuhanbatu Selatan',
13
+    'Kabupaten Labuhanbatu Utara',
14
+    'Kabupaten Langkat',
15
+    'Kabupaten Mandailing Natal',
16
+    'Kabupaten Nias',
17
+    'Kabupaten Nias Barat',
18
+    'Kabupaten Nias Selatan',
19
+    'Kabupaten Nias Utara',
20
+    'Kabupaten Padang Lawas',
21
+    'Kabupaten Padang Lawas Utara',
22
+    'Kabupaten Pakpak Bharat',
23
+    'Kabupaten Samosir',
24
+    'Kabupaten Serdang Bedagai',
25
+    'Kabupaten Simalungun',
26
+    'Kabupaten Tapanuli Selatan',
27
+    'Kabupaten Tapanuli Tengah',
28
+    'Kabupaten Tapanuli Utara',
29
+    'Kabupaten Toba',
30
+    'Kota Binjai',
31
+    'Kota Gunungsitoli',
32
+    'Kota Medan',
33
+    'Kota Padangsidimpuan',
34
+    'Kota Pematangsiantar',
35
+    'Kota Sibolga',
36
+    'Kota Tanjungbalai',
37
+    'Kota Tebing Tinggi'
38
+
39
+    // 33
40
+];
41
+
42
+exports.seedSumateraUtaraCities = async () => {
43
+    const province = await prisma.province.findFirst({
44
+        where: { name: 'Sumatera Utara' },
45
+    });
46
+
47
+    if (!province) {
48
+        console.error('❌ Province Sumatera Utara not found. Seed it first.');
49
+        return;
50
+    }
51
+
52
+    for (const name of cityNames) {
53
+        await prisma.city.upsert({
54
+            where: {
55
+                name_province_id: {
56
+                    name,
57
+                    province_id: province.id,
58
+                },
59
+            },
60
+            update: { updatedAt: timeLocal.now().toDate() },
61
+            create: {
62
+                name,
63
+                province_id: province.id,
64
+                createdAt: timeLocal.now().toDate()
65
+            },
66
+        });
67
+    }
68
+
69
+    console.log('✅ Sumatera Utara City seeded!.');
70
+};

+ 60 - 0
src/controllers/admin/CityController.js

@@ -0,0 +1,60 @@
1
+const cityService = require('../../services/admin/CityService.js');
2
+const { ListResponse } = require('../../utils/ListResponse.js');
3
+const { PaginationParam } = require('../../utils/PaginationParams.js');
4
+const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { validateStoreCityRequest } = require('../../validators/admin/city/CityValidators.js');
6
+
7
+exports.getAllCity = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { cities, total } = await cityService.getAllCityService({
12
+            page, limit, search, sortBy, orderBy
13
+        });
14
+
15
+        return ListResponse({ req, res, data: cities, page, limit, total, message: 'City data successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.showCity = async (req, res) => {
22
+    try {
23
+        const id = req.params.id;
24
+        const data = await cityService.showCityService(id);
25
+        return successResponse(res, data, 'Success show city');
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+};
30
+
31
+exports.storeCity = async (req, res) => {
32
+    try {
33
+        const validatedData = validateStoreCityRequest(req.body);
34
+        await cityService.storeCityService(validatedData, req);
35
+        return messageSuccessResponse(res, 'Success added city', 201);
36
+    } catch (err) {
37
+        return errorResponse(res, err);
38
+    }
39
+}
40
+
41
+exports.updateCity = async (req, res) => {
42
+    try {
43
+        const id = req.params.id;
44
+        const validatedData = validateStoreCityRequest(req.body);
45
+        await cityService.updateCityService(validatedData, id, req);
46
+        return messageSuccessResponse(res, 'Success update city');
47
+    } catch (err) {
48
+        return errorResponse(res, err);
49
+    }
50
+}
51
+
52
+exports.deleteCity = async (req, res) => {
53
+    try {
54
+        const id = req.params.id;
55
+        await cityService.deleteCityService(id, req);
56
+        return messageSuccessResponse(res, 'Success delete city');
57
+    } catch (err) {
58
+        return errorResponse(res, err);
59
+    }
60
+};

+ 60 - 0
src/controllers/admin/HospitalController.js

@@ -0,0 +1,60 @@
1
+const hospitalService = require('../../services/admin/HospitalService.js');
2
+const { ListResponse } = require('../../utils/ListResponse.js');
3
+const { PaginationParam } = require('../../utils/PaginationParams.js');
4
+const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { validateStoreHospitalRequest, validateUpdateHospitalRequest } = require('../../validators/admin/hospital/HospitalValidators.js');
6
+
7
+exports.getAllHospital = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { hospitals, total } = await hospitalService.getAllHospitalService({
12
+            page, limit, search, sortBy, orderBy
13
+        });
14
+
15
+        return ListResponse({ req, res, data: hospitals, page, limit, total, message: 'Hospital data successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.showHospital = async (req, res) => {
22
+    try {
23
+        const id = req.params.id;
24
+        const data = await hospitalService.showHospitalService(id);
25
+        return successResponse(res, data, 'Success show hospital');
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+};
30
+
31
+exports.storeHospital = async (req, res) => {
32
+    try {
33
+        const validatedData = validateStoreHospitalRequest(req.body);
34
+        await hospitalService.storeHospitalService(validatedData, req);
35
+        return messageSuccessResponse(res, 'Success added hospital', 201);
36
+    } catch (err) {
37
+        return errorResponse(res, err);
38
+    }
39
+}
40
+
41
+exports.updateHospital = async (req, res) => {
42
+    try {
43
+        const id = req.params.id;
44
+        const validatedData = validateUpdateHospitalRequest(req.body);
45
+        await hospitalService.updateHospitalService(validatedData, id, req);
46
+        return messageSuccessResponse(res, 'Success update hospital');
47
+    } catch (err) {
48
+        return errorResponse(res, err);
49
+    }
50
+}
51
+
52
+exports.deleteHospital = async (req, res) => {
53
+    try {
54
+        const id = req.params.id;
55
+        await hospitalService.deleteHospitalService(id, req);
56
+        return messageSuccessResponse(res, 'Success delete hospital');
57
+    } catch (err) {
58
+        return errorResponse(res, err);
59
+    }
60
+};

+ 60 - 0
src/controllers/admin/ProvinceController.js

@@ -0,0 +1,60 @@
1
+const provinceService = require('../../services/admin/ProvinceService.js');
2
+const { ListResponse } = require('../../utils/ListResponse.js');
3
+const { PaginationParam } = require('../../utils/PaginationParams.js');
4
+const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { validateStoreProvinceRequest } = require('../../validators/admin/province/ProvinceValidators.js');
6
+
7
+exports.getAllProvince = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { provinces, total } = await provinceService.getAllProvinceService({
12
+            page, limit, search, sortBy, orderBy
13
+        });
14
+
15
+        return ListResponse({ req, res, data: provinces, page, limit, total, message: 'Province data successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.showProvince = async (req, res) => {
22
+    try {
23
+        const id = req.params.id;
24
+        const data = await provinceService.showProvinceService(id);
25
+        return successResponse(res, data, 'Success show province');
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+};
30
+
31
+exports.storeProvince = async (req, res) => {
32
+    try {
33
+        const validatedData = validateStoreProvinceRequest(req.body);
34
+        await provinceService.storeProvinceService(validatedData, req);
35
+        return messageSuccessResponse(res, 'Success added province', 201);
36
+    } catch (err) {
37
+        return errorResponse(res, err);
38
+    }
39
+}
40
+
41
+exports.updateProvince = async (req, res) => {
42
+    try {
43
+        const id = req.params.id;
44
+        const validatedData = validateStoreProvinceRequest(req.body);
45
+        await provinceService.updateProvinceService(validatedData, id, req);
46
+        return messageSuccessResponse(res, 'Success update province');
47
+    } catch (err) {
48
+        return errorResponse(res, err);
49
+    }
50
+}
51
+
52
+exports.deleteProvince = async (req, res) => {
53
+    try {
54
+        const id = req.params.id;
55
+        await provinceService.deleteProvinceService(id, req);
56
+        return messageSuccessResponse(res, 'Success delete province');
57
+    } catch (err) {
58
+        return errorResponse(res, err);
59
+    }
60
+};

+ 60 - 0
src/controllers/admin/SalesController.js

@@ -0,0 +1,60 @@
1
+const salesService = require('../../services/admin/SalesService.js');
2
+const { ListResponse } = require('../../utils/ListResponse.js');
3
+const { PaginationParam } = require('../../utils/PaginationParams.js');
4
+const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { validateCreateSalesRequest, validateUpdateSalesRequest } = require('../../validators/admin/sales/SalesValidators.js');
6
+
7
+exports.getAllSales = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { sales, total } = await salesService.getAllSalesService({
12
+            page, limit, search, sortBy, orderBy
13
+        });
14
+
15
+        return ListResponse({ req, res, data: sales, page, limit, total, message: 'Sales data successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.showSales = async (req, res) => {
22
+    try {
23
+        const id = req.params.id;
24
+        const data = await salesService.showSalesService(id);
25
+        return successResponse(res, data, 'Success show sales');
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+};
30
+
31
+exports.storeSales = async (req, res) => {
32
+    try {
33
+        const validated = validateCreateSalesRequest(req.body);
34
+        await salesService.storeSalesService(validated, req);
35
+        return messageSuccessResponse(res, 'Sales user created successfully', 201);
36
+    } catch (err) {
37
+        return errorResponse(res, err);
38
+    }
39
+}
40
+
41
+exports.updateSales = async (req, res) => {
42
+    try {
43
+        const id = req.params.id;
44
+        const validatedData = validateUpdateSalesRequest(req.body);
45
+        await salesService.updateSalesService(validatedData, id, req);
46
+        return messageSuccessResponse(res, 'Success update sales');
47
+    } catch (err) {
48
+        return errorResponse(res, err);
49
+    }
50
+}
51
+
52
+exports.deleteSales = async (req, res) => {
53
+    try {
54
+        const id = req.params.id;
55
+        await salesService.deleteSalesService(id, req);
56
+        return messageSuccessResponse(res, 'Success delete sales');
57
+    } catch (err) {
58
+        return errorResponse(res, err);
59
+    }
60
+};

+ 31 - 0
src/controllers/auth/AuthControllers.js

@@ -0,0 +1,31 @@
1
+const { loginService, getUserService, logoutService } = require('../../services/auth/AuthService.js');
2
+const { successResponse, errorResponse, messageSuccessResponse } = require('../../utils/Response.js');
3
+const { validateLoginRequest } = require('../../validators/auth/LoginValidators.js');
4
+
5
+exports.login = async (req, res) => {
6
+    try {
7
+        const validated = validateLoginRequest(req.body);
8
+        const { user, token } = await loginService(validated, req);
9
+        return successResponse(res, { ...user, token }, 'Login success');
10
+    } catch (error) {
11
+        return errorResponse(res, error);
12
+    }
13
+};
14
+
15
+exports.getUser = async (req, res) => {
16
+    try {
17
+        const data = await getUserService(req);
18
+        return successResponse(res, data, 'Success get user');
19
+    } catch (error) {
20
+        return errorResponse(res, error);
21
+    }
22
+};
23
+
24
+exports.logout = async (req, res) => {
25
+    try {
26
+        await logoutService(req);
27
+        return messageSuccessResponse(res, 'Logout success', 200);
28
+    } catch (error) {
29
+        return errorResponse(res, error);
30
+    }
31
+}

+ 48 - 0
src/controllers/sales/HospitalController.js

@@ -0,0 +1,48 @@
1
+const { getAllHospitalByAreaService, storeHospitalService, updateHospitalService, showHospitalService } = require("../../services/sales/HospitalService");
2
+const { ListResponse } = require("../../utils/ListResponse");
3
+const { PaginationParam } = require("../../utils/PaginationParams");
4
+const { errorResponse, messageSuccessResponse, successResponse } = require("../../utils/Response");
5
+const { validateStoreHospitalRequest, validateUpdateHospitalRequest } = require('../../validators/admin/hospital/HospitalValidators.js');
6
+
7
+exports.getAllHospitalByArea = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { hospitals, total } = await getAllHospitalByAreaService({ page, limit, search, sortBy, orderBy, }, req);
12
+
13
+        return ListResponse({ req, res, data: hospitals, page, limit, total, message: 'Hospital data successfully retrieved' });
14
+    } catch (err) {
15
+        return errorResponse(res, err);
16
+    }
17
+};
18
+
19
+exports.storeHospital = async (req, res) => {
20
+    try {
21
+        const validatedData = validateStoreHospitalRequest(req.body);
22
+        await storeHospitalService(validatedData, req);
23
+        return messageSuccessResponse(res, 'Success added hospital', 201);
24
+    } catch (err) {
25
+        return errorResponse(res, err);
26
+    }
27
+}
28
+
29
+exports.updateHospital = async (req, res) => {
30
+    try {
31
+        const id = req.params.id;
32
+        const validatedData = validateUpdateHospitalRequest(req.body);
33
+        await updateHospitalService(validatedData, id, req);
34
+        return messageSuccessResponse(res, 'Success update hospital');
35
+    } catch (err) {
36
+        return errorResponse(res, err);
37
+    }
38
+}
39
+
40
+exports.showHospital = async (req, res) => {
41
+    try {
42
+        const id = req.params.id;
43
+        const data = await showHospitalService(id);
44
+        return successResponse(res, data, 'Success show hospital');
45
+    } catch (err) {
46
+        return errorResponse(res, err);
47
+    }
48
+};

+ 60 - 0
src/controllers/superadmin/AdminController.js

@@ -0,0 +1,60 @@
1
+const adminService = require('../../services/superadmin/AdminService.js');
2
+const { ListResponse } = require('../../utils/ListResponse.js');
3
+const { PaginationParam } = require('../../utils/PaginationParams.js');
4
+const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { validateCreateAdminRequest, validateUpdateAdminRequest } = require('../../validators/superadmin/admin/AdminValidators.js');
6
+
7
+exports.getAllAdmin = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { admin, total } = await adminService.getAllAdminService({
12
+            page, limit, search, sortBy, orderBy
13
+        });
14
+
15
+        return ListResponse({ req, res, data: admin, page, limit, total, message: 'Admin data successfully retrieved' });
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.showAdmin = async (req, res) => {
22
+    try {
23
+        const id = req.params.id;
24
+        const data = await adminService.showAdminService(id);
25
+        return successResponse(res, data, 'Success show admin');
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+};
30
+
31
+exports.storeAdmin = async (req, res) => {
32
+    try {
33
+        const validated = validateCreateAdminRequest(req.body);
34
+        await adminService.storeAdminService(validated, req);
35
+        return messageSuccessResponse(res, 'Admin user created successfully', 201);
36
+    } catch (err) {
37
+        return errorResponse(res, err);
38
+    }
39
+}
40
+
41
+exports.updateAdmin = async (req, res) => {
42
+    try {
43
+        const id = req.params.id;
44
+        const validatedData = validateUpdateAdminRequest(req.body);
45
+        await adminService.updateAdminService(validatedData, id, req);
46
+        return messageSuccessResponse(res, 'Success update admin');
47
+    } catch (err) {
48
+        return errorResponse(res, err);
49
+    }
50
+}
51
+
52
+exports.deleteAdmin = async (req, res) => {
53
+    try {
54
+        const id = req.params.id;
55
+        await adminService.deleteAdminService(id, req);
56
+        return messageSuccessResponse(res, 'Success delete admin');
57
+    } catch (err) {
58
+        return errorResponse(res, err);
59
+    }
60
+};

+ 18 - 0
src/middleware/CheckRole.js

@@ -0,0 +1,18 @@
1
+// src/middleware/checkRole.js
2
+const HttpException = require("../utils/HttpException");
3
+
4
+const checkRole = (requiredRoles = []) => {
5
+    return (req, res, next) => {
6
+        const userRoles = req.user?.roles || [];
7
+
8
+        const hasRole = requiredRoles.some(role => userRoles.includes(role));
9
+
10
+        if (!hasRole) {
11
+            throw new HttpException("You don't have permission to access this resource.", 403);
12
+        }
13
+
14
+        next();
15
+    };
16
+};
17
+
18
+module.exports = checkRole;

+ 19 - 0
src/middleware/ErrorHandler.js

@@ -0,0 +1,19 @@
1
+// module.exports = (err, req, res, next) => {
2
+//     const status = err.statusCode || 500;
3
+//     // const message = err.message || 'Internal Server Error';
4
+//     const message = Array.isArray(err.message) ? err.message : [err.message] || 'Internal Server Error';
5
+
6
+//     res.status(status).json({
7
+//         success: false,
8
+//         message,
9
+//     });
10
+// };
11
+
12
+module.exports = (err, req, res, next) => {
13
+    const status = err.statusCode || 500;
14
+
15
+    res.status(status).json({
16
+        success: false,
17
+        message: err.message || 'Internal Server Error',
18
+    });
19
+};

+ 24 - 0
src/middleware/UploadImage.js

@@ -0,0 +1,24 @@
1
+const multer = require('multer');
2
+const path = require('path');
3
+const fs = require('fs');
4
+
5
+// Pastikan folder storage/img ada
6
+const storagePath = path.join(__dirname, '../../storage/img');
7
+if (!fs.existsSync(storagePath)) {
8
+    fs.mkdirSync(storagePath, { recursive: true });
9
+}
10
+
11
+const storage = multer.diskStorage({
12
+    destination: (req, file, cb) => {
13
+        cb(null, storagePath);
14
+    },
15
+    filename: (req, file, cb) => {
16
+        const ext = path.extname(file.originalname);
17
+        const filename = `${Date.now()}-${Math.round(Math.random() * 1E9)}${ext}`;
18
+        cb(null, filename);
19
+    }
20
+});
21
+
22
+const upload = multer({ storage });
23
+
24
+module.exports = upload;

+ 44 - 0
src/middleware/VerifyJWT.js

@@ -0,0 +1,44 @@
1
+const jwt = require('jsonwebtoken');
2
+const { JWT_SECRET } = require('../../config/keycloak.js');
3
+const prisma = require('../prisma/PrismaClient.js');
4
+
5
+const errorMessage = async (res, message, status_code) => {
6
+    return res.status(status_code).json({ status: false, message })
7
+}
8
+
9
+const verifyJWT = async (req, res, next) => {
10
+    const authHeader = req.headers.authorization;
11
+
12
+    if (!authHeader || !authHeader.startsWith('Bearer ')) {
13
+        return errorMessage(res, 'Token not provided.', 401);
14
+    }
15
+
16
+    const token = authHeader.split(' ')[1];
17
+
18
+    try {
19
+        const decoded = jwt.verify(token, JWT_SECRET);
20
+
21
+        const revoked = await prisma.revokedToken.findFirst({
22
+            where: { token: token },
23
+        });
24
+
25
+        if (revoked) {
26
+            return errorMessage(res, 'Token has been revoked.', 401);
27
+        }
28
+
29
+        req.user = decoded;
30
+        next();
31
+    } catch (err) {
32
+        if (err.name === 'TokenExpiredError') {
33
+            return errorMessage(res, 'Token has expired.', 401);
34
+        } else if (err.name === 'JsonWebTokenError') {
35
+            return errorMessage(res, 'Invalid token.', 403);
36
+        } else if (err.name === 'NotBeforeError') {
37
+            return errorMessage(res, 'Token not active yet.', 401);
38
+        } else {
39
+            return errorMessage(res, 'Internal server error while verifying token.', 500);
40
+        }
41
+    }
42
+};
43
+
44
+module.exports = verifyJWT;

+ 5 - 0
src/prisma/PrismaClient.js

@@ -0,0 +1,5 @@
1
+const { PrismaClient } = require('@prisma/client');
2
+
3
+const prisma = new PrismaClient();
4
+
5
+module.exports = prisma;

+ 63 - 0
src/repository/admin/CityRepository.js

@@ -0,0 +1,63 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const CityRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return prisma.city.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                name: true,
13
+                // province_id: true,
14
+                createdAt: true,
15
+                updatedAt: true,
16
+                province: {
17
+                    select: {
18
+                        id: true,
19
+                        name: true
20
+                    }
21
+                }
22
+            },
23
+        });
24
+    },
25
+
26
+    countAll: async (where) => {
27
+        return prisma.city.count({ where });
28
+    },
29
+
30
+    findById: async (id) => {
31
+        return prisma.city.findFirst({
32
+            where: {
33
+                id,
34
+                deletedAt: null
35
+            },
36
+            select: {
37
+                id: true,
38
+                name: true,
39
+                createdAt: true,
40
+                updatedAt: true,
41
+                province: {
42
+                    select: {
43
+                        id: true,
44
+                        name: true
45
+                    }
46
+                }
47
+            }
48
+        });
49
+    },
50
+
51
+    create: async (data) => {
52
+        return prisma.city.create({ data });
53
+    },
54
+
55
+    update: async (id, data) => {
56
+        return prisma.city.update({
57
+            where: { id },
58
+            data
59
+        });
60
+    },
61
+};
62
+
63
+module.exports = CityRepository;

+ 87 - 0
src/repository/admin/HospitalRepository.js

@@ -0,0 +1,87 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const HospitalRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return await prisma.hospital.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                name: true,
13
+                hospital_code: true,
14
+                type: true,
15
+                ownership: true,
16
+                province: { select: { id: true, name: true } },
17
+                city: { select: { id: true, name: true } },
18
+                address: true,
19
+                simrs_type: true,
20
+                contact: true,
21
+                image: true,
22
+                progress_status: true,
23
+                note: true,
24
+                user: { select: { id: true, username: true } },
25
+                createdAt: true,
26
+                updatedAt: true,
27
+            }
28
+        });
29
+    },
30
+
31
+    countAll: async (where) => {
32
+        return prisma.hospital.count({ where });
33
+    },
34
+
35
+    findById: async (id) => {
36
+        return prisma.hospital.findFirst({
37
+            where: {
38
+                id,
39
+                deletedAt: null
40
+            },
41
+            select: {
42
+                id: true,
43
+                name: true,
44
+                hospital_code: true,
45
+                type: true,
46
+                ownership: true,
47
+                province: { select: { id: true, name: true } },
48
+                city: { select: { id: true, name: true } },
49
+                address: true,
50
+                simrs_type: true,
51
+                contact: true,
52
+                image: true,
53
+                progress_status: true,
54
+                note: true,
55
+                user: { select: { id: true, username: true } },
56
+                createdAt: true,
57
+                updatedAt: true,
58
+            }
59
+        });
60
+    },
61
+
62
+    create: async (data) => {
63
+        return prisma.hospital.create({ data });
64
+    },
65
+
66
+    update: async (id, data) => {
67
+        return prisma.hospital.update({
68
+            where: { id },
69
+            data: {
70
+                name: data.name,
71
+                hospital_code: data.hospital_code,
72
+                type: data.type,
73
+                ownership: data.ownership,
74
+                province: { connect: { id: data.province_id } },
75
+                city: { connect: { id: data.city_id } },
76
+                address: data.address,
77
+                simrs_type: data.simrs_type,
78
+                contact: data.contact,
79
+                note: data.note,
80
+                image: data.image,
81
+                progress_status: data.progress_status,
82
+            }
83
+        });
84
+    },
85
+};
86
+
87
+module.exports = HospitalRepository;

+ 50 - 0
src/repository/admin/ProvinceRepository.js

@@ -0,0 +1,50 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const ProvinceRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return prisma.province.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                name: true,
13
+                createdAt: true,
14
+                updatedAt: true,
15
+            }
16
+        });
17
+    },
18
+
19
+    countAll: async (where) => {
20
+        return prisma.province.count({ where });
21
+    },
22
+
23
+    findById: async (id) => {
24
+        return prisma.province.findFirst({
25
+            where: {
26
+                id,
27
+                deletedAt: null
28
+            },
29
+            select: {
30
+                id: true,
31
+                name: true,
32
+                createdAt: true,
33
+                updatedAt: true,
34
+            }
35
+        });
36
+    },
37
+
38
+    create: async (data) => {
39
+        return prisma.province.create({ data });
40
+    },
41
+
42
+    update: async (id, data) => {
43
+        return prisma.province.update({
44
+            where: { id },
45
+            data
46
+        });
47
+    },
48
+};
49
+
50
+module.exports = ProvinceRepository;

+ 261 - 0
src/repository/admin/SalesRepository.js

@@ -0,0 +1,261 @@
1
+const axios = require('axios');
2
+const qs = require('qs');
3
+const {
4
+    KEYCLOAK_REALM,
5
+    KEYCLOAK_ADMIN_URL,
6
+    CLIENT_ID,
7
+    CLIENT_SECRET
8
+} = require('../../../config/keycloak.js');
9
+const HttpException = require('../../utils/HttpException.js');
10
+const bcrypt = require('bcrypt');
11
+const prisma = require('../../prisma/PrismaClient.js');
12
+
13
+const getAdminToken = async () => {
14
+    const tokenParams = qs.stringify({
15
+        grant_type: 'client_credentials',
16
+        client_id: CLIENT_ID,
17
+        client_secret: CLIENT_SECRET
18
+    });
19
+
20
+    const { data } = await axios.post(
21
+        `${KEYCLOAK_ADMIN_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`,
22
+        tokenParams,
23
+        {
24
+            headers: {
25
+                'Content-Type': 'application/x-www-form-urlencoded'
26
+            }
27
+        }
28
+    );
29
+
30
+    return data.access_token;
31
+};
32
+
33
+const KeycloakRepository = {
34
+    createUser: async (userData) => {
35
+        const token = await getAdminToken();
36
+
37
+        // Cek apakah username sudah terpakai
38
+        const { data: usersByUsername } = await axios.get(
39
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users?username=${encodeURIComponent(userData.username)}`,
40
+            { headers: { Authorization: `Bearer ${token}` } }
41
+        );
42
+
43
+        if (usersByUsername.length > 0) {
44
+            throw new HttpException('Username already exists in Keycloak', 409);
45
+        }
46
+
47
+        // Cek apakah email sudah terpakai
48
+        const { data: usersByEmail } = await axios.get(
49
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users?email=${encodeURIComponent(userData.email)}`,
50
+            { headers: { Authorization: `Bearer ${token}` } }
51
+        );
52
+
53
+        if (usersByEmail.length > 0) {
54
+            throw new HttpException('Email already exists in Keycloak', 409);
55
+        }
56
+
57
+        const response = await axios.post(
58
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users`,
59
+            {
60
+                username: userData.username,
61
+                email: userData.email,
62
+                firstName: userData.firstname,
63
+                lastName: userData.lastname,
64
+                enabled: true,
65
+                credentials: [{
66
+                    type: "password",
67
+                    value: userData.password,
68
+                    temporary: false
69
+                }]
70
+            },
71
+            { headers: { Authorization: `Bearer ${token}` } }
72
+        );
73
+
74
+        return response.headers.location.split('/').pop();
75
+    },
76
+
77
+    assignSalesRole: async (userId) => {
78
+        const token = await getAdminToken();
79
+
80
+        // Get role sales
81
+        const { data: roles } = await axios.get(
82
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/roles`,
83
+            { headers: { Authorization: `Bearer ${token}` } }
84
+        );
85
+
86
+        const salesRole = roles.find(role => role.name === 'sales');
87
+        if (!salesRole) throw new HttpException('Sales role not found in Keycloak', 500);
88
+
89
+        // Assign role to user
90
+        await axios.post(
91
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users/${userId}/role-mappings/realm`,
92
+            [salesRole],
93
+            { headers: { Authorization: `Bearer ${token}` } }
94
+        );
95
+    },
96
+
97
+    updateUser: async (userId, updatedData) => {
98
+        const token = await getAdminToken();
99
+
100
+        await axios.put(
101
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users/${userId}`,
102
+            {
103
+                firstName: updatedData.firstname,
104
+                lastName: updatedData.lastname,
105
+                email: updatedData.email,
106
+                credentials: updatedData.password ? [{
107
+                    type: 'password',
108
+                    value: updatedData.password,
109
+                    temporary: false
110
+                }] : undefined
111
+            },
112
+            { headers: { Authorization: `Bearer ${token}` } }
113
+        );
114
+    },
115
+
116
+    deleteUser: async (userId) => {
117
+        const token = await getAdminToken();
118
+
119
+        await axios.delete(
120
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users/${userId}`,
121
+            { headers: { Authorization: `Bearer ${token}` } }
122
+        );
123
+    }
124
+};
125
+
126
+const UserRepository = {
127
+    createUser: async (userId, userData) => {
128
+        const hashedPassword = await bcrypt.hash(userData.password, 10);
129
+
130
+        return prisma.user.create({
131
+            data: {
132
+                id: userId,
133
+                username: userData.username,
134
+                email: userData.email,
135
+                firstname: userData.firstname,
136
+                lastname: userData.lastname,
137
+                password: hashedPassword,
138
+                role: 'sales'
139
+            }
140
+        });
141
+    },
142
+
143
+    findAll: async ({ skip, take, where, orderBy }) => {
144
+        return prisma.user.findMany({
145
+            where: {
146
+                ...where,
147
+                role: 'sales'
148
+            },
149
+            skip,
150
+            take,
151
+            orderBy,
152
+            select: {
153
+                id: true,
154
+                username: true,
155
+                email: true,
156
+                firstname: true,
157
+                lastname: true,
158
+                role: true,
159
+                user_areas: {
160
+                    select: {
161
+                        id: true,
162
+                        province: {
163
+                            select: {
164
+                                id: true,
165
+                                name: true
166
+                            }
167
+                        }
168
+                    }
169
+                },
170
+                createdAt: true,
171
+                updatedAt: true,
172
+            },
173
+        });
174
+    },
175
+
176
+    countAll: async (where) => {
177
+        return prisma.user.count({
178
+            where: {
179
+                ...where,
180
+                role: 'admin'
181
+            },
182
+        });
183
+    },
184
+
185
+    findById: async (id) => {
186
+        return prisma.user.findFirst({
187
+            where: {
188
+                id,
189
+                deletedAt: null
190
+            },
191
+            select: {
192
+                id: true,
193
+                username: true,
194
+                email: true,
195
+                firstname: true,
196
+                lastname: true,
197
+                user_areas: {
198
+                    select: {
199
+                        id: true,
200
+                        province: {
201
+                            select: {
202
+                                id: true,
203
+                                name: true
204
+                            }
205
+                        }
206
+                    }
207
+                },
208
+                createdAt: true,
209
+                updatedAt: true,
210
+            }
211
+        });
212
+    },
213
+
214
+    updateUser: async (id, updatedData) => {
215
+        const updatePayload = {
216
+            email: updatedData.email,
217
+            firstname: updatedData.firstname,
218
+            lastname: updatedData.lastname
219
+        };
220
+
221
+        if (updatedData.password) {
222
+            updatePayload.password = await bcrypt.hash(updatedData.password, 10);
223
+        }
224
+
225
+        return prisma.user.update({
226
+            where: { id },
227
+            data: updatePayload
228
+        });
229
+    },
230
+
231
+    deleteUser: async (id) => {
232
+        return prisma.user.update({
233
+            where: { id },
234
+            data: {
235
+                deletedAt: new Date()
236
+            }
237
+        });
238
+    }
239
+};
240
+
241
+const UserAreaRepository = {
242
+    createMany: async (userId, provinceIds) => {
243
+        const data = provinceIds.map(provinceId => ({
244
+            user_id: userId,
245
+            province_id: provinceId
246
+        }));
247
+
248
+        return prisma.userArea.createMany({
249
+            data,
250
+            skipDuplicates: true
251
+        });
252
+    },
253
+
254
+    deleteByUserId: async (userId) => {
255
+        return prisma.userArea.deleteMany({
256
+            where: { user_id: userId }
257
+        });
258
+    }
259
+};
260
+
261
+module.exports = { KeycloakRepository, UserRepository, UserAreaRepository };

+ 87 - 0
src/repository/sales/HospitalRepository.js

@@ -0,0 +1,87 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const HospitalRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return await prisma.hospital.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                name: true,
13
+                hospital_code: true,
14
+                type: true,
15
+                ownership: true,
16
+                province: { select: { id: true, name: true } },
17
+                city: { select: { id: true, name: true } },
18
+                address: true,
19
+                simrs_type: true,
20
+                contact: true,
21
+                image: true,
22
+                progress_status: true,
23
+                note: true,
24
+                user: { select: { id: true, username: true } },
25
+                createdAt: true,
26
+                updatedAt: true,
27
+            }
28
+        });
29
+    },
30
+
31
+    countAll: async (where) => {
32
+        return prisma.hospital.count({ where });
33
+    },
34
+
35
+    create: async (data) => {
36
+        return prisma.hospital.create({ data });
37
+    },
38
+
39
+    findById: async (id) => {
40
+        return prisma.hospital.findFirst({
41
+            where: {
42
+                id,
43
+                deletedAt: null
44
+            },
45
+            select: {
46
+                id: true,
47
+                name: true,
48
+                hospital_code: true,
49
+                type: true,
50
+                ownership: true,
51
+                province: { select: { id: true, name: true } },
52
+                city: { select: { id: true, name: true } },
53
+                address: true,
54
+                simrs_type: true,
55
+                contact: true,
56
+                image: true,
57
+                progress_status: true,
58
+                note: true,
59
+                user: { select: { id: true, username: true } },
60
+                createdAt: true,
61
+                updatedAt: true,
62
+            }
63
+        });
64
+    },
65
+
66
+    update: async (id, data) => {
67
+        return prisma.hospital.update({
68
+            where: { id },
69
+            data: {
70
+                name: data.name,
71
+                hospital_code: data.hospital_code,
72
+                type: data.type,
73
+                ownership: data.ownership,
74
+                province: { connect: { id: data.province_id } },
75
+                city: { connect: { id: data.city_id } },
76
+                address: data.address,
77
+                simrs_type: data.simrs_type,
78
+                contact: data.contact,
79
+                note: data.note,
80
+                image: data.image,
81
+                progress_status: data.progress_status,
82
+            }
83
+        });
84
+    },
85
+};
86
+
87
+module.exports = HospitalRepository;

+ 219 - 0
src/repository/superadmin/AdminRepository.js

@@ -0,0 +1,219 @@
1
+const axios = require('axios');
2
+const qs = require('qs');
3
+const {
4
+    KEYCLOAK_REALM,
5
+    KEYCLOAK_ADMIN_URL,
6
+    CLIENT_ID,
7
+    CLIENT_SECRET
8
+} = require('../../../config/keycloak.js');
9
+const HttpException = require('../../utils/HttpException.js');
10
+const bcrypt = require('bcrypt');
11
+const prisma = require('../../prisma/PrismaClient.js');
12
+
13
+const getAdminToken = async () => {
14
+    const tokenParams = qs.stringify({
15
+        grant_type: 'client_credentials',
16
+        client_id: CLIENT_ID,
17
+        client_secret: CLIENT_SECRET
18
+    });
19
+
20
+    const { data } = await axios.post(
21
+        `${KEYCLOAK_ADMIN_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`,
22
+        tokenParams,
23
+        {
24
+            headers: {
25
+                'Content-Type': 'application/x-www-form-urlencoded'
26
+            }
27
+        }
28
+    );
29
+
30
+    return data.access_token;
31
+};
32
+
33
+const KeycloakRepository = {
34
+    createUser: async (userData) => {
35
+        const token = await getAdminToken();
36
+
37
+        // Cek apakah username sudah terpakai
38
+        const { data: usersByUsername } = await axios.get(
39
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users?username=${encodeURIComponent(userData.username)}`,
40
+            { headers: { Authorization: `Bearer ${token}` } }
41
+        );
42
+
43
+        if (usersByUsername.length > 0) {
44
+            throw new HttpException('Username already exists in Keycloak', 409);
45
+        }
46
+
47
+        // Cek apakah email sudah terpakai
48
+        const { data: usersByEmail } = await axios.get(
49
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users?email=${encodeURIComponent(userData.email)}`,
50
+            { headers: { Authorization: `Bearer ${token}` } }
51
+        );
52
+
53
+        if (usersByEmail.length > 0) {
54
+            throw new HttpException('Email already exists in Keycloak', 409);
55
+        }
56
+
57
+        const response = await axios.post(
58
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users`,
59
+            {
60
+                username: userData.username,
61
+                email: userData.email,
62
+                firstName: userData.firstname,
63
+                lastName: userData.lastname,
64
+                enabled: true,
65
+                credentials: [{
66
+                    type: "password",
67
+                    value: userData.password,
68
+                    temporary: false
69
+                }]
70
+            },
71
+            { headers: { Authorization: `Bearer ${token}` } }
72
+        );
73
+
74
+        return response.headers.location.split('/').pop();
75
+    },
76
+
77
+    assignAdminRole: async (userId) => {
78
+        const token = await getAdminToken();
79
+
80
+        // Get role sales
81
+        const { data: roles } = await axios.get(
82
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/roles`,
83
+            { headers: { Authorization: `Bearer ${token}` } }
84
+        );
85
+
86
+        const adminRole = roles.find(role => role.name === 'admin');
87
+        if (!adminRole) throw new HttpException('Admin role not found in Keycloak', 500);
88
+
89
+        // Assign role to user
90
+        await axios.post(
91
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users/${userId}/role-mappings/realm`,
92
+            [adminRole],
93
+            { headers: { Authorization: `Bearer ${token}` } }
94
+        );
95
+    },
96
+
97
+    updateUser: async (userId, updatedData) => {
98
+        const token = await getAdminToken();
99
+
100
+        await axios.put(
101
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users/${userId}`,
102
+            {
103
+                firstName: updatedData.firstname,
104
+                lastName: updatedData.lastname,
105
+                email: updatedData.email,
106
+                credentials: updatedData.password ? [{
107
+                    type: 'password',
108
+                    value: updatedData.password,
109
+                    temporary: false
110
+                }] : undefined
111
+            },
112
+            { headers: { Authorization: `Bearer ${token}` } }
113
+        );
114
+    },
115
+
116
+    deleteUser: async (userId) => {
117
+        const token = await getAdminToken();
118
+
119
+        await axios.delete(
120
+            `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users/${userId}`,
121
+            { headers: { Authorization: `Bearer ${token}` } }
122
+        );
123
+    }
124
+};
125
+
126
+const AdminRepository = {
127
+    createUser: async (userId, userData) => {
128
+        const hashedPassword = await bcrypt.hash(userData.password, 10);
129
+
130
+        return prisma.user.create({
131
+            data: {
132
+                id: userId,
133
+                username: userData.username,
134
+                email: userData.email,
135
+                firstname: userData.firstname,
136
+                lastname: userData.lastname,
137
+                password: hashedPassword,
138
+                role: 'admin'
139
+            }
140
+        });
141
+    },
142
+
143
+    findAll: async ({ skip, take, where, orderBy }) => {
144
+        return prisma.user.findMany({
145
+            where: {
146
+                ...where,
147
+                role: 'admin'
148
+            },
149
+            skip,
150
+            take,
151
+            orderBy,
152
+            select: {
153
+                id: true,
154
+                username: true,
155
+                email: true,
156
+                firstname: true,
157
+                lastname: true,
158
+                role: true,
159
+                createdAt: true,
160
+                updatedAt: true,
161
+            },
162
+        });
163
+    },
164
+
165
+    countAll: async (where) => {
166
+        return prisma.user.count({
167
+            where: {
168
+                ...where,
169
+                role: 'admin'
170
+            }
171
+        });
172
+    },
173
+
174
+    findById: async (id) => {
175
+        return prisma.user.findFirst({
176
+            where: {
177
+                id,
178
+                deletedAt: null
179
+            },
180
+            select: {
181
+                id: true,
182
+                username: true,
183
+                email: true,
184
+                firstname: true,
185
+                lastname: true,
186
+                createdAt: true,
187
+                updatedAt: true,
188
+            }
189
+        });
190
+    },
191
+
192
+    updateUser: async (id, updatedData) => {
193
+        const updatePayload = {
194
+            email: updatedData.email,
195
+            firstname: updatedData.firstname,
196
+            lastname: updatedData.lastname
197
+        };
198
+
199
+        if (updatedData.password) {
200
+            updatePayload.password = await bcrypt.hash(updatedData.password, 10);
201
+        }
202
+
203
+        return prisma.user.update({
204
+            where: { id },
205
+            data: updatePayload
206
+        });
207
+    },
208
+
209
+    deleteUser: async (id) => {
210
+        return prisma.user.update({
211
+            where: { id },
212
+            data: {
213
+                deletedAt: new Date()
214
+            }
215
+        });
216
+    }
217
+};
218
+
219
+module.exports = { KeycloakRepository, AdminRepository };

+ 13 - 0
src/routes/admin/CityRoute.js

@@ -0,0 +1,13 @@
1
+const express = require('express')
2
+const router = express.Router()
3
+const cityController = require('../../controllers/admin/CityController.js')
4
+const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+const checkRole = require('../../middleware/CheckRole.js');
6
+
7
+router.get('/', verifyJWT, checkRole(['admin']), cityController.getAllCity);
8
+router.post('/', verifyJWT, checkRole(['admin']), cityController.storeCity);
9
+router.get('/:id', verifyJWT, checkRole(['admin']), cityController.showCity);
10
+router.patch('/:id', verifyJWT, checkRole(['admin']), cityController.updateCity);
11
+router.delete('/:id', verifyJWT, checkRole(['admin']), cityController.deleteCity);
12
+
13
+module.exports = router;

+ 14 - 0
src/routes/admin/HospitalRoute.js

@@ -0,0 +1,14 @@
1
+const express = require('express')
2
+const router = express.Router()
3
+const hospitalController = require('../../controllers/admin/HospitalController.js')
4
+const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+const checkRole = require('../../middleware/CheckRole.js');
6
+const upload = require('../../middleware/UploadImage.js');
7
+
8
+router.get('/', verifyJWT, checkRole(['admin']), hospitalController.getAllHospital);
9
+router.post('/', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.storeHospital);
10
+router.get('/:id', verifyJWT, checkRole(['admin']), hospitalController.showHospital);
11
+router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['admin']), hospitalController.updateHospital);
12
+router.delete('/:id', verifyJWT, checkRole(['admin']), hospitalController.deleteHospital);
13
+
14
+module.exports = router;

+ 19 - 0
src/routes/admin/ProvinceRoute.js

@@ -0,0 +1,19 @@
1
+const express = require('express')
2
+const router = express.Router()
3
+const provinceController = require('../../controllers/admin/ProvinceController.js')
4
+const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+const checkRole = require('../../middleware/CheckRole.js');
6
+
7
+// router.get('/', verifyJWT, checkRole(['admin', 'sales']), provinceController.getAllProvince);
8
+// router.post('/', verifyJWT, checkRole(['admin']), provinceController.storeProvince);
9
+// router.get('/:id', verifyJWT, checkRole(['admin']), provinceController.showProvince);
10
+// router.patch('/:id', verifyJWT, checkRole(['admin']), provinceController.updateProvince);
11
+// router.delete('/:id', verifyJWT, checkRole(['admin']), provinceController.deleteProvince);
12
+
13
+router.get('/', verifyJWT, checkRole(['admin']), provinceController.getAllProvince);
14
+router.post('/', verifyJWT, checkRole(['admin']), provinceController.storeProvince);
15
+router.get('/:id', verifyJWT, checkRole(['admin']), provinceController.showProvince);
16
+router.patch('/:id', verifyJWT, checkRole(['admin']), provinceController.updateProvince);
17
+router.delete('/:id', verifyJWT, checkRole(['admin']), provinceController.deleteProvince);
18
+
19
+module.exports = router;

+ 14 - 0
src/routes/admin/SalesRoute.js

@@ -0,0 +1,14 @@
1
+const express = require('express');
2
+const salesControllers = require('../../controllers/admin/SalesController.js');
3
+const verifyJWT = require('../../middleware/VerifyJWT.js');
4
+const checkRole = require('../../middleware/CheckRole.js');
5
+
6
+const router = express.Router();
7
+
8
+router.post('/', verifyJWT, checkRole(['admin']), salesControllers.storeSales);
9
+router.get('/', verifyJWT, checkRole(['admin']), salesControllers.getAllSales);
10
+router.get('/:id', verifyJWT, checkRole(['admin']), salesControllers.showSales);
11
+router.patch('/:id', verifyJWT, checkRole(['admin']), salesControllers.updateSales);
12
+router.delete('/:id', verifyJWT, checkRole(['admin']), salesControllers.deleteSales);
13
+
14
+module.exports = router;

+ 11 - 0
src/routes/auth/AuthRoute.js

@@ -0,0 +1,11 @@
1
+const express = require('express');
2
+const authControllers = require('../../controllers/auth/AuthControllers.js');
3
+const verifyJWT = require('../../middleware/VerifyJWT.js');
4
+
5
+const router = express.Router();
6
+
7
+router.post('/login', authControllers.login);
8
+router.get('/me', verifyJWT, authControllers.getUser);
9
+router.post('/logout', verifyJWT, authControllers.logout);
10
+
11
+module.exports = router;

+ 13 - 0
src/routes/sales/HospitalRoute.js

@@ -0,0 +1,13 @@
1
+const express = require('express')
2
+const router = express.Router()
3
+const hospitalController = require('../../controllers/sales/HospitalController.js')
4
+const verifyJWT = require('../../middleware/VerifyJWT.js');
5
+const checkRole = require('../../middleware/CheckRole.js');
6
+const upload = require('../../middleware/UploadImage.js');
7
+
8
+router.get('/', verifyJWT, checkRole(['sales']), hospitalController.getAllHospitalByArea);
9
+router.post('/', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.storeHospital);
10
+router.patch('/:id', verifyJWT, upload.single('image'), checkRole(['sales']), hospitalController.updateHospital);
11
+router.get('/:id', verifyJWT, checkRole(['sales']), hospitalController.showHospital);
12
+
13
+module.exports = router;

+ 14 - 0
src/routes/superadmin/AdminRoute.js

@@ -0,0 +1,14 @@
1
+const express = require('express');
2
+const adminControllers = require('../../controllers/superadmin/AdminController.js');
3
+const verifyJWT = require('../../middleware/VerifyJWT.js');
4
+const checkRole = require('../../middleware/CheckRole.js');
5
+
6
+const router = express.Router();
7
+
8
+router.post('/', verifyJWT, checkRole(['superadmin']), adminControllers.storeAdmin);
9
+router.get('/', verifyJWT, checkRole(['superadmin']), adminControllers.getAllAdmin);
10
+router.get('/:id', verifyJWT, checkRole(['superadmin']), adminControllers.showAdmin);
11
+router.patch('/:id', verifyJWT, checkRole(['superadmin']), adminControllers.updateAdmin);
12
+router.delete('/:id', verifyJWT, checkRole(['superadmin']), adminControllers.deleteAdmin);
13
+
14
+module.exports = router;

+ 70 - 0
src/services/admin/CityService.js

@@ -0,0 +1,70 @@
1
+const CityRepository = require('../../repository/admin/CityRepository.js');
2
+const HttpException = require('../../utils/HttpException.js');
3
+const prisma = require('../../prisma/PrismaClient.js');
4
+const { SearchFilter } = require('../../utils/SearchFilter.js');
5
+const timeLocal = require('../../utils/TimeLocal.js');
6
+const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
7
+const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js');
8
+
9
+exports.getAllCityService = async ({ page, limit, search, sortBy, orderBy }) => {
10
+    const skip = (page - 1) * limit;
11
+
12
+    const where = {
13
+        ...SearchFilter(search, ['name']),
14
+        deletedAt: null
15
+    };
16
+
17
+    const [cities, total] = await Promise.all([
18
+        CityRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
19
+        CityRepository.countAll(where)
20
+    ]);
21
+
22
+    return { cities, total };
23
+};
24
+
25
+exports.showCityService = async (id) => {
26
+    const city = await CityRepository.findById(id);
27
+    if (!city) {
28
+        throw new HttpException("Data city not found", 404);
29
+    }
30
+    return city;
31
+};
32
+
33
+exports.storeCityService = async (validateData, req) => {
34
+    const province = await ProvinceRepository.findById(validateData.province_id);
35
+    if (!province) {
36
+        throw new HttpException('Province not found', 404);
37
+    }
38
+
39
+    const data = await CityRepository.create(validateData);
40
+    await createLog(req, data);
41
+};
42
+
43
+exports.updateCityService = async (validateData, id, req) => {
44
+    const city = await CityRepository.findById(id);
45
+    if (!city) {
46
+        throw new HttpException("Data city not found", 404);
47
+    }
48
+
49
+    const province = await ProvinceRepository.findById(validateData.province_id);
50
+    if (!province) {
51
+        throw new HttpException('Province not found', 404);
52
+    }
53
+
54
+    const data = await CityRepository.update(id, validateData);
55
+    await updateLog(req, data);
56
+};
57
+
58
+exports.deleteCityService = async (id, req) => {
59
+    const city = await CityRepository.findById(id);
60
+
61
+    if (!city) {
62
+        throw new HttpException('City not found', 404);
63
+    }
64
+
65
+    const data = await CityRepository.update(id, {
66
+        deletedAt: timeLocal.now().toDate()
67
+    });
68
+
69
+    await deleteLog(req, data);
70
+};

+ 148 - 0
src/services/admin/HospitalService.js

@@ -0,0 +1,148 @@
1
+const HospitalRepository = require('../../repository/admin/HospitalRepository.js');
2
+const HttpException = require('../../utils/HttpException.js');
3
+const { SearchFilter } = require('../../utils/SearchFilter.js');
4
+const timeLocal = require('../../utils/TimeLocal.js');
5
+const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
6
+const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js');
7
+const CityRepository = require('../../repository/admin/CityRepository.js');
8
+const { BASE_URL } = require('../../../config/config.js');
9
+
10
+exports.getAllHospitalService = async ({ page, limit, search, sortBy, orderBy }) => {
11
+    const skip = (page - 1) * limit;
12
+
13
+    const where = {
14
+        ...SearchFilter(search, ['name',]),
15
+        deletedAt: null
16
+    };
17
+
18
+    const [hospitals, total] = await Promise.all([
19
+        HospitalRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
20
+        HospitalRepository.countAll(where)
21
+    ]);
22
+
23
+    return { hospitals, total };
24
+};
25
+
26
+exports.showHospitalService = async (id) => {
27
+    const hospital = await HospitalRepository.findById(id);
28
+    if (!hospital) {
29
+        throw new HttpException("Data hospital not found", 404);
30
+    }
31
+    return hospital;
32
+};
33
+
34
+exports.storeHospitalService = async (validateData, req) => {
35
+    const creatorId = req.user.id;
36
+    const province = await ProvinceRepository.findById(validateData.province_id);
37
+    if (!province) {
38
+        throw new HttpException('Province not found', 404);
39
+    }
40
+
41
+    const city = await CityRepository.findById(validateData.city_id);
42
+    if (!city) {
43
+        throw new HttpException('City not found', 404);
44
+    }
45
+
46
+    if (!req.file) {
47
+        throw new HttpException({ image: ['image file is required'] }, 422);
48
+    }
49
+
50
+    const existingHospital = await prisma.hospital.findFirst({
51
+        where: {
52
+            name: validateData.name,
53
+            city_id: validateData.city_id,
54
+            deletedAt: null
55
+        }
56
+    });
57
+
58
+    if (existingHospital) {
59
+        throw new HttpException('Hospital with same name in this city already exists', 400);
60
+    }
61
+
62
+    const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
63
+
64
+    const payload = {
65
+        ...validateData,
66
+        image: imagePath,
67
+        progress_status: "cari_data",
68
+        created_by: creatorId
69
+    };
70
+
71
+    const data = await HospitalRepository.create(payload);
72
+    await createLog(req, data);
73
+};
74
+
75
+const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
76
+
77
+exports.updateHospitalService = async (validateData, id, req) => {
78
+    // const hospital = await HospitalRepository.findById(id);
79
+    // if (!hospital) {
80
+    //     throw new HttpException("Data hospital not found", 404);
81
+    // }
82
+
83
+    // const data = await HospitalRepository.update(id, validateData);
84
+    // await updateLog(req, data);
85
+
86
+    const hospital = await HospitalRepository.findById(id);
87
+    if (!hospital) {
88
+        throw new HttpException("Hospital data not found", 404);
89
+    }
90
+
91
+    const province = await ProvinceRepository.findById(validateData.province_id);
92
+    if (!province) {
93
+        throw new HttpException('Province not found', 404);
94
+    }
95
+
96
+    const city = await CityRepository.findById(validateData.city_id);
97
+    if (!city) {
98
+        throw new HttpException('City not found', 404);
99
+    }
100
+
101
+    if (validateData.progress_status && !validProgressStatuses.includes(validateData.progress_status)) {
102
+        throw new HttpException(
103
+            `Invalid progress_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
104
+            422
105
+        );
106
+    }
107
+
108
+    const existingHospital = await prisma.hospital.findFirst({
109
+        where: {
110
+            name: validateData.name,
111
+            city_id: validateData.city_id,
112
+            deletedAt: null
113
+        }
114
+    });
115
+
116
+    if (existingHospital) {
117
+        throw new HttpException('Hospital with same name in this city already exists', 400);
118
+    }
119
+
120
+    // Jika ada file baru, replace image
121
+    let imagePath = hospital.image; // pakai yang lama
122
+    if (req.file) {
123
+        imagePath = `/storage/img/${req.file.filename}`; // path relatif
124
+    }
125
+
126
+    const payload = {
127
+        ...validateData,
128
+        image: imagePath,
129
+        updated_by: req.user.id,
130
+    };
131
+
132
+    const data = await HospitalRepository.update(id, payload);
133
+    await updateLog(req, data);
134
+};
135
+
136
+exports.deleteHospitalService = async (id, req) => {
137
+    const hospital = await HospitalRepository.findById(id);
138
+
139
+    if (!hospital) {
140
+        throw new HttpException('Hospital not found', 404);
141
+    }
142
+
143
+    const data = await HospitalRepository.update(id, {
144
+        deletedAt: timeLocal.now().toDate()
145
+    });
146
+
147
+    await deleteLog(req, data);
148
+};

+ 56 - 0
src/services/admin/ProvinceService.js

@@ -0,0 +1,56 @@
1
+const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js');
2
+const HttpException = require('../../utils/HttpException.js');
3
+const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
4
+const { SearchFilter } = require('../../utils/SearchFilter.js');
5
+const timeLocal = require('../../utils/TimeLocal.js')
6
+
7
+exports.getAllProvinceService = async ({ page, limit, search, sortBy, orderBy }) => {
8
+    const skip = (page - 1) * limit;
9
+
10
+    const where = {
11
+        ...SearchFilter(search, ['name']),
12
+        deletedAt: null
13
+    };
14
+
15
+    const [provinces, total] = await Promise.all([
16
+        ProvinceRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
17
+        ProvinceRepository.countAll(where)
18
+    ]);
19
+
20
+    return { provinces, total };
21
+};
22
+
23
+exports.showProvinceService = async (id) => {
24
+    const province = await ProvinceRepository.findById(id);
25
+    if (!province) {
26
+        throw new HttpException("Data province not found", 404);
27
+    }
28
+    return province;
29
+};
30
+
31
+exports.storeProvinceService = async (validateData, req) => {
32
+    const province = await ProvinceRepository.create(validateData);
33
+    await createLog(req, province);
34
+};
35
+
36
+exports.updateProvinceService = async (validateData, id, req) => {
37
+    const province = await ProvinceRepository.findById(id);
38
+    if (!province) {
39
+        throw new HttpException("Data province not found", 404);
40
+    }
41
+
42
+    const data = await ProvinceRepository.update(id, validateData);
43
+    await updateLog(req, data);
44
+};
45
+
46
+exports.deleteProvinceService = async (id, req) => {
47
+    const province = await ProvinceRepository.findById(id);
48
+    if (!province) {
49
+        throw new HttpException("Data province not found", 404);
50
+    }
51
+
52
+    const data = await ProvinceRepository.update(id, {
53
+        deletedAt: timeLocal.now().toDate()
54
+    });
55
+    await deleteLog(req, data);
56
+};

+ 68 - 0
src/services/admin/SalesService.js

@@ -0,0 +1,68 @@
1
+const SalesRepository = require('../../repository/admin/SalesRepository.js');
2
+const HttpException = require('../../utils/HttpException.js');
3
+const { SearchFilter } = require('../../utils/SearchFilter.js');
4
+const timeLocal = require('../../utils/TimeLocal.js');
5
+const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
6
+
7
+exports.getAllSalesService = async ({ page, limit, search, sortBy, orderBy }) => {
8
+    const skip = (page - 1) * limit;
9
+
10
+    const where = {
11
+        ...SearchFilter(search, ['username', 'email', 'firstname', 'lastname']),
12
+        deletedAt: null
13
+    };
14
+
15
+    const [sales, total] = await Promise.all([
16
+        SalesRepository.UserRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
17
+        SalesRepository.UserRepository.countAll(where)
18
+    ]);
19
+
20
+    return { sales, total };
21
+};
22
+
23
+exports.showSalesService = async (id) => {
24
+    const sales = await SalesRepository.UserRepository.findById(id);
25
+    if (!sales) {
26
+        throw new HttpException("Data sales not found", 404);
27
+    }
28
+    return sales;
29
+};
30
+
31
+exports.storeSalesService = async (userData, req) => {
32
+    const userId = await SalesRepository.KeycloakRepository.createUser(userData);
33
+    await SalesRepository.KeycloakRepository.assignSalesRole(userId);
34
+    const data = await SalesRepository.UserRepository.createUser(userId, userData);
35
+
36
+    if (userData.province_ids && userData.province_ids.length > 0) {
37
+        await SalesRepository.UserAreaRepository.createMany(userId, userData.province_ids);
38
+    }
39
+
40
+    await createLog(req, data);
41
+};
42
+
43
+exports.updateSalesService = async (userData, id, req) => {
44
+    const sales = await SalesRepository.UserRepository.findById(id);
45
+    if (!sales) throw new HttpException('Sales not found', 404);
46
+
47
+    await SalesRepository.KeycloakRepository.updateUser(id, userData);
48
+    const data = await SalesRepository.UserRepository.updateUser(id, userData);
49
+
50
+    if (userData.province_ids && userData.province_ids.length > 0) {
51
+        await SalesRepository.UserAreaRepository.deleteByUserId(id);
52
+        await SalesRepository.UserAreaRepository.createMany(id, userData.province_ids);
53
+    }
54
+
55
+    await updateLog(req, data);
56
+};
57
+
58
+exports.deleteSalesService = async (id, req) => {
59
+    const sales = await SalesRepository.UserRepository.findById(id);
60
+    if (!sales) {
61
+        throw new HttpException('Sales not found', 404);
62
+    }
63
+
64
+    await SalesRepository.KeycloakRepository.deleteUser(id);
65
+    const data = await SalesRepository.UserRepository.deleteUser(id, { deletedAt: timeLocal.now().toDate() });
66
+
67
+    await deleteLog(req, data);
68
+};

+ 152 - 0
src/services/auth/AuthService.js

@@ -0,0 +1,152 @@
1
+const axios = require('axios');
2
+const jwt = require('jsonwebtoken');
3
+const {
4
+    KEYCLOAK_TOKEN_URL,
5
+    CLIENT_ID,
6
+    CLIENT_SECRET,
7
+    JWT_SECRET,
8
+} = require('../../../config/keycloak.js');
9
+const HttpException = require('../../utils/HttpException.js');
10
+const prisma = require('../../prisma/PrismaClient.js');
11
+const { loginLog, logoutLog } = require('../../utils/LogActivity.js');
12
+
13
+exports.loginService = async ({ username, password }, req) => {
14
+    const params = new URLSearchParams();
15
+    params.append('grant_type', 'password');
16
+    params.append('client_id', CLIENT_ID);
17
+    params.append('client_secret', CLIENT_SECRET);
18
+    params.append('username', username);
19
+    params.append('password', password);
20
+
21
+    try {
22
+        const { data } = await axios.post(KEYCLOAK_TOKEN_URL, params, {
23
+            headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
24
+        });
25
+
26
+        const decoded = jwt.decode(data.access_token);
27
+
28
+        const user = {
29
+            id: decoded.sub,
30
+            username: decoded.preferred_username,
31
+            email: decoded.email,
32
+            firstname: decoded.given_name,
33
+            lastname: decoded.family_name,
34
+            roles: decoded.realm_access?.roles || []
35
+        };
36
+
37
+        await loginLog(req, user);
38
+
39
+        const token = jwt.sign(user, JWT_SECRET, { expiresIn: '1d' });
40
+
41
+        return { user, token };
42
+    } catch (error) {
43
+        throw new HttpException('Invalid username or password', 401);
44
+    }
45
+};
46
+
47
+exports.getUserService = async (req) => {
48
+    const user = req.user;
49
+
50
+    if (!user) {
51
+        throw new HttpException('Invalid or missing token', 401);
52
+    }
53
+
54
+    const { id, username, email, firstname, lastname, roles } = user;
55
+
56
+    return { id, username, email, firstname, lastname, roles };
57
+};
58
+
59
+// exports.registerService = async (req) => {
60
+//     const { username, email, firstname, lastname, password, phonenumber, alamathehe } = validateRegisterRequest(req.body);
61
+
62
+//     // Ambil admin token via client_credentials
63
+//     const tokenParams = qs.stringify({
64
+//         grant_type: 'client_credentials',
65
+//         client_id: CLIENT_ID,
66
+//         client_secret: CLIENT_SECRET
67
+//     });
68
+
69
+//     let adminToken;
70
+//     try {
71
+//         const tokenResponse = await axios.post(
72
+//             KEYCLOAK_TOKEN_URL,
73
+//             tokenParams,
74
+//             { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
75
+//         );
76
+
77
+//         adminToken = tokenResponse.data.access_token;
78
+//     } catch (err) {
79
+//         console.error('Token Error:', err.response?.data || err.message);
80
+//         throw new HttpException('Failed to get admin token', 400);
81
+//     }
82
+
83
+//     // Panggil endpoint pendaftaran user
84
+//     try {
85
+//         const response = await axios.post(
86
+//             `${KEYCLOAK_ADMIN_URL}/admin/realms/${KEYCLOAK_REALM}/users`,
87
+//             {
88
+//                 username,
89
+//                 email,
90
+//                 enabled: true,
91
+//                 firstName: firstname,
92
+//                 lastName: lastname,
93
+//                 credentials: [
94
+//                     {
95
+//                         type: 'password',
96
+//                         value: password,
97
+//                         temporary: false
98
+//                     }
99
+//                 ],
100
+//                 attributes: [
101
+//                     {
102
+//                         phoneNumber: phonenumber,
103
+//                     },
104
+//                     {
105
+//                         alamatHehe: alamathehe
106
+//                     }
107
+//                 ]
108
+//             },
109
+//             {
110
+//                 headers: {
111
+//                     Authorization: `Bearer ${adminToken}`,
112
+//                     'Content-Type': 'application/json'
113
+//                 }
114
+//             }
115
+//         );
116
+
117
+//         if (response.status !== 201) {
118
+//             throw new HttpException('Failed to register user in Keycloak', 400);
119
+//         }
120
+
121
+//     } catch (err) {
122
+//         // console.error('Register Error:', err.response?.data || err.message);
123
+//         // throw new HttpException('Keycloak registration error: ' + (err.response?.data?.errorMessage || err.message), 400);
124
+//         throw new HttpException((err.response?.data?.errorMessage || err.message), 400);
125
+//     }
126
+// };
127
+
128
+exports.logoutService = async (req) => {
129
+    const authHeader = req.headers.authorization;
130
+
131
+    if (!authHeader || !authHeader.startsWith('Bearer ')) {
132
+        throw new HttpException('No JWT token provided', 401);
133
+    }
134
+
135
+    const token = authHeader.split(' ')[1];
136
+
137
+    let decoded;
138
+    try {
139
+        decoded = jwt.verify(token, JWT_SECRET);
140
+    } catch (error) {
141
+        throw new HttpException('Invalid JWT token', 400);
142
+    }
143
+
144
+    // Simpan ke tabel revoked_tokens
145
+    await prisma.revokedToken.create({
146
+        data: {
147
+            token,
148
+        },
149
+    });
150
+
151
+    await logoutLog(req, decoded)
152
+};

+ 173 - 0
src/services/sales/HospitalService.js

@@ -0,0 +1,173 @@
1
+const salesHospitalRepository = require('../../repository/sales/HospitalRepository.js');
2
+const { SearchFilter } = require('../../utils/SearchFilter.js');
3
+const prisma = require('../../prisma/PrismaClient.js');
4
+const { createLog, updateLog } = require('../../utils/LogActivity.js');
5
+const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js');
6
+const CityRepository = require('../../repository/admin/CityRepository.js');
7
+const HttpException = require('../../utils/HttpException.js');
8
+const HospitalRepository = require('../../repository/admin/HospitalRepository.js');
9
+
10
+exports.getAllHospitalByAreaService = async ({ page, limit, search, sortBy, orderBy }, req) => {
11
+    const skip = (page - 1) * limit;
12
+
13
+    const userAreas = await prisma.userArea.findMany({
14
+        where: { user_id: req.user.id },
15
+        select: { province_id: true },
16
+    });
17
+
18
+    const provinceIds = userAreas.map(ua => ua.province_id);
19
+
20
+    // const where = {
21
+    //     ...SearchFilter(search, ['username', 'email', 'firstname', 'lastname']),
22
+    //     deletedAt: null
23
+    // };
24
+
25
+    const where = {
26
+        ...SearchFilter(search, [
27
+            'name',
28
+            'hospital_code',
29
+            'type',
30
+            'ownership',
31
+            'address',
32
+            'simrs_type',
33
+            'contact',
34
+            'image',
35
+            'progress_status',
36
+            'note',
37
+            'province.name',
38
+            'city.name',
39
+            'user.username'
40
+        ]),
41
+        province_id: { in: provinceIds },
42
+        deletedAt: null
43
+    };
44
+
45
+    const [hospitals, total] = await Promise.all([
46
+        salesHospitalRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
47
+        salesHospitalRepository.countAll(where)
48
+    ]);
49
+
50
+    return { hospitals, total };
51
+};
52
+
53
+exports.storeHospitalService = async (validateData, req) => {
54
+    const creatorId = req.user.id;
55
+
56
+    const province = await ProvinceRepository.findById(validateData.province_id);
57
+    if (!province) {
58
+        throw new HttpException('Province not found', 404);
59
+    }
60
+
61
+    const userArea = await prisma.userArea.findFirst({
62
+        where: {
63
+            user_id: req.user.id,
64
+            province_id: validateData.province_id
65
+        }
66
+    });
67
+
68
+    if (!userArea) {
69
+        throw new HttpException('You are not allowed to add hospital to this province', 403);
70
+    }
71
+
72
+    const city = await CityRepository.findById(validateData.city_id);
73
+    if (!city) {
74
+        throw new HttpException('City not found', 404);
75
+    }
76
+
77
+    const existingHospital = await prisma.hospital.findFirst({
78
+        where: {
79
+            name: validateData.name,
80
+            city_id: validateData.city_id,
81
+            deletedAt: null
82
+        }
83
+    });
84
+
85
+    if (existingHospital) {
86
+        throw new HttpException('Hospital with same name in this city already exists', 400);
87
+    }
88
+
89
+    if (!req.file) {
90
+        throw new HttpException({ image: ['image file is required'] }, 422);
91
+    }
92
+
93
+    const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
94
+
95
+    const payload = {
96
+        ...validateData,
97
+        image: imagePath,
98
+        progress_status: "cari_data",
99
+        created_by: creatorId
100
+    };
101
+
102
+    const data = await salesHospitalRepository.create(payload);
103
+    await createLog(req, data);
104
+};
105
+
106
+const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
107
+
108
+exports.updateHospitalService = async (validateData, id, req) => {
109
+    // const hospital = await HospitalRepository.findById(id);
110
+    // if (!hospital) {
111
+    //     throw new HttpException("Data hospital not found", 404);
112
+    // }
113
+
114
+    // const data = await HospitalRepository.update(id, validateData);
115
+    // await updateLog(req, data);
116
+
117
+    const hospital = await HospitalRepository.findById(id);
118
+    if (!hospital) {
119
+        throw new HttpException("Hospital data not found", 404);
120
+    }
121
+
122
+    const province = await ProvinceRepository.findById(validateData.province_id);
123
+    if (!province) {
124
+        throw new HttpException('Province not found', 404);
125
+    }
126
+
127
+    const city = await CityRepository.findById(validateData.city_id);
128
+    if (!city) {
129
+        throw new HttpException('City not found', 404);
130
+    }
131
+
132
+    if (validateData.progress_status && !validProgressStatuses.includes(validateData.progress_status)) {
133
+        throw new HttpException(
134
+            `Invalid progress_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
135
+            422
136
+        );
137
+    }
138
+
139
+    const existingHospital = await prisma.hospital.findFirst({
140
+        where: {
141
+            name: validateData.name,
142
+            city_id: validateData.city_id,
143
+            deletedAt: null
144
+        }
145
+    });
146
+
147
+    if (existingHospital) {
148
+        throw new HttpException('Hospital with same name in this city already exists', 400);
149
+    }
150
+
151
+    // Jika ada file baru, replace image
152
+    let imagePath = hospital.image; // pakai yang lama
153
+    if (req.file) {
154
+        imagePath = `/storage/img/${req.file.filename}`; // path relatif
155
+    }
156
+
157
+    const payload = {
158
+        ...validateData,
159
+        image: imagePath,
160
+        updated_by: req.user.id,
161
+    };
162
+
163
+    const data = await salesHospitalRepository.update(id, payload);
164
+    await updateLog(req, data);
165
+};
166
+
167
+exports.showHospitalService = async (id) => {
168
+    const hospital = await salesHospitalRepository.findById(id);
169
+    if (!hospital) {
170
+        throw new HttpException("Data hospital not found", 404);
171
+    }
172
+    return hospital;
173
+};

+ 59 - 0
src/services/superadmin/AdminService.js

@@ -0,0 +1,59 @@
1
+const AdminRepository = require('../../repository/superadmin/AdminRepository.js');
2
+const HttpException = require('../../utils/HttpException.js');
3
+const { SearchFilter } = require('../../utils/SearchFilter.js');
4
+const timeLocal = require('../../utils/TimeLocal.js');
5
+const { createLog, updateLog, deleteLog } = require('../../utils/LogActivity.js');
6
+
7
+exports.getAllAdminService = async ({ page, limit, search, sortBy, orderBy }) => {
8
+    const skip = (page - 1) * limit;
9
+
10
+    const where = {
11
+        ...SearchFilter(search, ['username', 'email', 'firstname', 'lastname']),
12
+        deletedAt: null
13
+    };
14
+
15
+    const [admin, total] = await Promise.all([
16
+        AdminRepository.AdminRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
17
+        AdminRepository.AdminRepository.countAll(where)
18
+    ]);
19
+
20
+    return { admin, total };
21
+};
22
+
23
+exports.showAdminService = async (id) => {
24
+    const admin = await AdminRepository.AdminRepository.findById(id);
25
+    if (!admin) {
26
+        throw new HttpException("Data Admin not found", 404);
27
+    }
28
+    return admin;
29
+};
30
+
31
+exports.storeAdminService = async (userData, req) => {
32
+    const userId = await AdminRepository.KeycloakRepository.createUser(userData);
33
+    await AdminRepository.KeycloakRepository.assignAdminRole(userId);
34
+    const data = await AdminRepository.AdminRepository.createUser(userId, userData);
35
+
36
+    await createLog(req, data);
37
+};
38
+
39
+exports.updateAdminService = async (userData, id, req) => {
40
+    const admin = await AdminRepository.AdminRepository.findById(id);
41
+    if (!admin) throw new HttpException('Admin not found', 404);
42
+
43
+    await AdminRepository.KeycloakRepository.updateUser(id, userData);
44
+    const data = await AdminRepository.AdminRepository.updateUser(id, userData);
45
+
46
+    await updateLog(req, data);
47
+};
48
+
49
+exports.deleteAdminService = async (id, req) => {
50
+    const admin = await AdminRepository.AdminRepository.findById(id);
51
+    if (!admin) {
52
+        throw new HttpException('Admin not found', 404);
53
+    }
54
+
55
+    await AdminRepository.KeycloakRepository.deleteUser(id);
56
+    const data = await AdminRepository.AdminRepository.deleteUser(id, { deletedAt: timeLocal.now().toDate() });
57
+
58
+    await deleteLog(req, data);
59
+};

BIN
src/storage/img/1750755220747-442027743.jpeg


BIN
src/storage/img/1750755241850-643108364.jpeg


BIN
src/storage/img/1750755362021-855858980.jpeg


+ 18 - 0
src/utils/HttpException.js

@@ -0,0 +1,18 @@
1
+// class HttpException extends Error {
2
+//     constructor(message, statusCode = 500) {
3
+//         super(message);
4
+//         this.statusCode = statusCode;
5
+//     }
6
+// }
7
+
8
+// module.exports = HttpException;
9
+
10
+class HttpException extends Error {
11
+    constructor(message = 'Internal Server Error', statusCode = 500) {
12
+        super(typeof message === 'string' ? message : 'Validation failed');
13
+        this.statusCode = statusCode;
14
+        this.message = message; // bisa object atau string
15
+    }
16
+}
17
+
18
+module.exports = HttpException;

+ 0 - 0
src/utils/ListResponse.js


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff