pearlgw hace 2 meses
padre
commit
06f4984043
Se han modificado 76 ficheros con 804 adiciones y 88 borrados
  1. 2 2
      entrypoint.sh
  2. 20 0
      prisma/migrations/20250703030428_add_table_status_history/migration.sql
  3. 35 0
      prisma/migrations/20250703030614_update_name_table/migration.sql
  4. 9 0
      prisma/migrations/20250703030902_update_name_field_table_status_histories/migration.sql
  5. 4 0
      prisma/migrations/20250703075659_add_field_in_table_hospital/migration.sql
  6. 34 13
      prisma/schema.prisma
  7. 29 0
      src/controllers/admin/StatusHistoryController.js
  8. 1 2
      src/controllers/admin/VendorController.js
  9. 29 0
      src/controllers/sales/StatusHistoryController.js
  10. 18 3
      src/repository/admin/HospitalRepository.js
  11. 69 0
      src/repository/admin/StatusHistoryRepository.js
  12. 18 3
      src/repository/sales/HospitalRepository.js
  13. 69 0
      src/repository/sales/StatusHistoryRepository.js
  14. 23 0
      src/resources/admin/status_history/StatusHistoryCollection.js
  15. 5 0
      src/routes/admin/HospitalRoute.js
  16. 4 0
      src/routes/sales/HospitalRoute.js
  17. 92 11
      src/services/admin/HospitalService.js
  18. 79 0
      src/services/admin/StatusHistoryService.js
  19. 94 8
      src/services/sales/HospitalService.js
  20. 99 0
      src/services/sales/StatusHistoryService.js
  21. 52 46
      src/validators/admin/hospital/HospitalValidators.js
  22. 19 0
      src/validators/admin/status_history/StatusHistoryValidators.js
  23. BIN
      storage/img/1751530124686-242372727.jpeg
  24. BIN
      storage/img/1751530180923-52842900.jpeg
  25. BIN
      storage/img/1751530194422-59175162.jpeg
  26. BIN
      storage/img/1751530268105-213241601.jpeg
  27. BIN
      storage/img/1751530283736-429201881.jpeg
  28. BIN
      storage/img/1751530339156-411930692.jpeg
  29. BIN
      storage/img/1751531740648-913448829.jpeg
  30. BIN
      storage/img/1751531755792-596895748.jpeg
  31. BIN
      storage/img/1751531931056-85404101.jpeg
  32. BIN
      storage/img/1751531939833-744370817.jpeg
  33. BIN
      storage/img/1751533085578-919962860.jpeg
  34. BIN
      storage/img/1751533138374-79830774.jpeg
  35. BIN
      storage/img/1751533144888-439729161.jpeg
  36. BIN
      storage/img/1751533229041-456120975.jpeg
  37. BIN
      storage/img/1751533232526-27606846.jpeg
  38. BIN
      storage/img/1751533366302-905436030.jpeg
  39. BIN
      storage/img/1751533371765-688758068.jpeg
  40. BIN
      storage/img/1751533414588-875811984.jpeg
  41. BIN
      storage/img/1751533483838-682770826.jpeg
  42. BIN
      storage/img/1751533489808-439186721.jpeg
  43. BIN
      storage/img/1751533625364-355301513.jpeg
  44. BIN
      storage/img/1751533629075-189527972.jpeg
  45. BIN
      storage/img/1751533645996-765118559.jpeg
  46. BIN
      storage/img/1751533661341-594783356.jpeg
  47. BIN
      storage/img/1751533682479-471230046.jpeg
  48. BIN
      storage/img/1751533687396-26757586.jpeg
  49. BIN
      storage/img/1751600673670-697106882.jpeg
  50. BIN
      storage/img/1751600707027-644225437.jpeg
  51. BIN
      storage/img/1751600737290-309748691.jpeg
  52. BIN
      storage/img/1751600784523-161212058.jpeg
  53. BIN
      storage/img/1751600916157-757176239.jpeg
  54. BIN
      storage/img/1751601000952-486008833.jpeg
  55. BIN
      storage/img/1751601036067-316245070.jpeg
  56. BIN
      storage/img/1751602417119-723126231.png
  57. BIN
      storage/img/1751602553616-291701965.png
  58. BIN
      storage/img/1751602558566-257160389.png
  59. BIN
      storage/img/1751602580094-621745417.png
  60. BIN
      storage/img/1751602584492-314175553.png
  61. BIN
      storage/img/1751603812511-236372234.png
  62. BIN
      storage/img/1751603872927-730181195.png
  63. BIN
      storage/img/1751603887789-354294165.png
  64. BIN
      storage/img/1751603901880-622251451.png
  65. BIN
      storage/img/1751603929998-540007390.png
  66. BIN
      storage/img/1751604446265-284837886.png
  67. BIN
      storage/img/1751604459294-361264422.png
  68. BIN
      storage/img/1751604487170-978421322.png
  69. BIN
      storage/img/1751604732952-131082581.png
  70. BIN
      storage/img/1751604753650-29307011.png
  71. BIN
      storage/img/1751604794637-627486397.png
  72. BIN
      storage/img/1751604912435-637315351.png
  73. BIN
      storage/img/1751605011757-849707384.png
  74. BIN
      storage/img/1751608744824-668781574.png
  75. BIN
      storage/img/1751609253933-105180482.png
  76. BIN
      storage/img/1751609259292-637789973.png

+ 2 - 2
entrypoint.sh

@@ -9,8 +9,8 @@ npx prisma migrate dev --name init
9 9
 # echo "♻️ Reset database dan menjalankan seeder..."
10 10
 # npx prisma migrate reset --force
11 11
 
12
-echo "🌱 Menjalankan seeder (aman)..."
13
-npx prisma db seed
12
+# echo "🌱 Menjalankan seeder (aman)..."
13
+# npx prisma db seed
14 14
 
15 15
 echo "🚀 Menjalankan aplikasi..."
16 16
 exec node index.js

+ 20 - 0
prisma/migrations/20250703030428_add_table_status_history/migration.sql

