Browse Source

Merge branch 'development'

pearlgw 6 days ago
parent
commit
d0d9eea274
26 changed files with 1508 additions and 39 deletions
  1. 6 0
      index.ts
  2. 24 0
      prisma/migrations/20250901021101_add_field_schedule_visitations/migration.sql
  3. 8 0
      prisma/migrations/20250901025643_add_field_in_shcedule_visitation/migration.sql
  4. 61 39
      prisma/schema.prisma
  5. 16 0
      src/controllers/admin/SalesController.ts
  6. 81 0
      src/controllers/admin/ScheduleVisitationController.ts
  7. 69 0
      src/controllers/sales/ScheduleVisitationController.ts
  8. 43 0
      src/repository/admin/SalesRepository.ts
  9. 139 0
      src/repository/admin/ScheduleVisitationRepository.ts
  10. 139 0
      src/repository/sales/ScheduleVisitationRepository.ts
  11. 56 0
      src/resources/admin/sales/SalesCollection.ts
  12. 45 0
      src/resources/admin/schedule_visitation/ScheduleVisitationCollection.ts
  13. 40 0
      src/resources/admin/schedule_visitation/ScheduleVisitationResource.ts
  14. 45 0
      src/resources/sales/schedule_visitation/ScheduleVisitationCollection.ts
  15. 40 0
      src/resources/sales/schedule_visitation/ScheduleVisitationResource.ts
  16. 11 0
      src/routes/admin/SalesRoute.ts
  17. 15 0
      src/routes/admin/ScheduleVisitationRoute.ts
  18. 14 0
      src/routes/sales/ScheduleVisitationRoute.ts
  19. 32 0
      src/services/admin/SalesService.ts
  20. 188 0
      src/services/admin/ScheduleVisitationService.ts
  21. 199 0
      src/services/sales/ScheduleVisitationService.ts
  22. 9 0
      src/types/admin/sales/SalesDTO.ts
  23. 80 0
      src/types/admin/schedule_visitation/ScheduleVisitationDTO.ts
  24. 80 0
      src/types/sales/schedule_visitation/ScheduleVisitationDTO.ts
  25. 34 0
      src/validators/admin/schedule_visitation/ScheduleVisitationValidators.ts
  26. 34 0
      src/validators/sales/schedule_visitation/ScheduleVisitationValidators.ts

+ 6 - 0
index.ts

@@ -14,6 +14,9 @@ import hospitalRoutes from './src/routes/admin/HospitalRoute';
14 14
 import salesHospitalRoutes from './src/routes/sales/HospitalRoute';
15 15
 import areaRoutes from './src/routes/sales/AreaRoute';
16 16
 import CategoryRoutes from './src/routes/admin/CategoryRoute';
17
+import ScheduleVisitationRoutes from './src/routes/admin/ScheduleVisitationRoute';
18
+import ScheduleVisitationForSalesRoutes from './src/routes/sales/ScheduleVisitationRoute';
19
+import salesRoutes from './src/routes/admin/SalesRoute';
17 20
 
18 21
 import './src/utils/Scheduler';
19 22
 
@@ -43,12 +46,15 @@ app.use('/storage/', express.static(path.join(__dirname, 'storage/')));
43 46
 
44 47
 const apiV1 = express.Router();
45 48
 apiV1.use('/province', provinceRoutes);
49
+apiV1.use('/sales', salesRoutes);
46 50
 apiV1.use('/city', cityRoutes);
47 51
 apiV1.use('/hospital', hospitalRoutes);
48 52
 apiV1.use('/hospital-area', salesHospitalRoutes);
49 53
 apiV1.use('/vendor', vendorRoutes);
50 54
 apiV1.use('/area', areaRoutes);
51 55
 apiV1.use('/category', CategoryRoutes);
56
+apiV1.use('/schedule-visitation', ScheduleVisitationRoutes);
57
+apiV1.use('/schedule-sales', ScheduleVisitationForSalesRoutes);
52 58
 