@@ -0,0 +1,20 @@
1
+-- CreateTable
2
+CREATE TABLE "StatusHistory" (
3
+    "id" TEXT NOT NULL,
4
+    "hospital_id" TEXT NOT NULL,
5
+    "user_id" TEXT NOT NULL,
6
+    "old_status" "ProgressStatus" NOT NULL,
7
+    "new_status" "ProgressStatus" NOT NULL,
8
+    "notes" TEXT,
9
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+    "updatedAt" TIMESTAMP(3) NOT NULL,
11
+    "deletedAt" TIMESTAMP(3),
12
+
13
+    CONSTRAINT "StatusHistory_pkey" PRIMARY KEY ("id")
14
+);
15
+
16
+-- AddForeignKey
17
+ALTER TABLE "StatusHistory" ADD CONSTRAINT "StatusHistory_hospital_id_fkey" FOREIGN KEY ("hospital_id") REFERENCES "hospitals"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
18
+
19
+-- AddForeignKey
20
+ALTER TABLE "StatusHistory" ADD CONSTRAINT "StatusHistory_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 35 - 0
prisma/migrations/20250703030614_update_name_table/migration.sql

@@ -0,0 +1,35 @@
1
+/*
2
+  Warnings:
3
+
4
+  - You are about to drop the `StatusHistory` table. If the table is not empty, all the data it contains will be lost.
5
+
6
+*/
7
+-- DropForeignKey
8
+ALTER TABLE "StatusHistory" DROP CONSTRAINT "StatusHistory_hospital_id_fkey";
9
+
10
+-- DropForeignKey
11
+ALTER TABLE "StatusHistory" DROP CONSTRAINT "StatusHistory_user_id_fkey";
12
+
13
+-- DropTable
14
+DROP TABLE "StatusHistory";
15
+
16
+-- CreateTable
17
+CREATE TABLE "status_histories" (
18
+    "id" TEXT NOT NULL,
19
+    "hospital_id" TEXT NOT NULL,
20
+    "user_id" TEXT NOT NULL,
21
+    "old_status" "ProgressStatus" NOT NULL,
22
+    "new_status" "ProgressStatus" NOT NULL,
23
+    "notes" TEXT,
24
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+    "updatedAt" TIMESTAMP(3) NOT NULL,
26
+    "deletedAt" TIMESTAMP(3),
27
+
28
+    CONSTRAINT "status_histories_pkey" PRIMARY KEY ("id")
29
+);
30
+
31
+-- AddForeignKey
32
+ALTER TABLE "status_histories" ADD CONSTRAINT "status_histories_hospital_id_fkey" FOREIGN KEY ("hospital_id") REFERENCES "hospitals"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
33
+
34
+-- AddForeignKey
35
+ALTER TABLE "status_histories" ADD CONSTRAINT "status_histories_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 9 - 0
prisma/migrations/20250703030902_update_name_field_table_status_histories/migration.sql

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

+ 4 - 0
prisma/migrations/20250703075659_add_field_in_table_hospital/migration.sql

@@ -0,0 +1,4 @@
1
+-- AlterTable
2
+ALTER TABLE "hospitals" ADD COLUMN     "gmaps_url" TEXT,
3
+ADD COLUMN     "latitude" DOUBLE PRECISION,
4
+ADD COLUMN     "longitude" DOUBLE PRECISION;

+ 34 - 13
prisma/schema.prisma

@@ -44,19 +44,20 @@ model ActivityLog {
44 44
 }
45 45
 
46 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
-  vendors    Vendor[]
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
+  vendors          Vendor[]
60
+  status_histories StatusHistory[]
60 61
 
61 62
   @@map("users")
62 63
 }