53 59
 app.get('', (req: Request, res: Response) => {
54 60
     res.send('Selamat Datang di API Radar Farmagitechs');

+ 24 - 0
prisma/migrations/20250901021101_add_field_schedule_visitations/migration.sql

@@ -0,0 +1,24 @@
1
+-- CreateTable
2
+CREATE TABLE "schedule_visitations" (
3
+    "id" TEXT NOT NULL,
4
+    "date_visit" TIMESTAMP(3) NOT NULL,
5
+    "sales_id" TEXT NOT NULL,
6
+    "hospital_id" TEXT NOT NULL,
7
+    "province_id" TEXT NOT NULL,
8
+    "notes" TEXT,
9
+    "created_by" TEXT NOT NULL,
10
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
+    "updatedAt" TIMESTAMP(3) NOT NULL,
12
+    "deletedAt" TIMESTAMP(3),
13
+
14
+    CONSTRAINT "schedule_visitations_pkey" PRIMARY KEY ("id")
15
+);
16
+
17
+-- AddForeignKey
18
+ALTER TABLE "schedule_visitations" ADD CONSTRAINT "schedule_visitations_hospital_id_fkey" FOREIGN KEY ("hospital_id") REFERENCES "hospitals"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
19
+
20
+-- AddForeignKey
21
+ALTER TABLE "schedule_visitations" ADD CONSTRAINT "schedule_visitations_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "keycloak_users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
22
+
23
+-- AddForeignKey
24
+ALTER TABLE "schedule_visitations" ADD CONSTRAINT "schedule_visitations_sales_id_fkey" FOREIGN KEY ("sales_id") REFERENCES "keycloak_users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

+ 8 - 0
prisma/migrations/20250901025643_add_field_in_shcedule_visitation/migration.sql

@@ -0,0 +1,8 @@
1
+/*
2
+  Warnings:
3
+
4
+  - Added the required column `city_id` to the `schedule_visitations` table without a default value. This is not possible if the table is not empty.
5
+
6
+*/
7
+-- AlterTable
8
+ALTER TABLE "schedule_visitations" ADD COLUMN     "city_id" TEXT NOT NULL;

+ 61 - 39
prisma/schema.prisma

@@ -59,12 +59,14 @@ model User {
59 59
 }
60 60
 
61 61
 model UserKeycloak {
62
-  id            String          @id @default(uuid())
63
-  fullname      String
64
-  Hospital      Hospital[]
65
-  UserArea      UserArea[]
66
-  Vendor        Vendor[]
67
-  StatusHistory StatusHistory[]
62
+  id                 String               @id @default(uuid())
63
+  fullname           String
64
+  Hospital           Hospital[]
65
+  UserArea           UserArea[]
66
+  Vendor             Vendor[]
67
+  StatusHistory      StatusHistory[]
68
+  CreatedVisitations ScheduleVisitation[] @relation("CreatedVisitations")
69
+  SalesVisitations   ScheduleVisitation[] @relation("SalesVisitations")
68 70
 
69 71
   @@map("keycloak_users")
70 72
 }
@@ -97,7 +99,7 @@ model City {
97 99
 }
98 100
 
99 101
 model Hospital {
100
-  id                   String              @id @default(uuid())
102
+  id                   String               @id @default(uuid())
101 103
   name                 String
102 104
   hospital_code        String?
103 105
   type                 String?
@@ -114,16 +116,17 @@ model Hospital {
114 116
   created_by           String
115 117
   latitude             Float?
116 118
   longitude            Float?
117
-  gmaps_url            String?             @db.Text
118
-  createdAt            DateTime            @default(now())
119
-  updatedAt            DateTime            @updatedAt
119
+  gmaps_url            String?              @db.Text
120
+  createdAt            DateTime             @default(now())
121
+  updatedAt            DateTime             @updatedAt
120 122
   deletedAt            DateTime?
121
-  province             Province            @relation(fields: [province_id], references: [id])
122
-  city                 City                @relation(fields: [city_id], references: [id])
123
-  user                 UserKeycloak        @relation(fields: [created_by], references: [id])
123
+  province             Province             @relation(fields: [province_id], references: [id])
124
+  city                 City                 @relation(fields: [city_id], references: [id])
125
+  user                 UserKeycloak         @relation(fields: [created_by], references: [id])
124 126
   vendor_experiences   VendorExperience[]
125 127
   executives_histories ExecutivesHistory[]
126 128
   status_histories     StatusHistory[]
129
+  ScheduleVisitation   ScheduleVisitation[]
127 130
 
128 131
   @@map("hospitals")
129 132
 }
@@ -176,7 +179,7 @@ model UserArea {
176 179
 // }
177 180
 
178 181
 model VendorExperience {
179
-  id                    String         @id @default(uuid())
182
+  id                    String    @id @default(uuid())
180 183
   hospital_id           String
181 184
   vendor_id             String?
182 185
   status                String?
@@ -184,13 +187,13 @@ model VendorExperience {
184 187
   contract_expired_date DateTime?
185 188
   contract_value_min    BigInt?
186 189
   contract_value_max    BigInt?
187
-  positive_notes        String?        @db.Text
188
-  negative_notes        String?        @db.Text
190
+  positive_notes        String?   @db.Text
191
+  negative_notes        String?   @db.Text
189 192
   simrs_type            String
190
-  hospital              Hospital       @relation(fields: [hospital_id], references: [id])
191
-  vendor                Vendor?        @relation(fields: [vendor_id], references: [id])
192
-  createdAt             DateTime       @default(now())
193
-  updatedAt             DateTime       @updatedAt
193
+  hospital              Hospital  @relation(fields: [hospital_id], references: [id])
194
+  vendor                Vendor?   @relation(fields: [vendor_id], references: [id])
195
+  createdAt             DateTime  @default(now())
196
+  updatedAt             DateTime  @updatedAt
194 197
   deletedAt             DateTime?
195 198
 
196 199
   @@map("vendor_experiences")
@@ -213,17 +216,17 @@ model ExecutivesHistory {
213 216
 }
214 217
 
215 218
 model StatusHistory {
216
-  id           String         @id @default(uuid())
217
-  hospital_id  String
218
-  user_id      String
219
-  old_status   ProgressStatus
220
-  new_status   ProgressStatus
221
-  note         String?        @db.Text
222
-  hospital     Hospital       @relation(fields: [hospital_id], references: [id])
223
-  user         UserKeycloak   @relation(fields: [user_id], references: [id])
224
-  createdAt    DateTime       @default(now())
225
-  updatedAt    DateTime       @updatedAt
226
-  deletedAt    DateTime?
219
+  id          String         @id @default(uuid())
220
+  hospital_id String
221
+  user_id     String
222
+  old_status  ProgressStatus
223
+  new_status  ProgressStatus
224
+  note        String?        @db.Text
225
+  hospital    Hospital       @relation(fields: [hospital_id], references: [id])
226
+  user        UserKeycloak   @relation(fields: [user_id], references: [id])
227
+  createdAt   DateTime       @default(now())
228
+  updatedAt   DateTime       @updatedAt
229
+  deletedAt   DateTime?
227 230
 
228 231
   @@map("status_histories")
229 232
 }
@@ -241,14 +244,33 @@ model Category {
241 244
 }
242 245
 
243 246
 model CategoryLink {
244
-  id               String            @id @default(uuid())
245
-  category_id      String
246
-  source_type      String?
247
-  source_id        String?
248
-  createdAt        DateTime          @default(now())
249
-  updatedAt        DateTime          @updatedAt
250
-  deletedAt        DateTime?
251
-  Category         Category?         @relation(fields: [category_id], references: [id])
247
+  id          String    @id @default(uuid())
248
+  category_id String
249
+  source_type String?
250
+  source_id   String?
251
+  createdAt   DateTime  @default(now())
252
+  updatedAt   DateTime  @updatedAt
253
+  deletedAt   DateTime?
254
+  Category    Category? @relation(fields: [category_id], references: [id])
252 255
 
253 256
   @@map("category_links")
254 257
 }
258
+
259
+model ScheduleVisitation {
260
+  id           String       @id @default(uuid())
261
+  date_visit   DateTime
262
+  sales_id     String
263
+  hospital_id  String
264
+  province_id  String
265
+  city_id      String
266
+  notes        String?      @db.Text
267
+  created_by   String
268
+  createdAt    DateTime     @default(now())
269
+  updatedAt    DateTime     @updatedAt
270
+  deletedAt    DateTime?
271
+  hospital     Hospital     @relation(fields: [hospital_id], references: [id])
272
+  user_created UserKeycloak @relation("CreatedVisitations", fields: [created_by], references: [id])
273
+  sales_visit  UserKeycloak @relation("SalesVisitations", fields: [sales_id], references: [id])
274
+
275
+  @@map("schedule_visitations")
276
+}

+ 16 - 0
src/controllers/admin/SalesController.ts

@@ -0,0 +1,16 @@
1
+import { Request, Response } from 'express';
2
+import * as SalesService from '../../services/admin/SalesService';
3
+import { PaginationParam } from '../../utils/PaginationParams';
4
+import { errorResponse } from '../../utils/Response';
5
+import { SalesCollection } from '../../resources/admin/sales/SalesCollection';
6
+
7
+export const getAllSales = async (req: Request, res: Response): Promise<Response> => {
8
+    try {
9
+        const { page, limit, search, sortBy, orderBy, } = PaginationParam(req);
10
+        const { sales, total } = await SalesService.getAllSalesService({ page, limit, search, sortBy, orderBy });
11
+
12
+        return SalesCollection(req, res, sales, total, page, limit, 'Sales data successfully retrieved');
13
+    } catch (err) {
14
+        return errorResponse(res, err);
15
+    }
16
+};

+ 81 - 0
src/controllers/admin/ScheduleVisitationController.ts

@@ -0,0 +1,81 @@
1
+import { Request, Response } from 'express';
2
+import * as ScheduleVisitationService from '../../services/admin/ScheduleVisitationService';
3
+import { PaginationParam } from '../../utils/PaginationParams';
4
+import { errorResponse, messageSuccessResponse } from '../../utils/Response';
5
+import { CustomRequest } from '../../types/token/CustomRequest';
6
+import { validateStoreScheduleVisitationRequest, validateUpdateScheduleVisitationRequest } from '../../validators/admin/schedule_visitation/ScheduleVisitationValidators';
7
+import { ScheduleVisitationCollection } from '../../resources/admin/schedule_visitation/ScheduleVisitationCollection';
8
+import { ScheduleVisitationResource } from '../../resources/admin/schedule_visitation/ScheduleVisitationResource';
9
+
10
+export const getAllScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
11
+    try {
12
+        const {
13
+            page,
14
+            limit,
15
+            search,
16
+            sortBy,
17
+            orderBy,
18
+            province,
19
+            city,
20
+            date_visit,
21
+            sales
22
+        } = PaginationParam(req, ['province', 'city', 'date_visit', 'sales']);
23
+
24
+        const { scheduleVisitations, total } = await ScheduleVisitationService.getAllScheduleVisitationService({
25
+            page,
26
+            limit,
27
+            search,
28
+            sortBy,
29
+            orderBy,
30
+            province,
31
+            city,
32
+            date_visit,
33
+            sales,
34
+        });
35
+
36
+        return ScheduleVisitationCollection(req, res, scheduleVisitations, total, page, limit, 'Schedule Visitation data successfully retrieved');
37
+    } catch (err) {
38
+        return errorResponse(res, err);
39
+    }
40
+};
41
+
42
+export const storeScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
43
+    try {
44
+        const validatedData = validateStoreScheduleVisitationRequest(req.body);
45
+        await ScheduleVisitationService.storeScheduleVisitationService(validatedData, req as CustomRequest);
46
+        return messageSuccessResponse(res, 'Success added schedule visitation', 201);
47
+    } catch (err) {
48
+        return errorResponse(res, err);
49
+    }
50
+};
51
+
52
+export const showScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
53
+    try {
54
+        const id = req.params.id;
55
+        const data = await ScheduleVisitationService.showScheduleVisitationService(id);
56
+        return ScheduleVisitationResource(res, data, 'Success show schedule visitation');
57
+    } catch (err) {
58
+        return errorResponse(res, err);
59
+    }
60
+};
61
+
62
+export const updateScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
63
+    try {
64
+        const id = req.params.id;
65
+        const validatedData = validateUpdateScheduleVisitationRequest(req.body);
66
+        await ScheduleVisitationService.updateScheduleVisitationService(validatedData, id, req as CustomRequest);
67
+        return messageSuccessResponse(res, 'Success update schedule visitation');
68
+    } catch (err) {
69
+        return errorResponse(res, err);
70
+    }
71
+};
72
+
73
+export const deleteScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
74
+    try {
75
+        const id = req.params.id;
76
+        await ScheduleVisitationService.deleteScheduleVisitationService(id, req as CustomRequest);
77
+        return messageSuccessResponse(res, 'Success delete hospital');
78
+    } catch (err) {
79
+        return errorResponse(res, err);
80
+    }
81
+};

+ 69 - 0
src/controllers/sales/ScheduleVisitationController.ts

@@ -0,0 +1,69 @@
1
+import { Request, Response } from 'express';
2
+import * as ScheduleVisitationService from '../../services/sales/ScheduleVisitationService';
3
+import { PaginationParam } from '../../utils/PaginationParams';
4
+import { errorResponse, messageSuccessResponse } from '../../utils/Response';
5
+import { CustomRequest } from '../../types/token/CustomRequest';
6
+import { validateStoreScheduleVisitationRequest, validateUpdateScheduleVisitationRequest } from '../../validators/sales/schedule_visitation/ScheduleVisitationValidators';
7
+import { ScheduleVisitationCollection } from '../../resources/sales/schedule_visitation/ScheduleVisitationCollection';
8
+import { ScheduleVisitationResource } from '../../resources/sales/schedule_visitation/ScheduleVisitationResource';
9
+
10
+export const getAllScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
11
+    try {
12
+        const {
13
+            page,
14
+            limit,
15
+            search,
16
+            sortBy,
17
+            orderBy,
18
+            province,
19
+            city,
20
+            date_visit,
21
+        } = PaginationParam(req, ['province', 'city', 'date_visit']);
22
+
23
+        const { scheduleVisitations, total } = await ScheduleVisitationService.getAllScheduleVisitationService(req as CustomRequest, {
24
+            page,
25
+            limit,
26
+            search,
27
+            sortBy,
28
+            orderBy,
29
+            province,
30
+            city,
31
+            date_visit,
32
+        });
33
+
34
+        return ScheduleVisitationCollection(req, res, scheduleVisitations, total, page, limit, 'Schedule Visitation data successfully retrieved');
35
+    } catch (err) {
36
+        return errorResponse(res, err);
37
+    }
38
+};
39
+
40
+export const storeScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
41
+    try {
42
+        const validatedData = validateStoreScheduleVisitationRequest(req.body);
43
+        await ScheduleVisitationService.storeScheduleVisitationService(validatedData, req as CustomRequest);
44
+        return messageSuccessResponse(res, 'Success added schedule visitation', 201);
45
+    } catch (err) {
46
+        return errorResponse(res, err);
47
+    }
48
+};
49
+
50
+export const showScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
51
+    try {
52
+        const id = req.params.id;
53
+        const data = await ScheduleVisitationService.showScheduleVisitationService(req as CustomRequest, id);
54
+        return ScheduleVisitationResource(res, data, 'Success show schedule visitation');
55
+    } catch (err) {
56
+        return errorResponse(res, err);
57
+    }
58
+};
59
+
60
+export const updateScheduleVisitation = async (req: Request, res: Response): Promise<Response> => {
61
+    try {
62
+        const id = req.params.id;
63
+        const validatedData = validateUpdateScheduleVisitationRequest(req.body);
64
+        await ScheduleVisitationService.updateScheduleVisitationService(validatedData, id, req as CustomRequest);
65
+        return messageSuccessResponse(res, 'Success update schedule visitation');
66
+    } catch (err) {
67
+        return errorResponse(res, err);
68
+    }
69
+};

+ 43 - 0
src/repository/admin/SalesRepository.ts

@@ -0,0 +1,43 @@
1
+import prisma from '../../prisma/PrismaClient';
2
+import { Prisma } from '@prisma/client';
3
+
4
+interface FindAllParams {
5
+    skip?: number;
6
+    take?: number;
7
+    where?: Prisma.UserWhereInput;
8
+    orderBy?: Prisma.UserOrderByWithRelationInput;
9
+}
10
+
11
+const SalesRepository = {
12
+    findAll: async ({ skip, take, where = {}, orderBy }: FindAllParams) => {
13
+        return prisma.user.findMany({
14
+            where: {
15
+                ...where,
16
+                role: 'sales',
17
+            },
18
+            skip,
19
+            take,
20
+            orderBy,
21
+            select: {
22
+                id: true,
23
+                email: true,
24
+                firstname: true,
25
+                lastname: true,
26
+                role: true,
27
+                createdAt: true,
28
+                updatedAt: true,
29
+            },
30
+        });
31
+    },
32
+
33
+    countAll: async (where: Prisma.UserWhereInput = {}): Promise<number> => {
34
+        return prisma.user.count({
35
+            where: {
36
+                ...where,
37
+                role: 'sales',
38
+            }
39
+        });
40
+    },
41
+};
42
+
43
+export default SalesRepository;

+ 139 - 0
src/repository/admin/ScheduleVisitationRepository.ts

@@ -0,0 +1,139 @@
1
+import prisma from '../../prisma/PrismaClient';
2
+import { Prisma } from '@prisma/client';
3
+import { ScheduleVisitationDTO } from '../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4
+
5
+interface FindAllParams {
6
+    skip: number;
7
+    take: number;
8
+    where?: Prisma.ScheduleVisitationWhereInput;
9
+    orderBy?: Prisma.ScheduleVisitationOrderByWithRelationInput;
10
+}
11
+
12
+const ScheduleVisitationRepository = {
13
+    findAll: async ({ skip, take, where, orderBy }: FindAllParams) => {
14
+        return prisma.scheduleVisitation.findMany({
15
+            where,
16
+            skip,
17
+            take,
18
+            orderBy,
19
+            include: {
20
+                hospital: {
21
+                    select: {
22
+                        id: true,
23
+                        name: true,
24
+                        hospital_code: true,
25
+                        type: true,
26
+                        ownership: true,
27
+                        address: true,
28
+                        contact: true,
29
+                        email: true,
30
+                        image: true,
31
+                        progress_status: true,
32
+                        note: true,
33
+                        latitude: true,
34
+                        longitude: true,
35
+                        gmaps_url: true,
36
+                        province: {
37
+                            select: {
38
+                                id: true,
39
+                                name: true,
40
+                            },
41
+                        },
42
+                        city: {
43
+                            select: {
44
+                                id: true,
45
+                                name: true,
46
+                            },
47
+                        },
48
+                    }
49
+                },
50
+                user_created: { select: { id: true, fullname: true } },
51
+                sales_visit: { select: { id: true, fullname: true } },
52
+            },
53
+        });
54
+    },
55
+
56
+    countAll: async (where?: Prisma.ScheduleVisitationWhereInput) => {
57
+        return prisma.scheduleVisitation.count({ where });
58
+    },
59
+
60
+    findById: async (id: string) => {
61
+        return await prisma.scheduleVisitation.findFirst({
62
+            where: { id, deletedAt: null },
63
+            select: {
64
+                id: true,
65
+                date_visit: true,
66
+                notes: true,
67
+                hospital: {
68
+                    select: {
69
+                        id: true,
70
+                        name: true,
71
+                        hospital_code: true,
72
+                        type: true,
73
+                        ownership: true,
74
+                        address: true,
75
+                        contact: true,
76
+                        email: true,
77
+                        image: true,
78
+                        progress_status: true,
79
+                        note: true,
80
+                        latitude: true,
81
+                        longitude: true,
82
+                        gmaps_url: true,
83
+                        province: {
84
+                            select: {
85
+                                id: true,
86
+                                name: true,
87
+                            },
88
+                        },
89
+                        city: {
90
+                            select: {
91
+                                id: true,
92
+                                name: true,
93
+                            },
94
+                        },
95
+                        vendor_experiences: {
96
+                            where: { status: "active", deletedAt: null },
97
+                            select: {
98
+                                id: true,
99
+                                vendor: {
100
+                                    select: { id: true, name: true }
101
+                                },
102
+                                contract_start_date: true,
103
+                                contract_expired_date: true,
104
+                                simrs_type: true,
105
+                            }
106
+                        },
107
+                        executives_histories: {
108
+                            where: { status: "active", deletedAt: null },
109
+                            select: {
110
+                                id: true,
111
+                                executive_name: true,
112
+                                contact: true,
113
+                                start_term: true,
114
+                                end_term: true,
115
+                            }
116
+                        }
117
+                    },
118
+                },
119
+                user_created: { select: { id: true, fullname: true } },
120
+                sales_visit: { select: { id: true, fullname: true } },
121
+                createdAt: true,
122
+                updatedAt: true,
123
+            },
124
+        });
125
+    },
126
+
127
+    create: async (data: Prisma.ScheduleVisitationCreateInput) => {
128
+        return prisma.scheduleVisitation.create({ data });
129
+    },
130
+
131
+    update: async (id: string, data: Prisma.ScheduleVisitationUpdateInput) => {
132
+        return prisma.scheduleVisitation.update({
133
+            where: { id },
134
+            data,
135
+        });
136
+    },
137
+};
138
+
139
+export default ScheduleVisitationRepository;