@@ -103,6 +104,9 @@ model Hospital {
103 104
   progress_status      ProgressStatus
104 105
   note                 String?
105 106
   created_by           String
107
+  latitude             Float?
108
+  longitude            Float?
109
+  gmaps_url            String?             @db.Text
106 110
   createdAt            DateTime            @default(now())
107 111
   updatedAt            DateTime            @updatedAt
108 112
   deletedAt            DateTime?
@@ -111,6 +115,7 @@ model Hospital {
111 115
   user                 User                @relation(fields: [created_by], references: [id])
112 116
   vendor_histories     VendorHistory[]
113 117
   executives_histories ExecutivesHistory[]
118
+  status_histories     StatusHistory[]
114 119
 
115 120
   @@map("hospitals")
116 121
 }
@@ -177,3 +182,19 @@ model ExecutivesHistory {
177 182
 
178 183
   @@map("executives_histories")
179 184
 }
185
+
186
+model StatusHistory {
187
+  id          String         @id @default(uuid())
188
+  hospital_id String
189
+  user_id     String
190
+  old_status  ProgressStatus
191
+  new_status  ProgressStatus
192
+  note        String?        @db.Text
193
+  hospital    Hospital       @relation(fields: [hospital_id], references: [id])
194
+  user        User           @relation(fields: [user_id], references: [id])
195
+  createdAt   DateTime       @default(now())
196
+  updatedAt   DateTime       @updatedAt
197
+  deletedAt   DateTime?
198
+
199
+  @@map("status_histories")
200
+}

+ 29 - 0
src/controllers/admin/StatusHistoryController.js

@@ -0,0 +1,29 @@
1
+const { PaginationParam } = require("../../utils/PaginationParams");
2
+const statusHistoryService = require('../../services/admin/StatusHistoryService.js');
3
+const { validateCreateStatusHisotryRequest } = require("../../validators/admin/status_history/StatusHistoryValidators.js");
4
+const { StatusHistoryCollection } = require("../../resources/admin/status_history/StatusHistoryCollection.js");
5
+const { errorResponse, messageSuccessResponse } = require("../../utils/Response.js");
6
+
7
+exports.getAllStatusHistory = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { status_histories, total } = await statusHistoryService.getAllStatusHistoryService({
12
+            page, limit, search, sortBy, orderBy
13
+        }, req);
14
+
15
+        return StatusHistoryCollection(req, res, status_histories, total, page, limit, 'Status history successfully retrieved');
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.storeStatusHistory = async (req, res) => {
22
+    try {
23
+        const validatedData = validateCreateStatusHisotryRequest(req.body);
24
+        await statusHistoryService.storeStatusHistoryService(validatedData, req);
25
+        return messageSuccessResponse(res, 'Success added status history', 201);
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+}

+ 1 - 2
src/controllers/admin/VendorController.js

@@ -1,9 +1,8 @@
1 1
 const { VendorCollection } = require('../../resources/admin/vendor/VendorCollection.js');
2 2
 const { VendorResource } = require('../../resources/admin/vendor/VendorResource.js');
3 3
 const vendorService = require('../../services/admin/VendorService.js');
4
-const { ListResponse } = require('../../utils/ListResponse.js');
5 4
 const { PaginationParam } = require('../../utils/PaginationParams.js');
6
-const { errorResponse, successResponse, messageSuccessResponse } = require('../../utils/Response.js');
5
+const { errorResponse, messageSuccessResponse } = require('../../utils/Response.js');
7 6
 const { validateStoreVendorRequest } = require('../../validators/sales/vendor/VendorValidators.js');
8 7
 
9 8
 exports.getAllVendor = async (req, res) => {

+ 29 - 0
src/controllers/sales/StatusHistoryController.js

@@ -0,0 +1,29 @@
1
+const { PaginationParam } = require("../../utils/PaginationParams");
2
+const statusHistoryService = require('../../services/sales/StatusHistoryService.js');
3
+const { validateCreateStatusHisotryRequest } = require("../../validators/admin/status_history/StatusHistoryValidators.js");
4
+const { StatusHistoryCollection } = require("../../resources/admin/status_history/StatusHistoryCollection.js");
5
+const { errorResponse, messageSuccessResponse } = require("../../utils/Response.js");
6
+
7
+exports.getAllStatusHistory = async (req, res) => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
10
+
11
+        const { status_histories, total } = await statusHistoryService.getAllStatusHistoryService({
12
+            page, limit, search, sortBy, orderBy
13
+        }, req);
14
+
15
+        return StatusHistoryCollection(req, res, status_histories, total, page, limit, 'Status history successfully retrieved');
16
+    } catch (err) {
17
+        return errorResponse(res, err);
18
+    }
19
+};
20
+
21
+exports.storeStatusHistory = async (req, res) => {
22
+    try {
23
+        const validatedData = validateCreateStatusHisotryRequest(req.body);
24
+        await statusHistoryService.storeStatusHistoryService(validatedData, req);
25
+        return messageSuccessResponse(res, 'Success added status history', 201);
26
+    } catch (err) {
27
+        return errorResponse(res, err);
28
+    }
29
+}

+ 18 - 3
src/repository/admin/HospitalRepository.js

@@ -21,6 +21,9 @@ const HospitalRepository = {
21 21
                 image: true,
22 22
                 progress_status: true,
23 23
                 note: true,
24
+                latitude: true,
25
+                longitude: true,
26
+                gmaps_url: true,
24 27
                 user: { select: { id: true, username: true } },
25 28
                 createdAt: true,
26 29
                 updatedAt: true,
@@ -68,6 +71,9 @@ const HospitalRepository = {
68 71
                 image: true,
69 72
                 progress_status: true,
70 73
                 note: true,
74
+                latitude: true,
75
+                longitude: true,
76
+                gmaps_url: true,
71 77
                 user: { select: { id: true, username: true } },
72 78
                 createdAt: true,
73 79
                 updatedAt: true,
@@ -103,14 +109,23 @@ const HospitalRepository = {
103 109
                 hospital_code: data.hospital_code,
104 110
                 type: data.type,
105 111
                 ownership: data.ownership,
106
-                province: { connect: { id: data.province_id } },
107
-                city: { connect: { id: data.city_id } },
112
+                // province: { connect: { id: data.province_id } },
113
+                // city: { connect: { id: data.city_id } },
114
+                ...(data.province_id && {
115
+                    province: { connect: { id: data.province_id } },
116
+                }),
117
+                ...(data.city_id && {
118
+                    city: { connect: { id: data.city_id } },
119
+                }),
108 120
                 address: data.address,
109 121
                 // simrs_type: data.simrs_type,
110 122
                 contact: data.contact,
111 123
                 note: data.note,
112 124
                 image: data.image,
113
-                progress_status: data.progress_status,
125
+                gmaps_url: data.gmaps_url,
126
+                latitude: data.latitude,
127
+                longitude: data.longitude,
128
+                // progress_status: data.progress_status,
114 129
             }
115 130
         });
116 131
     },

+ 69 - 0
src/repository/admin/StatusHistoryRepository.js

@@ -0,0 +1,69 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const StatusHistoryRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return prisma.statusHistory.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                hospital: {
13
+                    select: {
14
+                        id: true,
15
+                        name: true,
16
+                        // hospital_code: true,
17
+                        // type: true,
18
+                        // ownership: true,
19
+                        // province: {
20
+                        //     select: {
21
+                        //         id: true,
22
+                        //         name: true
23
+                        //     }
24
+                        // },
25
+                        // city: {
26
+                        //     select: {
27
+                        //         id: true,
28
+                        //         name: true
29
+                        //     }
30
+                        // },
31
+                        // address: true,
32
+                        // simrs_type: true,
33
+                        // contact: true,
34
+                        // image: true,
35
+                        progress_status: true,
36
+                        // note: true,
37
+                        // user: {
38
+                        //     select: {
39
+                        //         id: true,
40
+                        //         username: true
41
+                        //     }
42
+                        // }
43
+                    }
44
+                },
45
+                user: {
46
+                    select: {
47
+                        id: true,
48
+                        username: true,
49
+                    }
50
+                },
51
+                old_status: true,
52
+                new_status: true,
53
+                note: true,
54
+                createdAt: true,
55
+                updatedAt: true,
56
+            },
57
+        });
58
+    },
59
+
60
+    countAll: async (where) => {
61
+        return prisma.statusHistory.count({ where });
62
+    },
63
+
64
+    create: async (data) => {
65
+        return prisma.statusHistory.create({ data });
66
+    },
67
+};
68
+
69
+module.exports = StatusHistoryRepository;

+ 18 - 3
src/repository/sales/HospitalRepository.js