+ 139 - 0
src/repository/sales/ScheduleVisitationRepository.ts

@@ -0,0 +1,139 @@
1
+import prisma from '../../prisma/PrismaClient';
2
+import { Prisma } from '@prisma/client';
3
+import { ScheduleVisitationDTO } from '../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4
+
5
+interface FindAllParams {
6
+    skip: number;
7
+    take: number;
8
+    where?: Prisma.ScheduleVisitationWhereInput;
9
+    orderBy?: Prisma.ScheduleVisitationOrderByWithRelationInput;
10
+}
11
+
12
+const ScheduleVisitationRepository = {
13
+    findAll: async ({ skip, take, where, orderBy }: FindAllParams) => {
14
+        return prisma.scheduleVisitation.findMany({
15
+            where,
16
+            skip,
17
+            take,
18
+            orderBy,
19
+            include: {
20
+                hospital: {
21
+                    select: {
22
+                        id: true,
23
+                        name: true,
24
+                        hospital_code: true,
25
+                        type: true,
26
+                        ownership: true,
27
+                        address: true,
28
+                        contact: true,
29
+                        email: true,
30
+                        image: true,
31
+                        progress_status: true,
32
+                        note: true,
33
+                        latitude: true,
34
+                        longitude: true,
35
+                        gmaps_url: true,
36
+                        province: {
37
+                            select: {
38
+                                id: true,
39
+                                name: true,
40
+                            },
41
+                        },
42
+                        city: {
43
+                            select: {
44
+                                id: true,
45
+                                name: true,
46
+                            },
47
+                        },
48
+                    }
49
+                },
50
+                user_created: { select: { id: true, fullname: true } },
51
+                sales_visit: { select: { id: true, fullname: true } },
52
+            },
53
+        });
54
+    },
55
+
56
+    countAll: async (where?: Prisma.ScheduleVisitationWhereInput) => {
57
+        return prisma.scheduleVisitation.count({ where });
58
+    },
59
+
60
+    findById: async (id: string) => {
61
+        return await prisma.scheduleVisitation.findFirst({
62
+            where: { id, deletedAt: null },
63
+            select: {
64
+                id: true,
65
+                date_visit: true,
66
+                notes: true,
67
+                hospital: {
68
+                    select: {
69
+                        id: true,
70
+                        name: true,
71
+                        hospital_code: true,
72
+                        type: true,
73
+                        ownership: true,
74
+                        address: true,
75
+                        contact: true,
76
+                        email: true,
77
+                        image: true,
78
+                        progress_status: true,
79
+                        note: true,
80
+                        latitude: true,
81
+                        longitude: true,
82
+                        gmaps_url: true,
83
+                        province: {
84
+                            select: {
85
+                                id: true,
86
+                                name: true,
87
+                            },
88
+                        },
89
+                        city: {
90
+                            select: {
91
+                                id: true,
92
+                                name: true,
93
+                            },
94
+                        },
95
+                        vendor_experiences: {
96
+                            where: { status: "active", deletedAt: null },
97
+                            select: {
98
+                                id: true,
99
+                                vendor: {
100
+                                    select: { id: true, name: true }
101
+                                },
102
+                                contract_start_date: true,
103
+                                contract_expired_date: true,
104
+                                simrs_type: true,
105
+                            }
106
+                        },
107
+                        executives_histories: {
108
+                            where: { status: "active", deletedAt: null },
109
+                            select: {
110
+                                id: true,
111
+                                executive_name: true,
112
+                                contact: true,
113
+                                start_term: true,
114
+                                end_term: true,
115
+                            }
116
+                        }
117
+                    },
118
+                },
119
+                user_created: { select: { id: true, fullname: true } },
120
+                sales_visit: { select: { id: true, fullname: true } },
121
+                createdAt: true,
122
+                updatedAt: true,
123
+            },
124
+        });
125
+    },
126
+
127
+    create: async (data: Prisma.ScheduleVisitationCreateInput) => {
128
+        return prisma.scheduleVisitation.create({ data });
129
+    },
130
+
131
+    update: async (id: string, data: Prisma.ScheduleVisitationUpdateInput) => {
132
+        return prisma.scheduleVisitation.update({
133
+            where: { id },
134
+            data,
135
+        });
136
+    },
137
+};
138
+
139
+export default ScheduleVisitationRepository;

+ 56 - 0
src/resources/admin/sales/SalesCollection.ts

@@ -0,0 +1,56 @@
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import { SalesDTO } from '../../../types/admin/sales/SalesDTO';
5
+
6
+const formatItem = (item: SalesDTO) => ({
7
+    ...item,
8
+    createdAt: formatISOWithoutTimezone(item.createdAt),
9
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
10
+});
11
+
12
+export const SalesCollection = (req: Request, res: Response, data: SalesDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
13
+    const formattedData = data.map(formatItem);
14
+
15
+    if (typeof total !== 'number') {
16
+        return res.status(200).json({
17
+            success: true,
18
+            message,
19
+            data: Array.isArray(formattedData),
20
+        });
21
+    }
22
+
23
+    return ListResponse({
24
+        req,
25
+        res,
26
+        data: formattedData,
27
+        total,
28
+        page,
29
+        limit,
30
+        message,
31
+    });
32
+};
33
+
34
+
35
+// const { ListResponse } = require("../../../utils/ListResponse");
36
+// const { formatISOWithoutTimezone } = require("../../../utils/FormatDate.js");
37
+
38
+// const formatItem = (item) => ({
39
+//     ...item,
40
+//     createdAt: formatISOWithoutTimezone(item.createdAt),
41
+//     updatedAt: formatISOWithoutTimezone(item.updatedAt)
42
+// });
43
+
44
+// exports.CityCollection = (req, res, data = [], total = null, page = 1, limit = 10, message = 'Success') => {
45
+//     const formattedData = data.map(formatItem);
46
+
47
+//     if (typeof total !== 'number') {
48
+//         return res.status(200).json({
49
+//             success: true,
50
+//             message,
51
+//             data: Array.isArray(formattedData)
52
+//         });
53
+//     }
54
+
55
+//     return ListResponse({ req, res, data: formattedData, total, page, limit, message });
56
+// };

+ 45 - 0
src/resources/admin/schedule_visitation/ScheduleVisitationCollection.ts

@@ -0,0 +1,45 @@
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { ScheduleVisitationDTO } from '../../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4
+import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
5
+
6
+const formatItem = (item: ScheduleVisitationDTO) => ({
7
+    id: item.id,
8
+    date_visit: formatDateOnly(item.date_visit),
9
+    hospital: item.hospital,
10
+    user_created: item.user_created,
11
+    sales_visit: item.sales_visit,
12
+    notes: item.notes ?? null,
13
+    createdAt: formatISOWithoutTimezone(item.createdAt),
14
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
15
+});
16
+
17
+export const ScheduleVisitationCollection = async (
18
+    req: Request,
19
+    res: Response,
20
+    data: ScheduleVisitationDTO[] = [],
21
+    total: number | null = null,
22
+    page: number = 1,
23
+    limit: number = 10,
24
+    message: string = 'Success'
25
+): Promise<Response> => {
26
+    const formattedData = data.map(formatItem);
27
+
28
+    if (typeof total !== 'number') {
29
+        return res.status(200).json({
30
+            success: true,
31
+            message,
32
+            data: Array.isArray(formattedData),
33
+        });
34
+    }
35
+
36
+    return ListResponse({
37
+        req,
38
+        res,
39
+        data: formattedData,
40
+        total,
41
+        page,
42
+        limit,
43
+        message,
44
+    });
45
+};

+ 40 - 0
src/resources/admin/schedule_visitation/ScheduleVisitationResource.ts

@@ -0,0 +1,40 @@
1
+// ScheduleVisitationResource.ts
2
+import { Response } from 'express';
3
+import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import { ShowScheduleVisitationDTO } from '../../../types/admin/schedule_visitation/ScheduleVisitationDTO';
5
+
6
+const formatItem = (item: ShowScheduleVisitationDTO) => ({
7
+    ...item,
8
+    date_visit: formatDateOnly(item.date_visit),
9
+    hospital: {
10
+        ...item.hospital,
11
+        vendor_experiences: item.hospital.vendor_experiences.map((vendor) => ({
12
+            ...vendor,
13
+            contract_start_date: formatDateOnly(vendor.contract_start_date),
14
+            contract_expired_date: formatDateOnly(vendor.contract_expired_date),
15
+        })),
16
+        executives_histories: item.hospital.executives_histories.map((exec) => ({
17
+            ...exec,
18
+            start_term: formatDateOnly(exec.start_term),
19
+            end_term: formatDateOnly(exec.end_term),
20
+        })),
21
+    },
22
+    createdAt: formatISOWithoutTimezone(item.createdAt),
23
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
24
+});
25
+
26
+export const ScheduleVisitationResource = (
27
+    res: Response,
28
+    data: ShowScheduleVisitationDTO | ShowScheduleVisitationDTO[],
29
+    message: string = 'Success'
30
+): Response => {
31
+    const formattedData = Array.isArray(data)
32
+        ? data.map(formatItem)
33
+        : formatItem(data);
34
+
35
+    return res.status(200).json({
36
+        success: true,
37
+        message,
38
+        data: formattedData,
39
+    });
40
+};

+ 45 - 0
src/resources/sales/schedule_visitation/ScheduleVisitationCollection.ts

@@ -0,0 +1,45 @@
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { ScheduleVisitationDTO } from '../../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4
+import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
5
+
6
+const formatItem = (item: ScheduleVisitationDTO) => ({
7
+    id: item.id,
8
+    date_visit: formatDateOnly(item.date_visit),
9
+    hospital: item.hospital,
10
+    user_created: item.user_created,
11
+    sales_visit: item.sales_visit,
12
+    notes: item.notes ?? null,
13
+    createdAt: formatISOWithoutTimezone(item.createdAt),
14
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
15
+});
16
+
17
+export const ScheduleVisitationCollection = async (
18
+    req: Request,
19
+    res: Response,
20
+    data: ScheduleVisitationDTO[] = [],
21
+    total: number | null = null,
22
+    page: number = 1,
23
+    limit: number = 10,
24
+    message: string = 'Success'
25
+): Promise<Response> => {
26
+    const formattedData = data.map(formatItem);
27
+
28
+    if (typeof total !== 'number') {
29
+        return res.status(200).json({
30
+            success: true,
31
+            message,
32
+            data: Array.isArray(formattedData),
33
+        });
34
+    }
35
+
36
+    return ListResponse({
37
+        req,
38
+        res,
39
+        data: formattedData,
40
+        total,
41
+        page,
42
+        limit,
43
+        message,
44
+    });
45
+};

+ 40 - 0
src/resources/sales/schedule_visitation/ScheduleVisitationResource.ts

@@ -0,0 +1,40 @@
1
+// ScheduleVisitationResource.ts
2
+import { Response } from 'express';
3
+import { formatDateOnly, formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import { ShowScheduleVisitationDTO } from '../../../types/sales/schedule_visitation/ScheduleVisitationDTO';
5
+
6
+const formatItem = (item: ShowScheduleVisitationDTO) => ({
7
+    ...item,
8
+    date_visit: formatDateOnly(item.date_visit),
9
+    hospital: {
10
+        ...item.hospital,
11
+        vendor_experiences: item.hospital.vendor_experiences.map((vendor) => ({
12
+            ...vendor,
13
+            contract_start_date: formatDateOnly(vendor.contract_start_date),
14
+            contract_expired_date: formatDateOnly(vendor.contract_expired_date),
15
+        })),
16
+        executives_histories: item.hospital.executives_histories.map((exec) => ({
17
+            ...exec,
18
+            start_term: formatDateOnly(exec.start_term),
19
+            end_term: formatDateOnly(exec.end_term),
20
+        })),
21
+    },
22
+    createdAt: formatISOWithoutTimezone(item.createdAt),
23
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
24
+});
25
+
26
+export const ScheduleVisitationResource = (
27
+    res: Response,
28
+    data: ShowScheduleVisitationDTO | ShowScheduleVisitationDTO[],
29
+    message: string = 'Success'
30
+): Response => {
31
+    const formattedData = Array.isArray(data)
32
+        ? data.map(formatItem)
33
+        : formatItem(data);
34
+
35
+    return res.status(200).json({
36
+        success: true,
37
+        message,
38
+        data: formattedData,
39
+    });
40
+};

+ 11 - 0
src/routes/admin/SalesRoute.ts

@@ -0,0 +1,11 @@
1
+import express, { Router } from 'express';
2
+import * as SalesController from '../../controllers/admin/SalesController';
3
+import keycloak from '../../middleware/Keycloak';
4
+import { extractToken } from '../../middleware/ExtractToken';
5
+import checkRoles from '../../middleware/CheckRoles';
6
+
7
+const router: Router = express.Router();
8
+
9
+router.get('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], SalesController.getAllSales);
10
+
11
+export default router;

+ 15 - 0
src/routes/admin/ScheduleVisitationRoute.ts

@@ -0,0 +1,15 @@
1
+import express, { Router } from 'express';
2
+import * as ScheduleVisitationController from '../../controllers/admin/ScheduleVisitationController';
3
+import keycloak from '../../middleware/Keycloak';
4
+import { extractToken } from '../../middleware/ExtractToken';
5
+import checkRoles from '../../middleware/CheckRoles';
6
+
7
+const router: Router = express.Router();
8
+
9
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], ScheduleVisitationController.getAllScheduleVisitation);
10
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['admin'])], ScheduleVisitationController.storeScheduleVisitation);
11
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], ScheduleVisitationController.showScheduleVisitation);
12
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], ScheduleVisitationController.updateScheduleVisitation);
13
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(['admin'])], ScheduleVisitationController.deleteScheduleVisitation);
14
+
15
+export default router;

+ 14 - 0
src/routes/sales/ScheduleVisitationRoute.ts

@@ -0,0 +1,14 @@
1
+import express, { Router } from 'express';
2
+import * as ScheduleVisitationController from '../../controllers/sales/ScheduleVisitationController';
3
+import keycloak from '../../middleware/Keycloak';
4
+import { extractToken } from '../../middleware/ExtractToken';
5
+import checkRoles from '../../middleware/CheckRoles';
6
+
7
+const router: Router = express.Router();
8
+
9
+router.get('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], ScheduleVisitationController.getAllScheduleVisitation);
10
+router.post('/', [keycloak.protect(), extractToken, checkRoles(['sales'])], ScheduleVisitationController.storeScheduleVisitation);
11
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], ScheduleVisitationController.showScheduleVisitation);
12
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(['sales'])], ScheduleVisitationController.updateScheduleVisitation);
13
+
14
+export default router;

+ 32 - 0
src/services/admin/SalesService.ts

@@ -0,0 +1,32 @@
1
+import { SearchFilter } from '../../utils/SearchFilter';
2
+import { Prisma } from '@prisma/client';
3
+import SalesRepository from '../../repository/admin/SalesRepository';
4
+
5
+interface GetAllSalesParams {
6
+    page: number;
7
+    limit: number;
8
+    search?: string;
9
+    sortBy: string;
10
+    orderBy: 'asc' | 'desc';
11
+}
12
+
13
+export const getAllSalesService = async ({ page, limit, search = '', sortBy, orderBy }: GetAllSalesParams) => {
14
+    const skip = (page - 1) * limit;
15
+
16
+    const where: Prisma.UserWhereInput = {
17
+        ...SearchFilter(search, ['id', 'firstname', 'lastname']),
18
+        deletedAt: null,
19
+    };
20
+
21
+    const [sales, total] = await Promise.all([
22
+        SalesRepository.findAll({
23
+            skip,
24
+            take: limit,
25
+            where,
26
+            orderBy: { [sortBy]: orderBy },
27
+        }),
28
+        SalesRepository.countAll(where),
29
+    ]);
30
+
31
+    return { sales, total };
32
+};