@@ -21,6 +21,9 @@ const HospitalRepository = {
21 21
                 image: true,
22 22
                 progress_status: true,
23 23
                 note: true,
24
+                latitude: true,
25
+                longitude: true,
26
+                gmaps_url: true,
24 27
                 user: { select: { id: true, username: true } },
25 28
                 createdAt: true,
26 29
                 updatedAt: true,
@@ -56,6 +59,9 @@ const HospitalRepository = {
56 59
                 image: true,
57 60
                 progress_status: true,
58 61
                 note: true,
62
+                latitude: true,
63
+                longitude: true,
64
+                gmaps_url: true,
59 65
                 user: { select: { id: true, username: true } },
60 66
                 createdAt: true,
61 67
                 updatedAt: true,
@@ -71,14 +77,23 @@ const HospitalRepository = {
71 77
                 hospital_code: data.hospital_code,
72 78
                 type: data.type,
73 79
                 ownership: data.ownership,
74
-                province: { connect: { id: data.province_id } },
75
-                city: { connect: { id: data.city_id } },
80
+                // province: { connect: { id: data.province_id } },
81
+                // city: { connect: { id: data.city_id } },
82
+                ...(data.province_id && {
83
+                    province: { connect: { id: data.province_id } },
84
+                }),
85
+                ...(data.city_id && {
86
+                    city: { connect: { id: data.city_id } },
87
+                }),
76 88
                 address: data.address,
77 89
                 // simrs_type: data.simrs_type,
78 90
                 contact: data.contact,
79 91
                 note: data.note,
80 92
                 image: data.image,
81
-                progress_status: data.progress_status,
93
+                gmaps_url: data.gmaps_url,
94
+                latitude: data.latitude,
95
+                longitude: data.longitude,
96
+                // progress_status: data.progress_status,
82 97
             }
83 98
         });
84 99
     },

+ 69 - 0
src/repository/sales/StatusHistoryRepository.js

@@ -0,0 +1,69 @@
1
+const prisma = require('../../prisma/PrismaClient.js');
2
+
3
+const StatusHistoryRepository = {
4
+    findAll: async ({ skip, take, where, orderBy }) => {
5
+        return prisma.statusHistory.findMany({
6
+            where,
7
+            skip,
8
+            take,
9
+            orderBy,
10
+            select: {
11
+                id: true,
12
+                hospital: {
13
+                    select: {
14
+                        id: true,
15
+                        name: true,
16
+                        // hospital_code: true,
17
+                        // type: true,
18
+                        // ownership: true,
19
+                        // province: {
20
+                        //     select: {
21
+                        //         id: true,
22
+                        //         name: true
23
+                        //     }
24
+                        // },
25
+                        // city: {
26
+                        //     select: {
27
+                        //         id: true,
28
+                        //         name: true
29
+                        //     }
30
+                        // },
31
+                        // address: true,
32
+                        // simrs_type: true,
33
+                        // contact: true,
34
+                        // image: true,
35
+                        progress_status: true,
36
+                        // note: true,
37
+                        // user: {
38
+                        //     select: {
39
+                        //         id: true,
40
+                        //         username: true
41
+                        //     }
42
+                        // }
43
+                    }
44
+                },
45
+                user: {
46
+                    select: {
47
+                        id: true,
48
+                        username: true,
49
+                    }
50
+                },
51
+                old_status: true,
52
+                new_status: true,
53
+                note: true,
54
+                createdAt: true,
55
+                updatedAt: true,
56
+            },
57
+        });
58
+    },
59
+
60
+    countAll: async (where) => {
61
+        return prisma.statusHistory.count({ where });
62
+    },
63
+
64
+    create: async (data) => {
65
+        return prisma.statusHistory.create({ data });
66
+    },
67
+};
68
+
69
+module.exports = StatusHistoryRepository;

+ 23 - 0
src/resources/admin/status_history/StatusHistoryCollection.js

@@ -0,0 +1,23 @@
1
+const { ListResponse } = require("../../../utils/ListResponse");
2
+const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
3
+
4
+const formatItem = (item) => ({
5
+    ...item,
6
+    note: item.note?.trim() === '' ? null : item.note,
7
+    createdAt: formatISOWithoutTimezone(item.createdAt),
8
+    updatedAt: formatISOWithoutTimezone(item.updatedAt)
9
+});
10
+
11
+exports.StatusHistoryCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
12
+    const formattedData = data.map(formatItem);
13
+
14
+    if (typeof total !== 'number') {
15
+        return res.status(200).json({
16
+            success: true,
17
+            message,
18
+            data: Array.isArray(formattedData)
19
+        });
20
+    }
21
+
22
+    return ListResponse({ req, res, data: formattedData, total, page, limit, message });
23
+};

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

@@ -3,6 +3,7 @@ const router = express.Router()
3 3
 const hospitalController = require('../../controllers/admin/HospitalController.js')
4 4
 const vendorHistoryController = require('../../controllers/admin/VendorHistoryController.js')
5 5
 const executivesHistoryController = require('../../controllers/admin/ExecutivesHistoryController.js')
6
+const statusHistoriesController = require('../../controllers/admin/StatusHistoryController.js')
6 7
 const verifyJWT = require('../../middleware/VerifyJWT.js');
7 8
 const checkRole = require('../../middleware/CheckRole.js');
8 9
 const upload = require('../../middleware/UploadImage.js');
@@ -27,4 +28,8 @@ router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRol
27 28
 router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.updateExecutivesHistory);
28 29
 router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['admin']), executivesHistoryController.deleteExecutivesHistory);
29 30
 
31
+// Status History
32
+router.get('/:id/status-histories', verifyJWT, checkRole(['admin']), statusHistoriesController.getAllStatusHistory);
33
+router.post('/:id/status-histories', verifyJWT, checkRole(['admin']), statusHistoriesController.storeStatusHistory);
34
+
30 35
 module.exports = router;

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

@@ -3,6 +3,7 @@ const router = express.Router()
3 3
 const hospitalController = require('../../controllers/sales/HospitalController.js')
4 4
 const vendorHistoryController = require('../../controllers/sales/VendorHistoryController.js')
5 5
 const executivesHistoryController = require('../../controllers/sales/ExecutivesHistoryController.js')
6
+const statusHistoryController = require('../../controllers/sales/StatusHistoryController.js')
6 7
 const verifyJWT = require('../../middleware/VerifyJWT.js');
7 8
 const checkRole = require('../../middleware/CheckRole.js');
8 9
 const upload = require('../../middleware/UploadImage.js');