+ 188 - 0
src/services/admin/ScheduleVisitationService.ts

@@ -0,0 +1,188 @@
1
+import ScheduleVisitationRepository from '../../repository/admin/ScheduleVisitationRepository';
2
+import { HttpException } from '../../utils/HttpException';
3
+import { SearchFilter } from '../../utils/SearchFilter';
4
+import { now } from '../../utils/TimeLocal';
5
+import { createLog, updateLog, deleteLog } from '../../utils/LogActivity';
6
+import prisma from '../../prisma/PrismaClient';
7
+import { Prisma } from '@prisma/client';
8
+import { CustomRequest } from '../../types/token/CustomRequest';
9
+import { ScheduleVisitationRequestDTO } from '../../types/admin/schedule_visitation/ScheduleVisitationDTO';
10
+import HospitalRepository from '../../repository/admin/HospitalRepository';
11
+import { startOfMonth, endOfMonth, startOfDay, endOfDay } from "date-fns";
12
+
13
+interface PaginationParams {
14
+    page: number;
15
+    limit: number;
16
+    search?: string;
17
+    sortBy: string;
18
+    orderBy: 'asc' | 'desc';
19
+    province?: string;
20
+    city?: string;
21
+    date_visit?: Date;
22
+    hospital?: string;
23
+    sales?: string;
24
+}
25
+
26
+export const getAllScheduleVisitationService = async ({ page, limit, search, sortBy, orderBy, province, city, date_visit, hospital, sales }: PaginationParams) => {
27
+    const skip = (page - 1) * limit;
28
+    let dateFilter: Prisma.ScheduleVisitationWhereInput = {};
29
+
30
+    let dateStr: string | null = null;
31
+
32
+    if (date_visit) {
33
+        // Accept both Date and string
34
+        dateStr = typeof date_visit === "string"
35
+            ? date_visit
36
+            : date_visit.toISOString().slice(0, 10);
37
+    } else {
38
+        const today = new Date();
39
+        dateStr = today.toISOString().slice(0, 7); // "YYYY-MM"
40
+    }
41
+
42
+    // Apply filter
43
+    if (/^\d{4}-\d{2}$/.test(dateStr)) {
44
+        // Format: YYYY-MM (month filter)
45
+        const startDate = startOfMonth(new Date(`${dateStr}-01`));
46
+        const endDate = endOfMonth(startDate);
47
+
48
+        dateFilter = {
49
+            date_visit: {
50
+                gte: startDate,
51
+                lte: endDate,
52
+            },
53
+        };
54
+    } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
55
+        // Format: YYYY-MM-DD (day filter)
56
+        const startDate = startOfDay(new Date(dateStr));
57
+        const endDate = endOfDay(startDate);
58
+
59
+        dateFilter = {
60
+            date_visit: {
61
+                gte: startDate,
62
+                lte: endDate,
63
+            },
64
+        };
65
+    }
66
+
67
+    const where: Prisma.ScheduleVisitationWhereInput = {
68
+        ...SearchFilter(search, ['hospital.name']),
69
+        ...(province ? { province_id: province } : {}),
70
+        ...(city ? { city_id: city } : {}),
71
+        ...(sales ? { sales_visit: { id: sales } } : {}),
72
+        ...dateFilter,
73
+        deletedAt: null,
74
+    };
75
+
76
+    const [scheduleVisitations, total] = await Promise.all([
77
+        ScheduleVisitationRepository.findAll({
78
+            skip,
79
+            take: limit,
80
+            where,
81
+            orderBy: { [sortBy]: orderBy },
82
+        }),
83
+        ScheduleVisitationRepository.countAll(where),
84
+    ]);
85
+
86
+    return { scheduleVisitations, total };
87
+};
88
+
89
+export const storeScheduleVisitationService = async (validateData: ScheduleVisitationRequestDTO, req: CustomRequest) => {
90
+    const creatorId = req.tokenData.sub;
91
+
92
+    const sales = await prisma.user.findFirst({
93
+        where: {
94
+            id: validateData.sales_id,
95
+        }
96
+    });
97
+    if (!sales) {
98
+        throw new HttpException('Sales user not found', 404);
99
+    }
100
+
101
+    const hospital = await HospitalRepository.findById(validateData.hospital_id);
102
+    if (!hospital) {
103
+        throw new HttpException('Hospital not found', 404);
104
+    }
105
+
106
+    const provinceId = hospital.province?.id;
107
+    const cityId = hospital.city?.id;
108
+
109
+    if (!provinceId || !cityId) {
110
+        throw new HttpException('Hospital does not have province or city assigned', 400);
111
+    }
112
+
113
+    const payload = {
114
+        date_visit: new Date(validateData.date_visit),
115
+        notes: validateData.notes ?? '',
116
+        province_id: provinceId,
117
+        city_id: cityId,
118
+
119
+        hospital: { connect: { id: validateData.hospital_id } },
120
+        sales_visit: { connect: { id: validateData.sales_id } },
121
+        user_created: { connect: { id: creatorId } },
122
+    };
123
+
124
+    const data = await ScheduleVisitationRepository.create(payload);
125
+
126
+    await createLog(req, data);
127
+};
128
+
129
+export const showScheduleVisitationService = async (id: string) => {
130
+    const schedule_visitation = await ScheduleVisitationRepository.findById(id);
131
+    if (!schedule_visitation) {
132
+        throw new HttpException('Data schedule visitation not found', 404);
133
+    }
134
+
135
+    return schedule_visitation;
136
+};
137
+
138
+export const updateScheduleVisitationService = async (validateData: Partial<ScheduleVisitationRequestDTO>, id: string, req: CustomRequest) => {
139
+    const visitation = await ScheduleVisitationRepository.findById(id);
140
+    if (!visitation) throw new HttpException("Schedule visitation not found", 404);
141
+
142
+    const payload: Prisma.ScheduleVisitationUpdateInput = {};
143
+
144
+    if (validateData.date_visit) {
145
+        payload.date_visit = new Date(validateData.date_visit);
146
+    }
147
+
148
+    if (typeof validateData.notes !== "undefined") {
149
+        payload.notes = validateData.notes;
150
+    }
151
+
152
+    if (validateData.sales_id) {
153
+        const sales = await prisma.user.findFirst({ where: { id: validateData.sales_id } });
154
+        if (!sales) throw new HttpException("Sales user not found", 404);
155
+
156
+        payload.sales_visit = { connect: { id: validateData.sales_id } };
157
+    }
158
+
159
+    if (validateData.hospital_id) {
160
+        const hospital = await HospitalRepository.findById(validateData.hospital_id);
161
+        if (!hospital) throw new HttpException("Hospital not found", 404);
162
+
163
+        const provinceId = hospital.province?.id;
164
+        const cityId = hospital.city?.id;
165
+        if (!provinceId || !cityId) {
166
+            throw new HttpException("Hospital does not have province or city assigned", 400);
167
+        }
168
+
169
+        payload.hospital = { connect: { id: validateData.hospital_id } };
170
+        payload.province_id = provinceId;
171
+        payload.city_id = cityId;
172
+    }
173
+
174
+    const data = await ScheduleVisitationRepository.update(id, payload);
175
+
176
+    await updateLog(req, data);
177
+};
178
+
179
+export const deleteScheduleVisitationService = async (id: string, req: CustomRequest) => {
180
+    const schedule_visitation = await ScheduleVisitationRepository.findById(id);
181
+    if (!schedule_visitation) throw new HttpException('Schedule visitation not found', 404);
182
+
183
+    const data = await ScheduleVisitationRepository.update(id, {
184
+        deletedAt: now().toDate()
185
+    });
186
+
187
+    await deleteLog(req, data);
188
+};

+ 199 - 0
src/services/sales/ScheduleVisitationService.ts