@@ -26,4 +27,7 @@ router.get('/:id/executives-history/:id_executives_history', verifyJWT, checkRol
26 27
 router.patch('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.updateExecutivesHistory);
27 28
 router.delete('/:id/executives-history/:id_executives_history', verifyJWT, checkRole(['sales']), executivesHistoryController.deleteExecutivesHistory);
28 29
 
30
+router.get('/:id/status-history', verifyJWT, checkRole(['sales']), statusHistoryController.getAllStatusHistory);
31
+router.post('/:id/status-history', verifyJWT, checkRole(['sales']), statusHistoryController.storeStatusHistory);
32
+
29 33
 module.exports = router;

+ 92 - 11
src/services/admin/HospitalService.js

@@ -69,12 +69,45 @@ exports.storeHospitalService = async (validateData, req) => {
69 69
 
70 70
     const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
71 71
 
72
+    let latitude = validateData.latitude ?? null;
73
+    let longitude = validateData.longitude ?? null;
74
+    let gmapsUrl = validateData.gmaps_url ?? null;
75
+
76
+    if (gmapsUrl) {
77
+        if (gmapsUrl.includes("www.google.com/maps")) {
78
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
79
+            const match = gmapsUrl.match(regex);
80
+
81
+            if (match) {
82
+                latitude = parseFloat(match[1]);
83
+                longitude = parseFloat(match[2]);
84
+            } else {
85
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
86
+            }
87
+
88
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
89
+            latitude = null;
90
+            longitude = null;
91
+
92
+        } else {
93
+            // URL disediakan tapi bukan dari domain yang valid
94
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
95
+        }
96
+    } else if (latitude !== null && longitude !== null) {
97
+        gmapsUrl = null;
98
+    } else {
99
+        throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
100
+    }
101
+
72 102
     const payload = {
73 103
         ...validateData,
74 104
         image: imagePath,
75 105
         progress_status: "cari_data",
76 106
         // simrs_type: "-",
77
-        created_by: creatorId
107
+        created_by: creatorId,
108
+        latitude,
109
+        longitude,
110
+        gmaps_url: gmapsUrl,
78 111
     };
79 112
 
80 113
     const data = await HospitalRepository.create(payload);
@@ -105,17 +138,21 @@ exports.updateHospitalService = async (validateData, id, req) => {
105 138
             422
106 139
         );
107 140
     }
108
-
109
-    const existingHospital = await prisma.hospital.findFirst({
110
-        where: {
111
-            name: validateData.name,
112
-            city_id: validateData.city_id,
113
-            deletedAt: null
141
+    if (validateData.name && validateData.city_id) {
142
+        const existingHospital = await prisma.hospital.findFirst({
143
+            where: {
144
+                name: validateData.name,
145
+                city_id: validateData.city_id,
146
+                deletedAt: null,
147
+                // NOT: {
148
+                //     id: id
149
+                // }
150
+            }
151
+        });
152
+
153
+        if (existingHospital) {
154
+            throw new HttpException('Hospital with same name in this city already exists', 400);
114 155
         }
115
-    });
116
-
117
-    if (existingHospital) {
118
-        throw new HttpException('Hospital with same name in this city already exists', 400);
119 156
     }
120 157
 
121 158
     // Jika ada file baru, replace image
@@ -124,10 +161,54 @@ exports.updateHospitalService = async (validateData, id, req) => {
124 161
         imagePath = `/storage/img/${req.file.filename}`; // path relatif
125 162
     }
126 163
 
164
+
165
+    // Handle koordinat dan gmaps_url
166
+    let latitude = hospital.latitude;
167
+    let longitude = hospital.longitude;
168
+    let gmapsUrl = hospital.gmaps_url;
169
+
170
+    if (
171
+        validateData.latitude !== undefined &&
172
+        validateData.longitude !== undefined &&
173
+        validateData.latitude !== null &&
174
+        validateData.longitude !== null
175
+    ) {
176
+        // Jika diberikan lat long langsung
177
+        latitude = validateData.latitude;
178
+        longitude = validateData.longitude;
179
+        gmapsUrl = validateData.gmaps_url || gmapsUrl;
180
+    } else if (
181
+        validateData.gmaps_url &&
182
+        typeof validateData.gmaps_url === "string" &&
183
+        validateData.gmaps_url.trim() !== ""
184
+    ) {
185
+        gmapsUrl = validateData.gmaps_url;
186
+
187
+        if (gmapsUrl.includes("www.google.com/maps")) {
188
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
189
+            const match = gmapsUrl.match(regex);
190
+            if (match) {
191
+                latitude = parseFloat(match[1]);
192
+                longitude = parseFloat(match[2]);
193
+            } else {
194
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
195
+            }
196
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
197
+            // Tidak bisa ambil koordinat langsung
198
+            latitude = null;
199
+            longitude = null;
200
+        } else {
201
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
202
+        }
203
+    }
204
+
127 205
     const payload = {
128 206
         ...validateData,
129 207
         image: imagePath,
130 208
         // created_by: req.user.id,
209
+        latitude,
210
+        longitude,
211
+        gmaps_url: gmapsUrl,
131 212
     };
132 213
 
133 214
     const data = await HospitalRepository.update(id, payload);

+ 79 - 0
src/services/admin/StatusHistoryService.js

@@ -0,0 +1,79 @@
1
+const HttpException = require('../../utils/HttpException.js');
2
+const prisma = require('../../prisma/PrismaClient.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 { formatDateOnly, formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
7
+const StatusHistoryRepository = require('../../repository/admin/StatusHistoryRepository.js');
8
+
9
+exports.getAllStatusHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
10
+    const skip = (page - 1) * limit;
11
+
12
+    const hospitalId = req.params.id;
13
+    const hospital = await prisma.hospital.findFirst({
14
+        where: {
15
+            id: hospitalId
16
+        }
17
+    })
18
+    if (!hospital) {
19
+        throw new HttpException("Hospital not found", 404)
20
+    }
21
+
22
+    const where = {
23
+        hospital_id: req.params.id,
24
+        deletedAt: null
25
+    };
26
+
27
+    const [status_histories, total] = await Promise.all([
28
+        StatusHistoryRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
29
+        StatusHistoryRepository.countAll(where)
30
+    ]);
31
+
32
+    return { status_histories, total };
33
+};
34
+
35
+const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
36
+
37
+exports.storeStatusHistoryService = async (validateData, req) => {
38
+    const hospitalId = req.params.id;
39
+    const userId = req.user.id;
40
+
41
+    const hospital = await prisma.hospital.findFirst({
42
+        where: {
43
+            id: hospitalId
44
+        }
45
+    })
46
+    if (!hospital) {
47
+        throw new HttpException("Hospital not found", 404)
48
+    }
49
+
50
+    if (validateData.new_status && !validProgressStatuses.includes(validateData.new_status)) {
51
+        throw new HttpException(
52
+            `Invalid new_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
53
+            422
54
+        );
55
+    }
56
+
57
+    if (validateData.new_status === hospital.progress_status) {
58
+        throw new HttpException("New status change is the same as old status", 400)
59
+    }
60
+
61
+    const payload = {
62
+        hospital_id: hospitalId,
63
+        user_id: userId,
64
+        old_status: hospital.progress_status,
65
+        new_status: validateData.new_status,
66
+        note: validateData.note,
67
+    };
68
+
69
+    const data = await StatusHistoryRepository.create(payload);
70
+
71
+    await prisma.hospital.update({
72
+        where: { id: hospitalId },
73
+        data: {
74
+            progress_status: validateData.new_status
75
+        }
76
+    });
77
+
78
+    await createLog(req, data);
79
+};

+ 94 - 8
src/services/sales/HospitalService.js

@@ -6,7 +6,6 @@ const ProvinceRepository = require('../../repository/admin/ProvinceRepository.js
6 6
 const CityRepository = require('../../repository/admin/CityRepository.js');
7 7
 const HttpException = require('../../utils/HttpException.js');
8 8
 const HospitalRepository = require('../../repository/admin/HospitalRepository.js');
9
-const { formatISOWithoutTimezone } = require('../../utils/FormatDate.js');
10 9
 
11 10
 exports.getAllHospitalByAreaService = async ({ page, limit, search, sortBy, orderBy, province, city, type, ownership, progress_status }, req) => {
12 11
     const skip = (page - 1) * limit;
@@ -134,12 +133,45 @@ exports.storeHospitalService = async (validateData, req) => {
134 133
 
135 134
     const imagePath = req.file ? `/storage/img/${req.file.filename}` : null;
136 135
 
136
+    let latitude = validateData.latitude ?? null;
137
+    let longitude = validateData.longitude ?? null;
138
+    let gmapsUrl = validateData.gmaps_url ?? null;
139
+
140
+    if (gmapsUrl) {
141
+        if (gmapsUrl.includes("www.google.com/maps")) {
142
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
143
+            const match = gmapsUrl.match(regex);
144
+
145
+            if (match) {
146
+                latitude = parseFloat(match[1]);
147
+                longitude = parseFloat(match[2]);
148
+            } else {
149
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
150
+            }
151
+
152
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
153
+            latitude = null;
154
+            longitude = null;
155
+
156
+        } else {
157
+            // URL disediakan tapi bukan dari domain yang valid
158
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
159
+        }
160
+    } else if (latitude !== null && longitude !== null) {
161
+        gmapsUrl = null;
162
+    } else {
163
+        throw new HttpException("Either gmaps_url or coordinates must be provided", 400);
164
+    }
165
+
137 166
     const payload = {
138 167
         ...validateData,
139 168
         image: imagePath,
140 169
         progress_status: "cari_data",
141 170
         simrs_type: "-",
142
-        created_by: creatorId
171
+        created_by: creatorId,
172
+        latitude,
173
+        longitude,
174
+        gmaps_url: gmapsUrl,
143 175
     };
144 176
 
145 177
     const data = await salesHospitalRepository.create(payload);
@@ -182,13 +214,24 @@ exports.updateHospitalService = async (validateData, id, req) => {
182 214
         );
183 215
     }
184 216
 
185
-    const existingHospital = await prisma.hospital.findFirst({
186
-        where: {
187
-            name: validateData.name,
188
-            city_id: validateData.city_id,
189
-            deletedAt: null
217
+    let existingHospital = null;
218
+
219
+    if (validateData.name && validateData.city_id) {
220
+        existingHospital = await prisma.hospital.findFirst({
221
+            where: {
222
+                name: validateData.name,
223
+                city_id: validateData.city_id,
224
+                deletedAt: null,
225
+                // NOT: {
226
+                //     id: id,
227
+                // }
228
+            }
229
+        });
230
+
231
+        if (existingHospital) {
232
+            throw new HttpException('Hospital with same name in this city already exists', 400);
190 233
         }
191
-    });
234
+    }
192 235
 
193 236
     if (existingHospital) {
194 237
         throw new HttpException('Hospital with same name in this city already exists', 400);
@@ -200,10 +243,53 @@ exports.updateHospitalService = async (validateData, id, req) => {
200 243
         imagePath = `/storage/img/${req.file.filename}`; // path relatif
201 244
     }
202 245
 
246
+    // Handle koordinat dan gmaps_url
247
+    let latitude = hospital.latitude;
248
+    let longitude = hospital.longitude;
249
+    let gmapsUrl = hospital.gmaps_url;
250
+
251
+    if (
252
+        validateData.latitude !== undefined &&
253
+        validateData.longitude !== undefined &&
254
+        validateData.latitude !== null &&
255
+        validateData.longitude !== null
256
+    ) {
257
+        // Jika diberikan lat long langsung
258
+        latitude = validateData.latitude;
259
+        longitude = validateData.longitude;
260
+        gmapsUrl = validateData.gmaps_url || gmapsUrl;
261
+    } else if (
262
+        validateData.gmaps_url &&
263
+        typeof validateData.gmaps_url === "string" &&
264
+        validateData.gmaps_url.trim() !== ""
265
+    ) {
266
+        gmapsUrl = validateData.gmaps_url;
267
+
268
+        if (gmapsUrl.includes("www.google.com/maps")) {
269
+            const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
270
+            const match = gmapsUrl.match(regex);
271
+            if (match) {
272
+                latitude = parseFloat(match[1]);
273
+                longitude = parseFloat(match[2]);
274
+            } else {
275
+                throw new HttpException("Unable to extract coordinates from gmaps_url", 400);
276
+            }
277
+        } else if (gmapsUrl.includes("maps.app.goo.gl")) {
278
+            // Tidak bisa ambil koordinat langsung
279
+            latitude = null;
280
+            longitude = null;
281
+        } else {
282
+            throw new HttpException("gmaps_url must be a valid Google Maps URL", 400);
283
+        }
284
+    }
285
+
203 286
     const payload = {
204 287
         ...validateData,
205 288
         image: imagePath,
206 289
         // created_by: req.user.id,
290
+        latitude,
291
+        longitude,
292
+        gmaps_url: gmapsUrl,
207 293
     };
208 294
 
209 295
     const data = await salesHospitalRepository.update(id, payload);

+ 99 - 0
src/services/sales/StatusHistoryService.js

@@ -0,0 +1,99 @@
1
+const HttpException = require('../../utils/HttpException.js');
2
+const prisma = require('../../prisma/PrismaClient.js');
3
+const { createLog } = require('../../utils/LogActivity.js');
4
+const StatusHistoryRepository = require('../../repository/sales/StatusHistoryRepository.js');
5
+
6
+exports.getAllStatusHistoryService = async ({ page, limit, search, sortBy, orderBy }, req) => {
7
+    const skip = (page - 1) * limit;
8
+
9
+    const userId = req.user.id;
10
+    const hospitalId = req.params.id;
11
+    const hospital = await prisma.hospital.findFirst({
12
+        where: {
13
+            id: hospitalId
14
+        }
15
+    })
16
+    if (!hospital) {
17
+        throw new HttpException("Hospital not found", 404)
18
+    }
19
+
20
+    const userAreas = await prisma.userArea.findMany({
21
+        where: { user_id: userId },
22
+        select: { province_id: true }
23
+    });
24
+
25
+    const userProvinceIds = userAreas.map(ua => ua.province_id);
26
+
27
+    if (!userProvinceIds.includes(hospital.province_id)) {
28
+        throw new HttpException("This hospital is not your area", 403);
29
+    }
30
+
31
+    const where = {
32
+        hospital_id: req.params.id,
33
+        deletedAt: null
34
+    };
35
+
36
+    const [status_histories, total] = await Promise.all([
37
+        StatusHistoryRepository.findAll({ skip, take: limit, where, orderBy: { [sortBy]: orderBy } }),
38
+        StatusHistoryRepository.countAll(where)
39
+    ]);
40
+
41
+    return { status_histories, total };
42
+};
43
+
44
+const validProgressStatuses = ['cari_data', 'dihubungi', 'negosiasi', 'follow_up', 'mou', 'onboarded', 'tidak_berminat'];
45
+
46
+exports.storeStatusHistoryService = async (validateData, req) => {
47
+    const hospitalId = req.params.id;
48
+    const userId = req.user.id;
49
+
50
+    const hospital = await prisma.hospital.findFirst({
51
+        where: {
52
+            id: hospitalId
53
+        }
54
+    })
55
+    if (!hospital) {
56
+        throw new HttpException("Hospital not found", 404)
57
+    }
58
+
59
+    const userAreas = await prisma.userArea.findMany({
60
+        where: { user_id: userId },
61
+        select: { province_id: true }
62
+    });
63
+
64
+    const userProvinceIds = userAreas.map(ua => ua.province_id);
65
+
66
+    if (!userProvinceIds.includes(hospital.province_id)) {
67
+        throw new HttpException("This hospital is not your area", 403);
68
+    }
69
+
70
+    if (validateData.new_status && !validProgressStatuses.includes(validateData.new_status)) {
71
+        throw new HttpException(
72
+            `Invalid new_status. Allowed values are: ${validProgressStatuses.join(', ')}`,
73
+            422
74
+        );
75
+    }
76
+
77
+    if (validateData.new_status === hospital.progress_status) {
78
+        throw new HttpException("New status change is the same as old status", 400)
79
+    }
80
+
81
+    const payload = {
82
+        hospital_id: hospitalId,
83
+        user_id: userId,
84
+        old_status: hospital.progress_status,
85
+        new_status: validateData.new_status,
86
+        note: validateData.note,
87
+    };
88
+
89
+    const data = await StatusHistoryRepository.create(payload);
90
+
91
+    await prisma.hospital.update({
92
+        where: { id: hospitalId },
93
+        data: {
94
+            progress_status: validateData.new_status
95
+        }
96
+    });
97
+
98
+    await createLog(req, data);
99
+};

+ 52 - 46
src/validators/admin/hospital/HospitalValidators.js

@@ -1,7 +1,7 @@
1 1
 const HttpException = require('../../../utils/HttpException.js');
2 2
 
3 3
 exports.validateStoreHospitalRequest = (body) => {
4
-    const { name, hospital_code, type, ownership, province_id, city_id, address, contact, note } = body;
4
+    const { name, hospital_code, type, ownership, province_id, city_id, address, contact, note, gmaps_url, latitude, longitude } = body;
5 5
 
6 6
     const errors = {};
7 7
 
@@ -60,73 +60,79 @@ exports.validateStoreHospitalRequest = (body) => {
60 60
         // simrs_type: simrs_type.trim(),
61 61
         contact: contact.trim(),
62 62
         note: note.trim(),
63
+        gmaps_url: gmaps_url ? gmaps_url.trim() : null,
64
+        latitude: latitude !== undefined && latitude !== null ? parseFloat(latitude) : null,
65
+        longitude: longitude !== undefined && longitude !== null ? parseFloat(longitude) : null,
63 66
     };
64 67
 };
65 68
 
66 69
 exports.validateUpdateHospitalRequest = (body) => {
67
-    const { name, hospital_code, type, ownership, province_id, city_id, address, progress_status, contact, note } = body;
70
+    const { name, hospital_code, type, ownership, province_id, city_id, address, contact, note, gmaps_url, latitude, longitude } = body;
68 71
 
69
-    const errors = {};
72
+    // const errors = {};
70 73
 
71
-    if (!name || name.trim() === '') {
72
-        errors.name = ['name is required'];
73
-    }
74
+    // if (!name || name.trim() === '') {
75
+    //     errors.name = ['name is required'];
76
+    // }
74 77
 
75
-    if (!hospital_code || hospital_code.trim() === '') {
76
-        errors.hospital_code = ['hospital code is required'];
77
-    }
78
+    // if (!hospital_code || hospital_code.trim() === '') {
79
+    //     errors.hospital_code = ['hospital code is required'];
80
+    // }
78 81
 
79
-    if (!type || type.trim() === '') {
80
-        errors.type = ['type is required'];
81
-    }
82
+    // if (!type || type.trim() === '') {
83
+    //     errors.type = ['type is required'];
84
+    // }
82 85
 
83
-    if (!ownership || ownership.trim() === '') {
84
-        errors.ownership = ['ownership is required'];
85
-    }
86
+    // if (!ownership || ownership.trim() === '') {
87
+    //     errors.ownership = ['ownership is required'];
88
+    // }
86 89
 
87
-    if (!province_id || province_id.trim() === '') {
88
-        errors.province_id = ['province id is required'];
89
-    }
90
+    // if (!province_id || province_id.trim() === '') {
91
+    //     errors.province_id = ['province id is required'];
92
+    // }
90 93
 
91
-    if (!city_id || city_id.trim() === '') {
92
-        errors.city_id = ['city id is required'];
93
-    }
94
+    // if (!city_id || city_id.trim() === '') {
95
+    //     errors.city_id = ['city id is required'];
96
+    // }
94 97
 
95
-    if (!address || address.trim() === '') {
96
-        errors.address = ['address is required'];
97
-    }
98
+    // if (!address || address.trim() === '') {
99
+    //     errors.address = ['address is required'];
100
+    // }
98 101
 
99 102
     // if (!simrs_type || simrs_type.trim() === '') {
100 103
     //     errors.simrs_type = ['simrs type is required'];
101 104
     // }
102 105
 
103
-    if (!progress_status || progress_status.trim() === '') {
104
-        errors.progress_status = ['progress status is required'];
105
-    }
106
+    // if (!progress_status || progress_status.trim() === '') {
107
+    //     errors.progress_status = ['progress status is required'];
108
+    // }
106 109
 
107
-    if (!contact || contact.trim() === '') {
108
-        errors.contact = ['contact is required'];
109
-    }
110
+    // if (!contact || contact.trim() === '') {
111
+    //     errors.contact = ['contact is required'];
112
+    // }
110 113
 
111
-    if (!note || note.trim() === '') {
112
-        errors.note = ['note is required'];
113
-    }
114
+    // if (!note || note.trim() === '') {
115
+    //     errors.note = ['note is required'];
116
+    // }
114 117
 
115
-    if (Object.keys(errors).length > 0) {
116
-        throw new HttpException(errors, 422);
117
-    }
118
+    // if (Object.keys(errors).length > 0) {
119
+    //     throw new HttpException(errors, 422);
120
+    // }
118 121
 
119 122
     return {
120
-        name: name.trim(),
121
-        hospital_code: hospital_code.trim(),
122
-        type: type.trim(),
123
-        ownership: ownership.trim(),
124
-        province_id: province_id.trim(),
125
-        city_id: city_id.trim(),
126
-        address: address.trim(),
123
+        name,
124
+        hospital_code,
125
+        type,
126
+        ownership,
127
+        province_id,
128
+        city_id,
129
+        address,
127 130
         // simrs_type: simrs_type.trim(),
128
-        progress_status: progress_status.trim(),
129
-        contact: contact.trim(),
130
-        note: note.trim(),
131
+        // progress_status: progress_status.trim(),
132
+        contact,
133
+        note,
134
+        gmaps_url: gmaps_url ? gmaps_url.trim() : null,
135
+        latitude: latitude !== undefined && latitude !== null ? parseFloat(latitude) : null,
136
+        longitude: longitude !== undefined && longitude !== null ? parseFloat(longitude) : null,
131 137
     };
132 138
 };

+ 19 - 0
src/validators/admin/status_history/StatusHistoryValidators.js

@@ -0,0 +1,19 @@
1
+const HttpException = require('../../../utils/HttpException.js');
2
+
3
+exports.validateCreateStatusHisotryRequest = (body) => {
4
+    const { new_status, note } = body;
5
+    const errors = {};
6
+
7
+    if (!new_status || new_status.trim() === '') {
8
+        errors.new_status = ['new_status is required'];
9
+    }
10
+
11
+    if (Object.keys(errors).length > 0) {
12
+        throw new HttpException(errors, 422);
13
+    }
14
+
15
+    return {
16
+        new_status: new_status.trim(),
17
+        note: note.trim()
18
+    };
19
+};

BIN
storage/img/1751530124686-242372727.jpeg


BIN
storage/img/1751530180923-52842900.jpeg


BIN
storage/img/1751530194422-59175162.jpeg


BIN
storage/img/1751530268105-213241601.jpeg


BIN
storage/img/1751530283736-429201881.jpeg


BIN
storage/img/1751530339156-411930692.jpeg


BIN
storage/img/1751531740648-913448829.jpeg


BIN
storage/img/1751531755792-596895748.jpeg


BIN
storage/img/1751531931056-85404101.jpeg


BIN
storage/img/1751531939833-744370817.jpeg


BIN
storage/img/1751533085578-919962860.jpeg


BIN
storage/img/1751533138374-79830774.jpeg


BIN
storage/img/1751533144888-439729161.jpeg


BIN
storage/img/1751533229041-456120975.jpeg


BIN
storage/img/1751533232526-27606846.jpeg


BIN
storage/img/1751533366302-905436030.jpeg


BIN
storage/img/1751533371765-688758068.jpeg


BIN
storage/img/1751533414588-875811984.jpeg


BIN
storage/img/1751533483838-682770826.jpeg


BIN
storage/img/1751533489808-439186721.jpeg


BIN
storage/img/1751533625364-355301513.jpeg


BIN
storage/img/1751533629075-189527972.jpeg


BIN
storage/img/1751533645996-765118559.jpeg


BIN
storage/img/1751533661341-594783356.jpeg


BIN
storage/img/1751533682479-471230046.jpeg


BIN
storage/img/1751533687396-26757586.jpeg


BIN
storage/img/1751600673670-697106882.jpeg


BIN
storage/img/1751600707027-644225437.jpeg


BIN
storage/img/1751600737290-309748691.jpeg


BIN
storage/img/1751600784523-161212058.jpeg


BIN
storage/img/1751600916157-757176239.jpeg


BIN
storage/img/1751601000952-486008833.jpeg


BIN
storage/img/1751601036067-316245070.jpeg


BIN
storage/img/1751602417119-723126231.png


BIN
storage/img/1751602553616-291701965.png


BIN
storage/img/1751602558566-257160389.png


BIN
storage/img/1751602580094-621745417.png


BIN
storage/img/1751602584492-314175553.png


BIN
storage/img/1751603812511-236372234.png


BIN
storage/img/1751603872927-730181195.png


BIN
storage/img/1751603887789-354294165.png


BIN
storage/img/1751603901880-622251451.png


BIN
storage/img/1751603929998-540007390.png


BIN
storage/img/1751604446265-284837886.png


BIN
storage/img/1751604459294-361264422.png


BIN
storage/img/1751604487170-978421322.png


BIN
storage/img/1751604732952-131082581.png


BIN
storage/img/1751604753650-29307011.png


BIN
storage/img/1751604794637-627486397.png


BIN
storage/img/1751604912435-637315351.png


BIN
storage/img/1751605011757-849707384.png


BIN
storage/img/1751608744824-668781574.png


BIN
storage/img/1751609253933-105180482.png


BIN
storage/img/1751609259292-637789973.png