@@ -0,0 +1,199 @@
1
+import ScheduleVisitationRepository from '../../repository/sales/ScheduleVisitationRepository';
2
+import { HttpException } from '../../utils/HttpException';
3
+import { SearchFilter } from '../../utils/SearchFilter';
4
+import { now } from '../../utils/TimeLocal';
5
+import { createLog, updateLog, deleteLog } from '../../utils/LogActivity';
6
+import { Prisma } from '@prisma/client';
7
+import { CustomRequest } from '../../types/token/CustomRequest';
8
+import { ScheduleVisitationRequestDTO } from '../../types/sales/schedule_visitation/ScheduleVisitationDTO';
9
+import HospitalRepository from '../../repository/sales/HospitalRepository';
10
+import { endOfDay, endOfMonth, startOfDay, startOfMonth } from 'date-fns';
11
+
12
+interface PaginationParams {
13
+    page: number;
14
+    limit: number;
15
+    search?: string;
16
+    sortBy: string;
17
+    orderBy: 'asc' | 'desc';
18
+    province?: string;
19
+    city?: string;
20
+    date_visit?: Date;
21
+    hospital?: string;
22
+}
23
+
24
+export const getAllScheduleVisitationService = async (req: CustomRequest,
25
+    { page, limit, search, sortBy, orderBy, province, city, date_visit, hospital }: PaginationParams) => {
26
+    const creatorId = req.tokenData.sub;
27
+
28
+    const skip = (page - 1) * limit;
29
+
30
+    let dateFilter: Prisma.ScheduleVisitationWhereInput = {};
31
+
32
+    let dateStr: string | null = null;
33
+
34
+    if (date_visit) {
35
+        // Accept both Date and string
36
+        dateStr = typeof date_visit === "string"
37
+            ? date_visit
38
+            : date_visit.toISOString().slice(0, 10);
39
+    } else {
40
+        const today = new Date();
41
+        dateStr = today.toISOString().slice(0, 7); // "YYYY-MM"
42
+    }
43
+
44
+    // Apply filter
45
+    if (/^\d{4}-\d{2}$/.test(dateStr)) {
46
+        // Format: YYYY-MM (month filter)
47
+        const startDate = startOfMonth(new Date(`${dateStr}-01`));
48
+        const endDate = endOfMonth(startDate);
49
+
50
+        dateFilter = {
51
+            date_visit: {
52
+                gte: startDate,
53
+                lte: endDate,
54
+            },
55
+        };
56
+    } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
57
+        // Format: YYYY-MM-DD (day filter)
58
+        const startDate = startOfDay(new Date(dateStr));
59
+        const endDate = endOfDay(startDate);
60
+
61
+        dateFilter = {
62
+            date_visit: {
63
+                gte: startDate,
64
+                lte: endDate,
65
+            },
66
+        };
67
+    }
68
+
69
+    const where: Prisma.ScheduleVisitationWhereInput = {
70
+        sales_id: creatorId,
71
+        ...SearchFilter(search, [
72
+            'hospital.name',
73
+        ]),
74
+        ...(province ? { province_id: province } : {}),
75
+        ...(city ? { city_id: city } : {}),
76
+        // ...(date_visit ? { date_visit: new Date(date_visit) } : {}),
77
+        ...dateFilter,
78
+        deletedAt: null,
79
+    };
80
+
81
+    const [scheduleVisitations, total] = await Promise.all([
82
+        ScheduleVisitationRepository.findAll({
83
+            skip,
84
+            take: limit,
85
+            where,
86
+            orderBy: { [sortBy]: orderBy },
87
+        }),
88
+        ScheduleVisitationRepository.countAll(where),
89
+    ]);
90
+
91
+    return { scheduleVisitations, total };
92
+};
93
+
94
+export const storeScheduleVisitationService = async (validateData: ScheduleVisitationRequestDTO, req: CustomRequest) => {
95
+    const userLogin = req.tokenData.sub;
96
+
97
+    // const sales = await prisma.user.findFirst({
98
+    //     where: {
99
+    //         id: validateData.sales_id,
100
+    //     }
101
+    // });
102
+    // if (!sales) {
103
+    //     throw new HttpException('Sales user not found', 404);
104
+    // }
105
+
106
+    const hospital = await HospitalRepository.findById(validateData.hospital_id);
107
+    if (!hospital) {
108
+        throw new HttpException('Hospital not found', 404);
109
+    }
110
+
111
+    const provinceId = hospital.province?.id;
112
+    const cityId = hospital.city?.id;
113
+
114
+    if (!provinceId || !cityId) {
115
+        throw new HttpException('Hospital does not have province or city assigned', 400);
116
+    }
117
+
118
+    const payload = {
119
+        date_visit: new Date(validateData.date_visit),
120
+        notes: validateData.notes ?? '',
121
+        province_id: provinceId,
122
+        city_id: cityId,
123
+
124
+        hospital: { connect: { id: validateData.hospital_id } },
125
+        sales_visit: { connect: { id: userLogin } },
126
+        user_created: { connect: { id: userLogin } },
127
+    };
128
+
129
+    const data = await ScheduleVisitationRepository.create(payload);
130
+
131
+    await createLog(req, data);
132
+};
133
+
134
+export const showScheduleVisitationService = async (req: CustomRequest, id: string) => {
135
+    const userLogin = req.tokenData.sub;
136
+
137
+    const schedule_visitation = await ScheduleVisitationRepository.findById(id);
138
+    if (!schedule_visitation) {
139
+        throw new HttpException('Data schedule visitation not found', 404);
140
+    }
141
+
142
+    if (schedule_visitation.sales_visit.id !== userLogin) {
143
+        throw new HttpException('Unauthorized access to this schedule visitation', 403);
144
+    }
145
+
146
+    return schedule_visitation;
147
+};
148
+
149
+export const updateScheduleVisitationService = async (validateData: Partial<ScheduleVisitationRequestDTO>, id: string, req: CustomRequest) => {
150
+    const visitation = await ScheduleVisitationRepository.findById(id);
151
+    if (!visitation) throw new HttpException("Schedule visitation not found", 404);
152
+
153
+    const payload: Prisma.ScheduleVisitationUpdateInput = {};
154
+
155
+    if (validateData.date_visit) {
156
+        payload.date_visit = new Date(validateData.date_visit);
157
+    }
158
+
159
+    if (typeof validateData.notes !== "undefined") {
160
+        payload.notes = validateData.notes;
161
+    }
162
+
163
+    // if (validateData.sales_id) {
164
+    //     const sales = await prisma.user.findFirst({ where: { id: validateData.sales_id } });
165
+    //     if (!sales) throw new HttpException("Sales user not found", 404);
166
+
167
+    //     payload.sales_visit = { connect: { id: validateData.sales_id } };
168
+    // }
169
+
170
+    if (validateData.hospital_id) {
171
+        const hospital = await HospitalRepository.findById(validateData.hospital_id);
172
+        if (!hospital) throw new HttpException("Hospital not found", 404);
173
+
174
+        const provinceId = hospital.province?.id;
175
+        const cityId = hospital.city?.id;
176
+        if (!provinceId || !cityId) {
177
+            throw new HttpException("Hospital does not have province or city assigned", 400);
178
+        }
179
+
180
+        payload.hospital = { connect: { id: validateData.hospital_id } };
181
+        payload.province_id = provinceId;
182
+        payload.city_id = cityId;
183
+    }
184
+
185
+    const data = await ScheduleVisitationRepository.update(id, payload);
186
+
187
+    await updateLog(req, data);
188
+};
189
+
190
+export const deleteScheduleVisitationService = async (id: string, req: CustomRequest) => {
191
+    const schedule_visitation = await ScheduleVisitationRepository.findById(id);
192
+    if (!schedule_visitation) throw new HttpException('Schedule visitation not found', 404);
193
+
194
+    const data = await ScheduleVisitationRepository.update(id, {
195
+        deletedAt: now().toDate()
196
+    });
197
+
198
+    await deleteLog(req, data);
199
+};

+ 9 - 0
src/types/admin/sales/SalesDTO.ts

@@ -0,0 +1,9 @@
1
+export interface SalesDTO {
2
+    id: string;
3
+    email: string;
4
+    firstname: string;
5
+    lastname: string;
6
+    role: string;
7
+    createdAt: Date;
8
+    updatedAt: Date;
9
+}

+ 80 - 0
src/types/admin/schedule_visitation/ScheduleVisitationDTO.ts

@@ -0,0 +1,80 @@
1
+import { ProgressStatus } from "@prisma/client";
2
+
3
+export interface ScheduleVisitationRequestDTO {
4
+    date_visit: string | Date;
5
+    sales_id: string;
6
+    hospital_id: string;
7
+    notes?: string;
8
+}
9
+
10
+export type ScheduleVisitationDTO = {
11
+    id: string;
12
+    date_visit: string | Date | null;
13
+    notes?: string | null;
14
+    createdAt: Date;
15
+    updatedAt: Date;
16
+    hospital: {
17
+        id: string;
18
+        name: string;
19
+        hospital_code: string | null;
20
+        type: string | null;
21
+        ownership: string | null;
22
+        address: string | null;
23
+        contact: string | null;
24
+        image: string | null;
25
+        email: string | null;
26
+        progress_status: ProgressStatus;
27
+        note: string | null;
28
+        latitude: number | null;
29
+        longitude: number | null;
30
+        gmaps_url: string | null;
31
+        province: { id: string; name: string };
32
+        city: { id: string; name: string };
33
+    };
34
+    user_created: { id: string; fullname: string };
35
+    sales_visit: { id: string; fullname: string };
36
+};
37
+
38
+export type ShowScheduleVisitationDTO = {
39
+    id: string;
40
+    date_visit: string | Date | null;
41
+    notes?: string | null;
42
+    createdAt: Date;
43
+    updatedAt: Date;
44
+    hospital: {
45
+        id: string;
46
+        name: string;
47
+        hospital_code: string | null;
48
+        type: string | null;
49
+        ownership: string | null;
50
+        address: string | null;
51
+        contact: string | null;
52
+        email: string | null;
53
+        image: string | null;
54
+        progress_status: ProgressStatus;
55
+        note: string | null;
56
+        latitude: number | null;
57
+        longitude: number | null;
58
+        gmaps_url: string | null;
59
+        province: { id: string; name: string };
60
+        city: { id: string; name: string };
61
+
62
+        vendor_experiences: {
63
+            id: string;
64
+            vendor: { id: string; name: string } | null;
65
+            contract_start_date: Date | null;
66
+            contract_expired_date: Date | null;
67
+            simrs_type: string;
68
+        }[];
69
+
70
+        executives_histories: {
71
+            id: string;
72
+            executive_name: string | null;
73
+            contact: string | null;
74
+            start_term: Date | null;
75
+            end_term: Date | null;
76
+        }[];
77
+    };
78
+    user_created: { id: string; fullname: string };
79
+    sales_visit: { id: string; fullname: string };
80
+};

+ 80 - 0
src/types/sales/schedule_visitation/ScheduleVisitationDTO.ts

@@ -0,0 +1,80 @@
1
+import { ProgressStatus } from "@prisma/client";
2
+
3
+export interface ScheduleVisitationRequestDTO {
4
+    date_visit: string | Date;
5
+    sales_id: string;
6
+    hospital_id: string;
7
+    notes?: string;
8
+}
9
+
10
+export type ScheduleVisitationDTO = {
11
+    id: string;
12
+    date_visit: string | Date | null;
13
+    notes?: string | null;
14
+    createdAt: Date;
15
+    updatedAt: Date;
16
+    hospital: {
17
+        id: string;
18
+        name: string;
19
+        hospital_code: string | null;
20
+        type: string | null;
21
+        ownership: string | null;
22
+        address: string | null;
23
+        contact: string | null;
24
+        image: string | null;
25
+        email: string | null;
26
+        progress_status: ProgressStatus;
27
+        note: string | null;
28
+        latitude: number | null;
29
+        longitude: number | null;
30
+        gmaps_url: string | null;
31
+        province: { id: string; name: string };
32
+        city: { id: string; name: string };
33
+    };
34
+    user_created: { id: string; fullname: string };
35
+    sales_visit: { id: string; fullname: string };
36
+};
37
+
38
+export type ShowScheduleVisitationDTO = {
39
+    id: string;
40
+    date_visit: string | Date | null;
41
+    notes?: string | null;
42
+    createdAt: Date;
43
+    updatedAt: Date;
44
+    hospital: {
45
+        id: string;
46
+        name: string;
47
+        hospital_code: string | null;
48
+        type: string | null;
49
+        ownership: string | null;
50
+        address: string | null;
51
+        contact: string | null;
52
+        email: string | null;
53
+        image: string | null;
54
+        progress_status: ProgressStatus;
55
+        note: string | null;
56
+        latitude: number | null;
57
+        longitude: number | null;
58
+        gmaps_url: string | null;
59
+        province: { id: string; name: string };
60
+        city: { id: string; name: string };
61
+
62
+        vendor_experiences: {
63
+            id: string;
64
+            vendor: { id: string; name: string } | null;
65
+            contract_start_date: Date | null;
66
+            contract_expired_date: Date | null;
67
+            simrs_type: string;
68
+        }[];
69
+
70
+        executives_histories: {
71
+            id: string;
72
+            executive_name: string | null;
73
+            contact: string | null;
74
+            start_term: Date | null;
75
+            end_term: Date | null;
76
+        }[];
77
+    };
78
+    user_created: { id: string; fullname: string };
79
+    sales_visit: { id: string; fullname: string };
80
+};

+ 34 - 0
src/validators/admin/schedule_visitation/ScheduleVisitationValidators.ts

@@ -0,0 +1,34 @@
1
+import Joi from 'joi';
2
+import { validateWithSchema } from '../../ValidateSchema';
3
+import { ScheduleVisitationRequestDTO } from '../../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4
+
5
+export const storeScheduleVisitationSchema = Joi.object({
6
+    date_visit: Joi.date().required().messages({
7
+        'any.required': 'date_visit is required',
8
+        'date.base': 'date_visit must be a valid date',
9
+    }),
10
+    sales_id: Joi.string().trim().required().messages({
11
+        'string.empty': 'sales_id is required',
12
+    }),
13
+    hospital_id: Joi.string().trim().required().messages({
14
+        'string.empty': 'hospital_id is required',
15
+    }),
16
+    notes: Joi.string().trim().optional().allow('', null),
17
+});
18
+
19
+export const updateScheduleVisitationSchema = Joi.object({
20
+    date_visit: Joi.date().optional().messages({
21
+        'date.base': 'date_visit must be a valid date',
22
+    }),
23
+    sales_id: Joi.string().trim().optional(),
24
+    hospital_id: Joi.string().trim().optional(),
25
+    notes: Joi.string().trim().optional().allow('', null),
26
+});
27
+
28
+export const validateStoreScheduleVisitationRequest = (body: unknown): ScheduleVisitationRequestDTO => {
29
+    return validateWithSchema<ScheduleVisitationRequestDTO>(storeScheduleVisitationSchema, body);
30
+}
31
+
32
+export const validateUpdateScheduleVisitationRequest = (body: unknown): Partial<ScheduleVisitationRequestDTO> => {
33
+    return validateWithSchema<Partial<ScheduleVisitationRequestDTO>>(updateScheduleVisitationSchema, body);
34
+}

+ 34 - 0
src/validators/sales/schedule_visitation/ScheduleVisitationValidators.ts

@@ -0,0 +1,34 @@
1
+import Joi from 'joi';
2
+import { validateWithSchema } from '../../ValidateSchema';
3
+import { ScheduleVisitationRequestDTO } from '../../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4
+
5
+export const storeScheduleVisitationSchema = Joi.object({
6
+    date_visit: Joi.date().required().messages({
7
+        'any.required': 'date_visit is required',
8
+        'date.base': 'date_visit must be a valid date',
9
+    }),
10
+    // sales_id: Joi.string().trim().required().messages({
11
+    //     'string.empty': 'sales_id is required',
12
+    // }),
13
+    hospital_id: Joi.string().trim().required().messages({
14
+        'string.empty': 'hospital_id is required',
15
+    }),
16
+    notes: Joi.string().trim().optional().allow('', null),
17
+});
18
+
19
+export const updateScheduleVisitationSchema = Joi.object({
20
+    date_visit: Joi.date().optional().messages({
21
+        'date.base': 'date_visit must be a valid date',
22
+    }),
23
+    // sales_id: Joi.string().trim().optional(),
24
+    hospital_id: Joi.string().trim().optional(),
25
+    notes: Joi.string().trim().optional().allow('', null),
26
+});
27
+
28
+export const validateStoreScheduleVisitationRequest = (body: unknown): ScheduleVisitationRequestDTO => {
29
+    return validateWithSchema<ScheduleVisitationRequestDTO>(storeScheduleVisitationSchema, body);
30
+}
31
+
32
+export const validateUpdateScheduleVisitationRequest = (body: unknown): Partial<ScheduleVisitationRequestDTO> => {
33
+    return validateWithSchema<Partial<ScheduleVisitationRequestDTO>>(updateScheduleVisitationSchema, body);
34
+}